diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index 2038915c29..0000000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "beta", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000000..7ac2fba5fe --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "beta", + "flavors": {} +} \ No newline at end of file diff --git a/.github/workflows/deploy_play_store.yml b/.github/workflows/deploy_play_store.yml index 94e4194305..da63783196 100644 --- a/.github/workflows/deploy_play_store.yml +++ b/.github/workflows/deploy_play_store.yml @@ -11,6 +11,7 @@ on: options: - internal - alpha + - production # Declare default permissions as read only. permissions: read-all diff --git a/.github/workflows/draft_github_release.yml b/.github/workflows/draft_github_release.yml index 4c551738d6..6bcd8d27ea 100644 --- a/.github/workflows/draft_github_release.yml +++ b/.github/workflows/draft_github_release.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Draft release with release notes id: create_release - uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 with: tag_name: ${{ github.event.inputs.version }} draft: true diff --git a/.gitignore b/.gitignore index 4376ea4237..0625ef950f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,9 @@ doc/api/ *.ipr *.iws .idea/ -.fvm/flutter_sdk + +# FVM Version Cache +.fvm/ + +# VS Code +.vscode/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56b8e1eec5..fad7f4fb8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,8 @@ ## I want to contribute code to Lichess Mobile -- [Set up your development environment](https://docs.flutter.dev/get-started/install); +- [Set up your development environment](https://github.com/lichess-org/mobile/blob/main/docs/setting_dev_env.md); - Communicate with other devs on [Discord](https://discord.gg/lichess). -- You may want to install a local instance of lila, [see lila wiki for that](https://github.com/lichess-org/lila/wiki/Lichess-Development-Onboarding); -- read the [coding style guide](https://github.com/lichess-org/mobile/blob/main/docs/coding_style.md) -- don't edit manually the `app_en.arb` file! See [about internationalization](https://github.com/lichess-org/mobile/blob/main/docs/internationalisation.md) in the docs. - check the [docs](https://github.com/lichess-org/mobile/tree/main/docs) for more documentation ### What to work on @@ -33,6 +30,22 @@ like, but check the existing pull requests to avoid duplicated work. Once you start working on an issue, submit a pull request as soon as possible (in draft mode if it's not ready yet) to let others know that you're working on it. +## Before submitting a Pull Request + +- Make sure your code follows the [coding style guide](https://github.com/lichess-org/mobile/blob/main/docs/coding_style.md) +- Don't manually edit the `app_en.arb` file! See the [internalizations docs](https://github.com/lichess-org/mobile/blob/main/docs/internationalisation.md) for instructions on how to add new translations. +- If possible, write a new widget test for your bugfix or new feature. +- Consider adding a screenshot and/or screen recording to the PR description. +- Run the linter and tests: +```sh +flutter analyze +flutter test +``` +- Ensure your code is formatted correctly (or use your editor's "format on save" feature): +```sh +dart format --output=none --set-exit-if-changed $(find lib/src -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" -o -name "*lichess_icons.dart" \) ) +dart format --output=none --set-exit-if-changed $(find test -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" \) ) +``` ## I want to report a bug or a problem about Lichess Mobile diff --git a/README.md b/README.md index 7999423c0d..d8822da6fb 100644 --- a/README.md +++ b/README.md @@ -8,90 +8,9 @@ Contributions to this project are welcome! If you want to contribute, please read the [contributing guide](./CONTRIBUTING.md). -## Setup +## Setup and Run -1. Follow the [Flutter guide](https://docs.flutter.dev/get-started/install) to - install Flutter and the platform of you choice (iOS and/or Android). **Note, if you're on Linux**, you should install flutter manually because there is an [issue](https://github.com/lichess-org/mobile/issues/123) with snapd when building Stockfish. Note that this project is not meant to be run on web platform. -2. Switch to the beta channel by running `flutter channel beta` and `flutter - upgrade` -3. Ensure Flutter is correctly configured by running `flutter doctor` - -### Flutter version - -While the app is in beta we'll use the `beta` channel of Flutter. - -#### Flutter Version Management - -If you want to use FVM to manage your Flutter versions effectively, please consult the [FVM (Flutter Version Management) guide](https://fvm.app/documentation/getting-started/installation) for comprehensive instructions on installing Flutter on your specific operating system. - -**Pro Tip:** Remember to prepend the 'flutter' prefix when using FVM commands, like this: `fvm flutter [command]`. - -### Local lila - -You will need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) instance to work on this -project. You will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) (websocket server). - -Instructions to install both are found in [the lila wiki](https://github.com/lichess-org/lila/wiki). - -The mobile application is configured by default to target `http://127.0.0.1:9663` and `ws://127.0.0.1:9664`, so keep these when installing lila. - -### Using Lichess dev server - -To use the [Lichess dev](https://lichess.dev/) to run this app, run the following command to set up the Lichess host URLs in the app. - -``` -flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev -``` - -**Note : Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** - - -## Run - -We don't commit generated code to the repository. So you need to run the code -generator first: - -```sh -flutter pub get -dart run build_runner watch -``` - -Check you have some connected device with: `flutter devices`. -If you target an android emulator you need to run these commands so the device can reach the local lila instance. - -**Note: Only run the command if you are using a local Lila server; otherwise, there's no need to set up port forwarding.** - -```sh -adb reverse tcp:9663 tcp:9663 -adb reverse tcp:9664 tcp:9664 -``` - -Then run on your device: - -```sh -flutter run -d -``` - -You can find more information about emulators [in the documentation](https://github.com/lichess-org/mobile/blob/main/docs/setting_dev_env.md). - -You can find more information about the `flutter run` command by running `flutter run --help`. - -### Test - -Before submitting a pull request, please run the tests: - -```sh -flutter analyze -flutter test -``` - -### Logging - -```sh -dart devtools -``` - -Then run the app with the `flutter run` command above, and a link to the logging page will be printed in the console. +See [the dev environment docs](./docs/setting_dev_env.md) for detailed instructions. ## Internationalisation diff --git a/analysis_options.yaml b/analysis_options.yaml index b6dfa1feec..3934e2b4b4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,8 +26,12 @@ analyzer: plugins: - custom_lint +formatter: + page_width: 100 + linter: rules: + require_trailing_commas: false prefer_single_quotes: true always_use_package_imports: false avoid_redundant_argument_values: false diff --git a/android/Gemfile.lock b/android/Gemfile.lock index cbd2b355ea..4168b7b944 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -5,25 +5,25 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.934.0) - aws-sdk-core (3.196.1) + aws-partitions (1.1013.0) + aws-sdk-core (3.213.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.82.0) - aws-sdk-core (~> 3, >= 3.193.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.173.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -60,15 +60,15 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.221.1) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,32 +150,31 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.8.2) + jwt (2.9.3) base64 - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.5) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.3) - strscan + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,7 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -195,14 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/android/app/build.gradle b/android/app/build.gradle index 860c8c35d0..ff93ebd5f9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -36,6 +36,7 @@ android { ndkVersion flutter.ndkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -49,6 +50,7 @@ android { } defaultConfig { + multiDexEnabled true applicationId "org.lichess.mobileV2" minSdkVersion 26 targetSdkVersion flutter.targetSdkVersion @@ -96,4 +98,5 @@ flutter { } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 00a7e574f6..8cfc52e999 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -2,3 +2,36 @@ -keep class java.net.URL { *; } -keep class java.util.concurrent.Executors { *; } -keep class org.chromium.net.** { *; } + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +##---------------End: proguard configuration for Gson ---------- diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f21c4f27c7..ac5dfc7a14 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,11 +15,22 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" + android:resizeableActivity="false" android:fullBackupContent="@xml/backup_rules"> + + + + + diff --git a/android/app/src/main/res/drawable/cross.png b/android/app/src/main/res/drawable/cross.png new file mode 100644 index 0000000000..a0ae4179ba Binary files /dev/null and b/android/app/src/main/res/drawable/cross.png differ diff --git a/android/app/src/main/res/drawable/logo_black.png b/android/app/src/main/res/drawable/logo_black.png new file mode 100644 index 0000000000..b844aa2546 Binary files /dev/null and b/android/app/src/main/res/drawable/logo_black.png differ diff --git a/android/app/src/main/res/drawable/tick.png b/android/app/src/main/res/drawable/tick.png new file mode 100644 index 0000000000..a4ab554ef4 Binary files /dev/null and b/android/app/src/main/res/drawable/tick.png differ diff --git a/android/app/src/main/res/raw/org_lichess_mobile_keep.xml b/android/app/src/main/res/raw/org_lichess_mobile_keep.xml new file mode 100644 index 0000000000..7d4fe18883 --- /dev/null +++ b/android/app/src/main/res/raw/org_lichess_mobile_keep.xml @@ -0,0 +1,3 @@ + + diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 63d6088225..f3f523549b 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -29,6 +29,13 @@ platform :android do end end +platform :android do + desc "Upload a production version to Google Play" + lane :production do + deploy_to_play_store('production') + end +end + def deploy_to_play_store(track) sh "flutter build appbundle -v --obfuscate --split-debug-info=./build/app/outputs/bundle/release/symbols --dart-define=cronetHttpNoPlay=true --dart-define=LICHESS_HOST=lichess.org --dart-define=LICHESS_WS_HOST=socket.lichess.org --dart-define=LICHESS_WS_SECRET=#{ENV['WS_SECRET']}" upload_to_play_store( diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6..8bc9958ab0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 662e8f0f62..5a558f4d5b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.4.2" apply false + id "com.android.application" version '8.1.0' apply false id "org.jetbrains.kotlin.android" version "1.8.21" apply false id "com.google.gms.google-services" version "4.4.0" apply false id "com.google.firebase.crashlytics" version "2.9.9" apply false diff --git a/assets/fonts/LichessIcons.ttf b/assets/fonts/LichessIcons.ttf index 4a9a94b90a..faa08325e3 100644 Binary files a/assets/fonts/LichessIcons.ttf and b/assets/fonts/LichessIcons.ttf differ diff --git a/assets/images/broadcast_image.png b/assets/images/broadcast_image.png new file mode 100644 index 0000000000..fed57f3293 Binary files /dev/null and b/assets/images/broadcast_image.png differ diff --git a/assets/images/fide-fed/AFG.png b/assets/images/fide-fed/AFG.png new file mode 100644 index 0000000000..c8a7005e40 Binary files /dev/null and b/assets/images/fide-fed/AFG.png differ diff --git a/assets/images/fide-fed/AHO.png b/assets/images/fide-fed/AHO.png new file mode 100644 index 0000000000..819b66da07 Binary files /dev/null and b/assets/images/fide-fed/AHO.png differ diff --git a/assets/images/fide-fed/ALB.png b/assets/images/fide-fed/ALB.png new file mode 100644 index 0000000000..c7be70dc01 Binary files /dev/null and b/assets/images/fide-fed/ALB.png differ diff --git a/assets/images/fide-fed/ALG.png b/assets/images/fide-fed/ALG.png new file mode 100644 index 0000000000..9adce557cc Binary files /dev/null and b/assets/images/fide-fed/ALG.png differ diff --git a/assets/images/fide-fed/AND.png b/assets/images/fide-fed/AND.png new file mode 100644 index 0000000000..fd2f7d4804 Binary files /dev/null and b/assets/images/fide-fed/AND.png differ diff --git a/assets/images/fide-fed/ANG.png b/assets/images/fide-fed/ANG.png new file mode 100644 index 0000000000..3ed31cb922 Binary files /dev/null and b/assets/images/fide-fed/ANG.png differ diff --git a/assets/images/fide-fed/ANT.png b/assets/images/fide-fed/ANT.png new file mode 100644 index 0000000000..3c26640b7d Binary files /dev/null and b/assets/images/fide-fed/ANT.png differ diff --git a/assets/images/fide-fed/ARG.png b/assets/images/fide-fed/ARG.png new file mode 100644 index 0000000000..fe9e946829 Binary files /dev/null and b/assets/images/fide-fed/ARG.png differ diff --git a/assets/images/fide-fed/ARM.png b/assets/images/fide-fed/ARM.png new file mode 100644 index 0000000000..a08aefafd7 Binary files /dev/null and b/assets/images/fide-fed/ARM.png differ diff --git a/assets/images/fide-fed/ARU.png b/assets/images/fide-fed/ARU.png new file mode 100644 index 0000000000..fd8df3a8d4 Binary files /dev/null and b/assets/images/fide-fed/ARU.png differ diff --git a/assets/images/fide-fed/AUS.png b/assets/images/fide-fed/AUS.png new file mode 100644 index 0000000000..c5ab755e48 Binary files /dev/null and b/assets/images/fide-fed/AUS.png differ diff --git a/assets/images/fide-fed/AUT.png b/assets/images/fide-fed/AUT.png new file mode 100644 index 0000000000..b84713cea5 Binary files /dev/null and b/assets/images/fide-fed/AUT.png differ diff --git a/assets/images/fide-fed/AZE.png b/assets/images/fide-fed/AZE.png new file mode 100644 index 0000000000..1cc78ac19c Binary files /dev/null and b/assets/images/fide-fed/AZE.png differ diff --git a/assets/images/fide-fed/BAH.png b/assets/images/fide-fed/BAH.png new file mode 100644 index 0000000000..431ca8acfb Binary files /dev/null and b/assets/images/fide-fed/BAH.png differ diff --git a/assets/images/fide-fed/BAN.png b/assets/images/fide-fed/BAN.png new file mode 100644 index 0000000000..d11ce55900 Binary files /dev/null and b/assets/images/fide-fed/BAN.png differ diff --git a/assets/images/fide-fed/BAR.png b/assets/images/fide-fed/BAR.png new file mode 100644 index 0000000000..e7f0df4882 Binary files /dev/null and b/assets/images/fide-fed/BAR.png differ diff --git a/assets/images/fide-fed/BDI.png b/assets/images/fide-fed/BDI.png new file mode 100644 index 0000000000..0411b99d14 Binary files /dev/null and b/assets/images/fide-fed/BDI.png differ diff --git a/assets/images/fide-fed/BEL.png b/assets/images/fide-fed/BEL.png new file mode 100644 index 0000000000..80422e766a Binary files /dev/null and b/assets/images/fide-fed/BEL.png differ diff --git a/assets/images/fide-fed/BER.png b/assets/images/fide-fed/BER.png new file mode 100644 index 0000000000..4f05b1dbfe Binary files /dev/null and b/assets/images/fide-fed/BER.png differ diff --git a/assets/images/fide-fed/BHU.png b/assets/images/fide-fed/BHU.png new file mode 100644 index 0000000000..be5f4acfa2 Binary files /dev/null and b/assets/images/fide-fed/BHU.png differ diff --git a/assets/images/fide-fed/BIH.png b/assets/images/fide-fed/BIH.png new file mode 100644 index 0000000000..92b805db9a Binary files /dev/null and b/assets/images/fide-fed/BIH.png differ diff --git a/assets/images/fide-fed/BIZ.png b/assets/images/fide-fed/BIZ.png new file mode 100644 index 0000000000..000bf146a4 Binary files /dev/null and b/assets/images/fide-fed/BIZ.png differ diff --git a/assets/images/fide-fed/BLR.png b/assets/images/fide-fed/BLR.png new file mode 100644 index 0000000000..cc87102a65 Binary files /dev/null and b/assets/images/fide-fed/BLR.png differ diff --git a/assets/images/fide-fed/BOL.png b/assets/images/fide-fed/BOL.png new file mode 100644 index 0000000000..6335fab640 Binary files /dev/null and b/assets/images/fide-fed/BOL.png differ diff --git a/assets/images/fide-fed/BOT.png b/assets/images/fide-fed/BOT.png new file mode 100644 index 0000000000..05eead0171 Binary files /dev/null and b/assets/images/fide-fed/BOT.png differ diff --git a/assets/images/fide-fed/BRA.png b/assets/images/fide-fed/BRA.png new file mode 100644 index 0000000000..3fdab925af Binary files /dev/null and b/assets/images/fide-fed/BRA.png differ diff --git a/assets/images/fide-fed/BRN.png b/assets/images/fide-fed/BRN.png new file mode 100644 index 0000000000..680c53237c Binary files /dev/null and b/assets/images/fide-fed/BRN.png differ diff --git a/assets/images/fide-fed/BRU.png b/assets/images/fide-fed/BRU.png new file mode 100644 index 0000000000..88a5cec650 Binary files /dev/null and b/assets/images/fide-fed/BRU.png differ diff --git a/assets/images/fide-fed/BUL.png b/assets/images/fide-fed/BUL.png new file mode 100644 index 0000000000..b8af81b6d7 Binary files /dev/null and b/assets/images/fide-fed/BUL.png differ diff --git a/assets/images/fide-fed/BUR.png b/assets/images/fide-fed/BUR.png new file mode 100644 index 0000000000..90835deec9 Binary files /dev/null and b/assets/images/fide-fed/BUR.png differ diff --git a/assets/images/fide-fed/CAF.png b/assets/images/fide-fed/CAF.png new file mode 100644 index 0000000000..5474902fd4 Binary files /dev/null and b/assets/images/fide-fed/CAF.png differ diff --git a/assets/images/fide-fed/CAM.png b/assets/images/fide-fed/CAM.png new file mode 100644 index 0000000000..e8f8d84187 Binary files /dev/null and b/assets/images/fide-fed/CAM.png differ diff --git a/assets/images/fide-fed/CAN.png b/assets/images/fide-fed/CAN.png new file mode 100644 index 0000000000..376801d059 Binary files /dev/null and b/assets/images/fide-fed/CAN.png differ diff --git a/assets/images/fide-fed/CAY.png b/assets/images/fide-fed/CAY.png new file mode 100644 index 0000000000..89612f0cff Binary files /dev/null and b/assets/images/fide-fed/CAY.png differ diff --git a/assets/images/fide-fed/CHA.png b/assets/images/fide-fed/CHA.png new file mode 100644 index 0000000000..281eca2d7c Binary files /dev/null and b/assets/images/fide-fed/CHA.png differ diff --git a/assets/images/fide-fed/CHI.png b/assets/images/fide-fed/CHI.png new file mode 100644 index 0000000000..8bd17f2780 Binary files /dev/null and b/assets/images/fide-fed/CHI.png differ diff --git a/assets/images/fide-fed/CHN.png b/assets/images/fide-fed/CHN.png new file mode 100644 index 0000000000..47f623d664 Binary files /dev/null and b/assets/images/fide-fed/CHN.png differ diff --git a/assets/images/fide-fed/CIV.png b/assets/images/fide-fed/CIV.png new file mode 100644 index 0000000000..d59c2ce96e Binary files /dev/null and b/assets/images/fide-fed/CIV.png differ diff --git a/assets/images/fide-fed/CMR.png b/assets/images/fide-fed/CMR.png new file mode 100644 index 0000000000..4e44a591a7 Binary files /dev/null and b/assets/images/fide-fed/CMR.png differ diff --git a/assets/images/fide-fed/COD.png b/assets/images/fide-fed/COD.png new file mode 100644 index 0000000000..3339a170d8 Binary files /dev/null and b/assets/images/fide-fed/COD.png differ diff --git a/assets/images/fide-fed/COL.png b/assets/images/fide-fed/COL.png new file mode 100644 index 0000000000..38c890d744 Binary files /dev/null and b/assets/images/fide-fed/COL.png differ diff --git a/assets/images/fide-fed/COM.png b/assets/images/fide-fed/COM.png new file mode 100644 index 0000000000..67d9023f06 Binary files /dev/null and b/assets/images/fide-fed/COM.png differ diff --git a/assets/images/fide-fed/CPV.png b/assets/images/fide-fed/CPV.png new file mode 100644 index 0000000000..a294b11a84 Binary files /dev/null and b/assets/images/fide-fed/CPV.png differ diff --git a/assets/images/fide-fed/CRC.png b/assets/images/fide-fed/CRC.png new file mode 100644 index 0000000000..fff60173bf Binary files /dev/null and b/assets/images/fide-fed/CRC.png differ diff --git a/assets/images/fide-fed/CRO.png b/assets/images/fide-fed/CRO.png new file mode 100644 index 0000000000..faf263b2f2 Binary files /dev/null and b/assets/images/fide-fed/CRO.png differ diff --git a/assets/images/fide-fed/CUB.png b/assets/images/fide-fed/CUB.png new file mode 100644 index 0000000000..cb25d66e95 Binary files /dev/null and b/assets/images/fide-fed/CUB.png differ diff --git a/assets/images/fide-fed/CYP.png b/assets/images/fide-fed/CYP.png new file mode 100644 index 0000000000..61a28aa9c5 Binary files /dev/null and b/assets/images/fide-fed/CYP.png differ diff --git a/assets/images/fide-fed/CZE.png b/assets/images/fide-fed/CZE.png new file mode 100644 index 0000000000..42f4f7dc6b Binary files /dev/null and b/assets/images/fide-fed/CZE.png differ diff --git a/assets/images/fide-fed/DEN.png b/assets/images/fide-fed/DEN.png new file mode 100644 index 0000000000..43912e8e84 Binary files /dev/null and b/assets/images/fide-fed/DEN.png differ diff --git a/assets/images/fide-fed/DJI.png b/assets/images/fide-fed/DJI.png new file mode 100644 index 0000000000..0a29d21f2e Binary files /dev/null and b/assets/images/fide-fed/DJI.png differ diff --git a/assets/images/fide-fed/DMA.png b/assets/images/fide-fed/DMA.png new file mode 100644 index 0000000000..90a61bad31 Binary files /dev/null and b/assets/images/fide-fed/DMA.png differ diff --git a/assets/images/fide-fed/DOM.png b/assets/images/fide-fed/DOM.png new file mode 100644 index 0000000000..b2917eab7e Binary files /dev/null and b/assets/images/fide-fed/DOM.png differ diff --git a/assets/images/fide-fed/ECU.png b/assets/images/fide-fed/ECU.png new file mode 100644 index 0000000000..e3662e038b Binary files /dev/null and b/assets/images/fide-fed/ECU.png differ diff --git a/assets/images/fide-fed/EGY.png b/assets/images/fide-fed/EGY.png new file mode 100644 index 0000000000..5ad1d5dd00 Binary files /dev/null and b/assets/images/fide-fed/EGY.png differ diff --git a/assets/images/fide-fed/ENG.png b/assets/images/fide-fed/ENG.png new file mode 100644 index 0000000000..626793a3cf Binary files /dev/null and b/assets/images/fide-fed/ENG.png differ diff --git a/assets/images/fide-fed/ERI.png b/assets/images/fide-fed/ERI.png new file mode 100644 index 0000000000..04bd3de493 Binary files /dev/null and b/assets/images/fide-fed/ERI.png differ diff --git a/assets/images/fide-fed/ESA.png b/assets/images/fide-fed/ESA.png new file mode 100644 index 0000000000..84ed17a1ac Binary files /dev/null and b/assets/images/fide-fed/ESA.png differ diff --git a/assets/images/fide-fed/ESP.png b/assets/images/fide-fed/ESP.png new file mode 100644 index 0000000000..2032276732 Binary files /dev/null and b/assets/images/fide-fed/ESP.png differ diff --git a/assets/images/fide-fed/EST.png b/assets/images/fide-fed/EST.png new file mode 100644 index 0000000000..8bc918f98b Binary files /dev/null and b/assets/images/fide-fed/EST.png differ diff --git a/assets/images/fide-fed/ETH.png b/assets/images/fide-fed/ETH.png new file mode 100644 index 0000000000..56361d8041 Binary files /dev/null and b/assets/images/fide-fed/ETH.png differ diff --git a/assets/images/fide-fed/FAI.png b/assets/images/fide-fed/FAI.png new file mode 100644 index 0000000000..7dc2ac3aa2 Binary files /dev/null and b/assets/images/fide-fed/FAI.png differ diff --git a/assets/images/fide-fed/FID.png b/assets/images/fide-fed/FID.png new file mode 100644 index 0000000000..ac0a1bee02 Binary files /dev/null and b/assets/images/fide-fed/FID.png differ diff --git a/assets/images/fide-fed/FIJ.png b/assets/images/fide-fed/FIJ.png new file mode 100644 index 0000000000..738282ac2c Binary files /dev/null and b/assets/images/fide-fed/FIJ.png differ diff --git a/assets/images/fide-fed/FIN.png b/assets/images/fide-fed/FIN.png new file mode 100644 index 0000000000..ea639f4cb8 Binary files /dev/null and b/assets/images/fide-fed/FIN.png differ diff --git a/assets/images/fide-fed/FRA.png b/assets/images/fide-fed/FRA.png new file mode 100644 index 0000000000..25b3d3368c Binary files /dev/null and b/assets/images/fide-fed/FRA.png differ diff --git a/assets/images/fide-fed/GAB.png b/assets/images/fide-fed/GAB.png new file mode 100644 index 0000000000..350963bee9 Binary files /dev/null and b/assets/images/fide-fed/GAB.png differ diff --git a/assets/images/fide-fed/GAM.png b/assets/images/fide-fed/GAM.png new file mode 100644 index 0000000000..1f7eea0796 Binary files /dev/null and b/assets/images/fide-fed/GAM.png differ diff --git a/assets/images/fide-fed/GCI.png b/assets/images/fide-fed/GCI.png new file mode 100644 index 0000000000..0b7788d4b8 Binary files /dev/null and b/assets/images/fide-fed/GCI.png differ diff --git a/assets/images/fide-fed/GEO.png b/assets/images/fide-fed/GEO.png new file mode 100644 index 0000000000..ccf4c640da Binary files /dev/null and b/assets/images/fide-fed/GEO.png differ diff --git a/assets/images/fide-fed/GEQ.png b/assets/images/fide-fed/GEQ.png new file mode 100644 index 0000000000..e770ebf7b8 Binary files /dev/null and b/assets/images/fide-fed/GEQ.png differ diff --git a/assets/images/fide-fed/GER.png b/assets/images/fide-fed/GER.png new file mode 100644 index 0000000000..0f9d5a0369 Binary files /dev/null and b/assets/images/fide-fed/GER.png differ diff --git a/assets/images/fide-fed/GHA.png b/assets/images/fide-fed/GHA.png new file mode 100644 index 0000000000..fbd57bf515 Binary files /dev/null and b/assets/images/fide-fed/GHA.png differ diff --git a/assets/images/fide-fed/GRE.png b/assets/images/fide-fed/GRE.png new file mode 100644 index 0000000000..84bfae716a Binary files /dev/null and b/assets/images/fide-fed/GRE.png differ diff --git a/assets/images/fide-fed/GRN.png b/assets/images/fide-fed/GRN.png new file mode 100644 index 0000000000..f162e5b157 Binary files /dev/null and b/assets/images/fide-fed/GRN.png differ diff --git a/assets/images/fide-fed/GUA.png b/assets/images/fide-fed/GUA.png new file mode 100644 index 0000000000..9ded98238b Binary files /dev/null and b/assets/images/fide-fed/GUA.png differ diff --git a/assets/images/fide-fed/GUM.png b/assets/images/fide-fed/GUM.png new file mode 100644 index 0000000000..eebcd75fb2 Binary files /dev/null and b/assets/images/fide-fed/GUM.png differ diff --git a/assets/images/fide-fed/GUY.png b/assets/images/fide-fed/GUY.png new file mode 100644 index 0000000000..8ab03d3b97 Binary files /dev/null and b/assets/images/fide-fed/GUY.png differ diff --git a/assets/images/fide-fed/HAI.png b/assets/images/fide-fed/HAI.png new file mode 100644 index 0000000000..1ae857ea54 Binary files /dev/null and b/assets/images/fide-fed/HAI.png differ diff --git a/assets/images/fide-fed/HKG.png b/assets/images/fide-fed/HKG.png new file mode 100644 index 0000000000..8d05655c25 Binary files /dev/null and b/assets/images/fide-fed/HKG.png differ diff --git a/assets/images/fide-fed/HON.png b/assets/images/fide-fed/HON.png new file mode 100644 index 0000000000..afe4a38f9a Binary files /dev/null and b/assets/images/fide-fed/HON.png differ diff --git a/assets/images/fide-fed/HUN.png b/assets/images/fide-fed/HUN.png new file mode 100644 index 0000000000..c8d60ec6b0 Binary files /dev/null and b/assets/images/fide-fed/HUN.png differ diff --git a/assets/images/fide-fed/INA.png b/assets/images/fide-fed/INA.png new file mode 100644 index 0000000000..790d711d8d Binary files /dev/null and b/assets/images/fide-fed/INA.png differ diff --git a/assets/images/fide-fed/IND.png b/assets/images/fide-fed/IND.png new file mode 100644 index 0000000000..d5fa3a810d Binary files /dev/null and b/assets/images/fide-fed/IND.png differ diff --git a/assets/images/fide-fed/IOM.png b/assets/images/fide-fed/IOM.png new file mode 100644 index 0000000000..5aea22623f Binary files /dev/null and b/assets/images/fide-fed/IOM.png differ diff --git a/assets/images/fide-fed/IRI.png b/assets/images/fide-fed/IRI.png new file mode 100644 index 0000000000..fee14e1eda Binary files /dev/null and b/assets/images/fide-fed/IRI.png differ diff --git a/assets/images/fide-fed/IRL.png b/assets/images/fide-fed/IRL.png new file mode 100644 index 0000000000..57078f02a2 Binary files /dev/null and b/assets/images/fide-fed/IRL.png differ diff --git a/assets/images/fide-fed/IRQ.png b/assets/images/fide-fed/IRQ.png new file mode 100644 index 0000000000..53619bb3ef Binary files /dev/null and b/assets/images/fide-fed/IRQ.png differ diff --git a/assets/images/fide-fed/ISL.png b/assets/images/fide-fed/ISL.png new file mode 100644 index 0000000000..e296c3c9a7 Binary files /dev/null and b/assets/images/fide-fed/ISL.png differ diff --git a/assets/images/fide-fed/ISR.png b/assets/images/fide-fed/ISR.png new file mode 100644 index 0000000000..1fecb98023 Binary files /dev/null and b/assets/images/fide-fed/ISR.png differ diff --git a/assets/images/fide-fed/ISV.png b/assets/images/fide-fed/ISV.png new file mode 100644 index 0000000000..907703be7c Binary files /dev/null and b/assets/images/fide-fed/ISV.png differ diff --git a/assets/images/fide-fed/ITA.png b/assets/images/fide-fed/ITA.png new file mode 100644 index 0000000000..f250cb068a Binary files /dev/null and b/assets/images/fide-fed/ITA.png differ diff --git a/assets/images/fide-fed/IVB.png b/assets/images/fide-fed/IVB.png new file mode 100644 index 0000000000..ca02273a6f Binary files /dev/null and b/assets/images/fide-fed/IVB.png differ diff --git a/assets/images/fide-fed/JAM.png b/assets/images/fide-fed/JAM.png new file mode 100644 index 0000000000..ab65c2d32d Binary files /dev/null and b/assets/images/fide-fed/JAM.png differ diff --git a/assets/images/fide-fed/JCI.png b/assets/images/fide-fed/JCI.png new file mode 100644 index 0000000000..427fa1ac03 Binary files /dev/null and b/assets/images/fide-fed/JCI.png differ diff --git a/assets/images/fide-fed/JOR.png b/assets/images/fide-fed/JOR.png new file mode 100644 index 0000000000..2d54302439 Binary files /dev/null and b/assets/images/fide-fed/JOR.png differ diff --git a/assets/images/fide-fed/JPN.png b/assets/images/fide-fed/JPN.png new file mode 100644 index 0000000000..d0b941649c Binary files /dev/null and b/assets/images/fide-fed/JPN.png differ diff --git a/assets/images/fide-fed/KAZ.png b/assets/images/fide-fed/KAZ.png new file mode 100644 index 0000000000..8bce39af9f Binary files /dev/null and b/assets/images/fide-fed/KAZ.png differ diff --git a/assets/images/fide-fed/KEN.png b/assets/images/fide-fed/KEN.png new file mode 100644 index 0000000000..4ac40e7f82 Binary files /dev/null and b/assets/images/fide-fed/KEN.png differ diff --git a/assets/images/fide-fed/KGZ.png b/assets/images/fide-fed/KGZ.png new file mode 100644 index 0000000000..cde59d53ac Binary files /dev/null and b/assets/images/fide-fed/KGZ.png differ diff --git a/assets/images/fide-fed/KOR.png b/assets/images/fide-fed/KOR.png new file mode 100644 index 0000000000..52baf8dfd6 Binary files /dev/null and b/assets/images/fide-fed/KOR.png differ diff --git a/assets/images/fide-fed/KOS.png b/assets/images/fide-fed/KOS.png new file mode 100644 index 0000000000..cd8a9ca0d2 Binary files /dev/null and b/assets/images/fide-fed/KOS.png differ diff --git a/assets/images/fide-fed/KSA.png b/assets/images/fide-fed/KSA.png new file mode 100644 index 0000000000..9881dc9dbc Binary files /dev/null and b/assets/images/fide-fed/KSA.png differ diff --git a/assets/images/fide-fed/KUW.png b/assets/images/fide-fed/KUW.png new file mode 100644 index 0000000000..806d8c6282 Binary files /dev/null and b/assets/images/fide-fed/KUW.png differ diff --git a/assets/images/fide-fed/LAO.png b/assets/images/fide-fed/LAO.png new file mode 100644 index 0000000000..a17f66bce0 Binary files /dev/null and b/assets/images/fide-fed/LAO.png differ diff --git a/assets/images/fide-fed/LAT.png b/assets/images/fide-fed/LAT.png new file mode 100644 index 0000000000..5c51383708 Binary files /dev/null and b/assets/images/fide-fed/LAT.png differ diff --git a/assets/images/fide-fed/LBA.png b/assets/images/fide-fed/LBA.png new file mode 100644 index 0000000000..771b53c2fc Binary files /dev/null and b/assets/images/fide-fed/LBA.png differ diff --git a/assets/images/fide-fed/LBN.png b/assets/images/fide-fed/LBN.png new file mode 100644 index 0000000000..71fb1f6429 Binary files /dev/null and b/assets/images/fide-fed/LBN.png differ diff --git a/assets/images/fide-fed/LBR.png b/assets/images/fide-fed/LBR.png new file mode 100644 index 0000000000..367d77c2d1 Binary files /dev/null and b/assets/images/fide-fed/LBR.png differ diff --git a/assets/images/fide-fed/LCA.png b/assets/images/fide-fed/LCA.png new file mode 100644 index 0000000000..827610f5a0 Binary files /dev/null and b/assets/images/fide-fed/LCA.png differ diff --git a/assets/images/fide-fed/LES.png b/assets/images/fide-fed/LES.png new file mode 100644 index 0000000000..f04b741c34 Binary files /dev/null and b/assets/images/fide-fed/LES.png differ diff --git a/assets/images/fide-fed/LIE.png b/assets/images/fide-fed/LIE.png new file mode 100644 index 0000000000..6c82df7dea Binary files /dev/null and b/assets/images/fide-fed/LIE.png differ diff --git a/assets/images/fide-fed/LTU.png b/assets/images/fide-fed/LTU.png new file mode 100644 index 0000000000..197142273b Binary files /dev/null and b/assets/images/fide-fed/LTU.png differ diff --git a/assets/images/fide-fed/LUX.png b/assets/images/fide-fed/LUX.png new file mode 100644 index 0000000000..8e407908fa Binary files /dev/null and b/assets/images/fide-fed/LUX.png differ diff --git a/assets/images/fide-fed/MAC.png b/assets/images/fide-fed/MAC.png new file mode 100644 index 0000000000..4ab76a076d Binary files /dev/null and b/assets/images/fide-fed/MAC.png differ diff --git a/assets/images/fide-fed/MAD.png b/assets/images/fide-fed/MAD.png new file mode 100644 index 0000000000..9aecc1a2bf Binary files /dev/null and b/assets/images/fide-fed/MAD.png differ diff --git a/assets/images/fide-fed/MAR.png b/assets/images/fide-fed/MAR.png new file mode 100644 index 0000000000..d650c8813f Binary files /dev/null and b/assets/images/fide-fed/MAR.png differ diff --git a/assets/images/fide-fed/MAS.png b/assets/images/fide-fed/MAS.png new file mode 100644 index 0000000000..f50d77f267 Binary files /dev/null and b/assets/images/fide-fed/MAS.png differ diff --git a/assets/images/fide-fed/MAW.png b/assets/images/fide-fed/MAW.png new file mode 100644 index 0000000000..be07dbd289 Binary files /dev/null and b/assets/images/fide-fed/MAW.png differ diff --git a/assets/images/fide-fed/MDA.png b/assets/images/fide-fed/MDA.png new file mode 100644 index 0000000000..b14b9af682 Binary files /dev/null and b/assets/images/fide-fed/MDA.png differ diff --git a/assets/images/fide-fed/MDV.png b/assets/images/fide-fed/MDV.png new file mode 100644 index 0000000000..8b550c9929 Binary files /dev/null and b/assets/images/fide-fed/MDV.png differ diff --git a/assets/images/fide-fed/MEX.png b/assets/images/fide-fed/MEX.png new file mode 100644 index 0000000000..c8b987c9c7 Binary files /dev/null and b/assets/images/fide-fed/MEX.png differ diff --git a/assets/images/fide-fed/MGL.png b/assets/images/fide-fed/MGL.png new file mode 100644 index 0000000000..4a385c7092 Binary files /dev/null and b/assets/images/fide-fed/MGL.png differ diff --git a/assets/images/fide-fed/MKD.png b/assets/images/fide-fed/MKD.png new file mode 100644 index 0000000000..40ca5211ca Binary files /dev/null and b/assets/images/fide-fed/MKD.png differ diff --git a/assets/images/fide-fed/MLI.png b/assets/images/fide-fed/MLI.png new file mode 100644 index 0000000000..506c53f263 Binary files /dev/null and b/assets/images/fide-fed/MLI.png differ diff --git a/assets/images/fide-fed/MLT.png b/assets/images/fide-fed/MLT.png new file mode 100644 index 0000000000..59bab487a3 Binary files /dev/null and b/assets/images/fide-fed/MLT.png differ diff --git a/assets/images/fide-fed/MNC.png b/assets/images/fide-fed/MNC.png new file mode 100644 index 0000000000..0d65a7eda0 Binary files /dev/null and b/assets/images/fide-fed/MNC.png differ diff --git a/assets/images/fide-fed/MNE.png b/assets/images/fide-fed/MNE.png new file mode 100644 index 0000000000..cfbbfe52f0 Binary files /dev/null and b/assets/images/fide-fed/MNE.png differ diff --git a/assets/images/fide-fed/MOZ.png b/assets/images/fide-fed/MOZ.png new file mode 100644 index 0000000000..8690e7bbd5 Binary files /dev/null and b/assets/images/fide-fed/MOZ.png differ diff --git a/assets/images/fide-fed/MRI.png b/assets/images/fide-fed/MRI.png new file mode 100644 index 0000000000..9353d6d6b1 Binary files /dev/null and b/assets/images/fide-fed/MRI.png differ diff --git a/assets/images/fide-fed/MTN.png b/assets/images/fide-fed/MTN.png new file mode 100644 index 0000000000..1574aa6dff Binary files /dev/null and b/assets/images/fide-fed/MTN.png differ diff --git a/assets/images/fide-fed/MYA.png b/assets/images/fide-fed/MYA.png new file mode 100644 index 0000000000..4062fe3fa0 Binary files /dev/null and b/assets/images/fide-fed/MYA.png differ diff --git a/assets/images/fide-fed/NAM.png b/assets/images/fide-fed/NAM.png new file mode 100644 index 0000000000..d8bbdbae2e Binary files /dev/null and b/assets/images/fide-fed/NAM.png differ diff --git a/assets/images/fide-fed/NCA.png b/assets/images/fide-fed/NCA.png new file mode 100644 index 0000000000..86f69d554c Binary files /dev/null and b/assets/images/fide-fed/NCA.png differ diff --git a/assets/images/fide-fed/NED.png b/assets/images/fide-fed/NED.png new file mode 100644 index 0000000000..819b66da07 Binary files /dev/null and b/assets/images/fide-fed/NED.png differ diff --git a/assets/images/fide-fed/NEP.png b/assets/images/fide-fed/NEP.png new file mode 100644 index 0000000000..b5ecefb0df Binary files /dev/null and b/assets/images/fide-fed/NEP.png differ diff --git a/assets/images/fide-fed/NGR.png b/assets/images/fide-fed/NGR.png new file mode 100644 index 0000000000..663c4076ea Binary files /dev/null and b/assets/images/fide-fed/NGR.png differ diff --git a/assets/images/fide-fed/NIG.png b/assets/images/fide-fed/NIG.png new file mode 100644 index 0000000000..7a75a84a1c Binary files /dev/null and b/assets/images/fide-fed/NIG.png differ diff --git a/assets/images/fide-fed/NOR.png b/assets/images/fide-fed/NOR.png new file mode 100644 index 0000000000..bb080da527 Binary files /dev/null and b/assets/images/fide-fed/NOR.png differ diff --git a/assets/images/fide-fed/NRU.png b/assets/images/fide-fed/NRU.png new file mode 100644 index 0000000000..28551edf92 Binary files /dev/null and b/assets/images/fide-fed/NRU.png differ diff --git a/assets/images/fide-fed/NZL.png b/assets/images/fide-fed/NZL.png new file mode 100644 index 0000000000..1fdee26a96 Binary files /dev/null and b/assets/images/fide-fed/NZL.png differ diff --git a/assets/images/fide-fed/OMA.png b/assets/images/fide-fed/OMA.png new file mode 100644 index 0000000000..820936e4c8 Binary files /dev/null and b/assets/images/fide-fed/OMA.png differ diff --git a/assets/images/fide-fed/PAK.png b/assets/images/fide-fed/PAK.png new file mode 100644 index 0000000000..ae8207459d Binary files /dev/null and b/assets/images/fide-fed/PAK.png differ diff --git a/assets/images/fide-fed/PAN.png b/assets/images/fide-fed/PAN.png new file mode 100644 index 0000000000..34237dbcea Binary files /dev/null and b/assets/images/fide-fed/PAN.png differ diff --git a/assets/images/fide-fed/PAR.png b/assets/images/fide-fed/PAR.png new file mode 100644 index 0000000000..deb01d2307 Binary files /dev/null and b/assets/images/fide-fed/PAR.png differ diff --git a/assets/images/fide-fed/PER.png b/assets/images/fide-fed/PER.png new file mode 100644 index 0000000000..03561cabd4 Binary files /dev/null and b/assets/images/fide-fed/PER.png differ diff --git a/assets/images/fide-fed/PHI.png b/assets/images/fide-fed/PHI.png new file mode 100644 index 0000000000..ac36fafe49 Binary files /dev/null and b/assets/images/fide-fed/PHI.png differ diff --git a/assets/images/fide-fed/PLE.png b/assets/images/fide-fed/PLE.png new file mode 100644 index 0000000000..e37a851d92 Binary files /dev/null and b/assets/images/fide-fed/PLE.png differ diff --git a/assets/images/fide-fed/PLW.png b/assets/images/fide-fed/PLW.png new file mode 100644 index 0000000000..ec846bac45 Binary files /dev/null and b/assets/images/fide-fed/PLW.png differ diff --git a/assets/images/fide-fed/PNG.png b/assets/images/fide-fed/PNG.png new file mode 100644 index 0000000000..a648db1c6a Binary files /dev/null and b/assets/images/fide-fed/PNG.png differ diff --git a/assets/images/fide-fed/POL.png b/assets/images/fide-fed/POL.png new file mode 100644 index 0000000000..28fa067209 Binary files /dev/null and b/assets/images/fide-fed/POL.png differ diff --git a/assets/images/fide-fed/POR.png b/assets/images/fide-fed/POR.png new file mode 100644 index 0000000000..8235d2946d Binary files /dev/null and b/assets/images/fide-fed/POR.png differ diff --git a/assets/images/fide-fed/PUR.png b/assets/images/fide-fed/PUR.png new file mode 100644 index 0000000000..c8c1091452 Binary files /dev/null and b/assets/images/fide-fed/PUR.png differ diff --git a/assets/images/fide-fed/QAT.png b/assets/images/fide-fed/QAT.png new file mode 100644 index 0000000000..dd7e17b824 Binary files /dev/null and b/assets/images/fide-fed/QAT.png differ diff --git a/assets/images/fide-fed/ROU.png b/assets/images/fide-fed/ROU.png new file mode 100644 index 0000000000..2c3c838a4b Binary files /dev/null and b/assets/images/fide-fed/ROU.png differ diff --git a/assets/images/fide-fed/RSA.png b/assets/images/fide-fed/RSA.png new file mode 100644 index 0000000000..953fb87de2 Binary files /dev/null and b/assets/images/fide-fed/RSA.png differ diff --git a/assets/images/fide-fed/RUS.png b/assets/images/fide-fed/RUS.png new file mode 100644 index 0000000000..cc87102a65 Binary files /dev/null and b/assets/images/fide-fed/RUS.png differ diff --git a/assets/images/fide-fed/RWA.png b/assets/images/fide-fed/RWA.png new file mode 100644 index 0000000000..4c58e5a707 Binary files /dev/null and b/assets/images/fide-fed/RWA.png differ diff --git a/assets/images/fide-fed/SCO.png b/assets/images/fide-fed/SCO.png new file mode 100644 index 0000000000..5905f8dad6 Binary files /dev/null and b/assets/images/fide-fed/SCO.png differ diff --git a/assets/images/fide-fed/SEN.png b/assets/images/fide-fed/SEN.png new file mode 100644 index 0000000000..5e45af0a43 Binary files /dev/null and b/assets/images/fide-fed/SEN.png differ diff --git a/assets/images/fide-fed/SEY.png b/assets/images/fide-fed/SEY.png new file mode 100644 index 0000000000..17c2bd9796 Binary files /dev/null and b/assets/images/fide-fed/SEY.png differ diff --git a/assets/images/fide-fed/SGP.png b/assets/images/fide-fed/SGP.png new file mode 100644 index 0000000000..7d7c4f9ce8 Binary files /dev/null and b/assets/images/fide-fed/SGP.png differ diff --git a/assets/images/fide-fed/SKN.png b/assets/images/fide-fed/SKN.png new file mode 100644 index 0000000000..db7a3cd1e9 Binary files /dev/null and b/assets/images/fide-fed/SKN.png differ diff --git a/assets/images/fide-fed/SLE.png b/assets/images/fide-fed/SLE.png new file mode 100644 index 0000000000..7692087a3c Binary files /dev/null and b/assets/images/fide-fed/SLE.png differ diff --git a/assets/images/fide-fed/SLO.png b/assets/images/fide-fed/SLO.png new file mode 100644 index 0000000000..69fdb42037 Binary files /dev/null and b/assets/images/fide-fed/SLO.png differ diff --git a/assets/images/fide-fed/SMR.png b/assets/images/fide-fed/SMR.png new file mode 100644 index 0000000000..415e146b1c Binary files /dev/null and b/assets/images/fide-fed/SMR.png differ diff --git a/assets/images/fide-fed/SOL.png b/assets/images/fide-fed/SOL.png new file mode 100644 index 0000000000..37f4060108 Binary files /dev/null and b/assets/images/fide-fed/SOL.png differ diff --git a/assets/images/fide-fed/SOM.png b/assets/images/fide-fed/SOM.png new file mode 100644 index 0000000000..2b20f43788 Binary files /dev/null and b/assets/images/fide-fed/SOM.png differ diff --git a/assets/images/fide-fed/SRB.png b/assets/images/fide-fed/SRB.png new file mode 100644 index 0000000000..f1d2c04880 Binary files /dev/null and b/assets/images/fide-fed/SRB.png differ diff --git a/assets/images/fide-fed/SRI.png b/assets/images/fide-fed/SRI.png new file mode 100644 index 0000000000..f0c2f01b19 Binary files /dev/null and b/assets/images/fide-fed/SRI.png differ diff --git a/assets/images/fide-fed/SSD.png b/assets/images/fide-fed/SSD.png new file mode 100644 index 0000000000..eca75c1e95 Binary files /dev/null and b/assets/images/fide-fed/SSD.png differ diff --git a/assets/images/fide-fed/STP.png b/assets/images/fide-fed/STP.png new file mode 100644 index 0000000000..cb486a32cc Binary files /dev/null and b/assets/images/fide-fed/STP.png differ diff --git a/assets/images/fide-fed/SUD.png b/assets/images/fide-fed/SUD.png new file mode 100644 index 0000000000..e96ab263c8 Binary files /dev/null and b/assets/images/fide-fed/SUD.png differ diff --git a/assets/images/fide-fed/SUI.png b/assets/images/fide-fed/SUI.png new file mode 100644 index 0000000000..8b4265c95e Binary files /dev/null and b/assets/images/fide-fed/SUI.png differ diff --git a/assets/images/fide-fed/SUR.png b/assets/images/fide-fed/SUR.png new file mode 100644 index 0000000000..4efce2c1f7 Binary files /dev/null and b/assets/images/fide-fed/SUR.png differ diff --git a/assets/images/fide-fed/SVK.png b/assets/images/fide-fed/SVK.png new file mode 100644 index 0000000000..3d7b9d771f Binary files /dev/null and b/assets/images/fide-fed/SVK.png differ diff --git a/assets/images/fide-fed/SWE.png b/assets/images/fide-fed/SWE.png new file mode 100644 index 0000000000..e026d02144 Binary files /dev/null and b/assets/images/fide-fed/SWE.png differ diff --git a/assets/images/fide-fed/SWZ.png b/assets/images/fide-fed/SWZ.png new file mode 100644 index 0000000000..e904908f1b Binary files /dev/null and b/assets/images/fide-fed/SWZ.png differ diff --git a/assets/images/fide-fed/SYR.png b/assets/images/fide-fed/SYR.png new file mode 100644 index 0000000000..1a47478db8 Binary files /dev/null and b/assets/images/fide-fed/SYR.png differ diff --git a/assets/images/fide-fed/TAN.png b/assets/images/fide-fed/TAN.png new file mode 100644 index 0000000000..f499c27b3a Binary files /dev/null and b/assets/images/fide-fed/TAN.png differ diff --git a/assets/images/fide-fed/THA.png b/assets/images/fide-fed/THA.png new file mode 100644 index 0000000000..3a41ad8cc1 Binary files /dev/null and b/assets/images/fide-fed/THA.png differ diff --git a/assets/images/fide-fed/TJK.png b/assets/images/fide-fed/TJK.png new file mode 100644 index 0000000000..85e66b9f9b Binary files /dev/null and b/assets/images/fide-fed/TJK.png differ diff --git a/assets/images/fide-fed/TKM.png b/assets/images/fide-fed/TKM.png new file mode 100644 index 0000000000..d5fac31c09 Binary files /dev/null and b/assets/images/fide-fed/TKM.png differ diff --git a/assets/images/fide-fed/TLS.png b/assets/images/fide-fed/TLS.png new file mode 100644 index 0000000000..e8e5af7230 Binary files /dev/null and b/assets/images/fide-fed/TLS.png differ diff --git a/assets/images/fide-fed/TOG.png b/assets/images/fide-fed/TOG.png new file mode 100644 index 0000000000..ef9cec3776 Binary files /dev/null and b/assets/images/fide-fed/TOG.png differ diff --git a/assets/images/fide-fed/TPE.png b/assets/images/fide-fed/TPE.png new file mode 100644 index 0000000000..9817a50880 Binary files /dev/null and b/assets/images/fide-fed/TPE.png differ diff --git a/assets/images/fide-fed/TTO.png b/assets/images/fide-fed/TTO.png new file mode 100644 index 0000000000..238ec77a30 Binary files /dev/null and b/assets/images/fide-fed/TTO.png differ diff --git a/assets/images/fide-fed/TUN.png b/assets/images/fide-fed/TUN.png new file mode 100644 index 0000000000..20106f784b Binary files /dev/null and b/assets/images/fide-fed/TUN.png differ diff --git a/assets/images/fide-fed/TUR.png b/assets/images/fide-fed/TUR.png new file mode 100644 index 0000000000..3f2682ae15 Binary files /dev/null and b/assets/images/fide-fed/TUR.png differ diff --git a/assets/images/fide-fed/UAE.png b/assets/images/fide-fed/UAE.png new file mode 100644 index 0000000000..8d26494e69 Binary files /dev/null and b/assets/images/fide-fed/UAE.png differ diff --git a/assets/images/fide-fed/UGA.png b/assets/images/fide-fed/UGA.png new file mode 100644 index 0000000000..940f75a1fa Binary files /dev/null and b/assets/images/fide-fed/UGA.png differ diff --git a/assets/images/fide-fed/UKR.png b/assets/images/fide-fed/UKR.png new file mode 100644 index 0000000000..760aacd407 Binary files /dev/null and b/assets/images/fide-fed/UKR.png differ diff --git a/assets/images/fide-fed/URU.png b/assets/images/fide-fed/URU.png new file mode 100644 index 0000000000..afcc53bd98 Binary files /dev/null and b/assets/images/fide-fed/URU.png differ diff --git a/assets/images/fide-fed/USA.png b/assets/images/fide-fed/USA.png new file mode 100644 index 0000000000..46180fe902 Binary files /dev/null and b/assets/images/fide-fed/USA.png differ diff --git a/assets/images/fide-fed/UZB.png b/assets/images/fide-fed/UZB.png new file mode 100644 index 0000000000..3abeee4c08 Binary files /dev/null and b/assets/images/fide-fed/UZB.png differ diff --git a/assets/images/fide-fed/VAN.png b/assets/images/fide-fed/VAN.png new file mode 100644 index 0000000000..1f730b4b03 Binary files /dev/null and b/assets/images/fide-fed/VAN.png differ diff --git a/assets/images/fide-fed/VEN.png b/assets/images/fide-fed/VEN.png new file mode 100644 index 0000000000..4dfe9aa593 Binary files /dev/null and b/assets/images/fide-fed/VEN.png differ diff --git a/assets/images/fide-fed/VIE.png b/assets/images/fide-fed/VIE.png new file mode 100644 index 0000000000..35bc7967d6 Binary files /dev/null and b/assets/images/fide-fed/VIE.png differ diff --git a/assets/images/fide-fed/VIN.png b/assets/images/fide-fed/VIN.png new file mode 100644 index 0000000000..f445943b1a Binary files /dev/null and b/assets/images/fide-fed/VIN.png differ diff --git a/assets/images/fide-fed/W.png b/assets/images/fide-fed/W.png new file mode 100644 index 0000000000..cc87102a65 Binary files /dev/null and b/assets/images/fide-fed/W.png differ diff --git a/assets/images/fide-fed/WLS.png b/assets/images/fide-fed/WLS.png new file mode 100644 index 0000000000..01848f9c5b Binary files /dev/null and b/assets/images/fide-fed/WLS.png differ diff --git a/assets/images/fide-fed/YEM.png b/assets/images/fide-fed/YEM.png new file mode 100644 index 0000000000..a3584aaddb Binary files /dev/null and b/assets/images/fide-fed/YEM.png differ diff --git a/assets/images/fide-fed/ZAM.png b/assets/images/fide-fed/ZAM.png new file mode 100644 index 0000000000..98334b17b4 Binary files /dev/null and b/assets/images/fide-fed/ZAM.png differ diff --git a/assets/images/fide-fed/ZIM.png b/assets/images/fide-fed/ZIM.png new file mode 100644 index 0000000000..89d14f9d41 Binary files /dev/null and b/assets/images/fide-fed/ZIM.png differ diff --git a/build.yaml b/build.yaml index bf6e7dfc2f..5bed70aa37 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,19 @@ targets: $default: builders: + json_serializable: + generate_for: + - lib/src/model/**/*.dart freezed: + generate_for: + - lib/src/model/**/*.dart options: from_json: false to_json: false + riverpod_generator: + generate_for: + - lib/src/localizations.dart + - lib/src/model/**/*.dart + - lib/src/network/*.dart + - lib/src/db/*.dart + - lib/src/**/*_providers.dart diff --git a/docs/coding_style.md b/docs/coding_style.md index 87787a3817..24d0804fa3 100644 --- a/docs/coding_style.md +++ b/docs/coding_style.md @@ -44,6 +44,25 @@ return [ ]; ``` +## Platform-dependent code + +If one of your widget needs to display different things on iOS and Android, you can use the `PlatformWidget` helper: + +```dart +PlatformWidget( + androidBuilder: (context) => { + // return widget to display for Android + }, + iosBuilder: (context) => { + // return widget to display for iOS + }, +) +``` + +> [!TIP] +> The codebase already has some common platform-aware widgets that you can use, for example `PlatformScaffold`, +> `PlatformAppBar`, `PlatformListTile`, ... + ## Writing UI code Here is a list of practical tips when writing widgets. These are generally coding best practices in flutter, and serve to keep the project consistent. diff --git a/docs/internationalisation.md b/docs/internationalisation.md index 8e40517433..9475c0ba03 100644 --- a/docs/internationalisation.md +++ b/docs/internationalisation.md @@ -1,7 +1,7 @@ # Internationalisation We're using the standard way of internationalising our app, as desbribed in the -[official documentation)(https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#setting-up). +[official documentation](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#setting-up). What is specific to this project is the way we produce the template `lib/l10n/app_en.arb` file. diff --git a/docs/setting_dev_env.md b/docs/setting_dev_env.md index 6f64881e95..9c5f7a5694 100644 --- a/docs/setting_dev_env.md +++ b/docs/setting_dev_env.md @@ -4,13 +4,61 @@ The following instructions outline how to set up your development environment fo If you get stuck during the installation process the most suitable place to seek help is the `#lichess-dev-onboarding` channel on Discord (https://discord.gg/lichess). -This project uses Flutter. The best way to get started is to follow [the flutter install guide](https://docs.flutter.dev/get-started/install). -Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. Installing flutter manually is a known workaround. +## Installing Flutter -This project is meant to run on iOS and Android, so you need to follow the "Platform setup" section of that guide to install the iOS and/or Android platform. +This project uses Flutter. + +1. Follow [the flutter install guide](https://docs.flutter.dev/get-started/install). + This project is meant to run on iOS and Android, so you need to follow the "Platform setup" section of that guide to + install the iOS and/or Android platform. +> [!WARNING] +> Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. +> Installing flutter manually is a known workaround. +2. Switch to the beta channel by running `flutter channel beta` and `flutter upgrade` +> [!NOTE] +> We'll use Flutter's `beta` channel while the app itself is in Beta. +3. Ensure Flutter is correctly configured by running `flutter doctor` Note that this application is not meant to be run on web platform. +### Flutter Version Management + +If you want to use FVM to manage your Flutter versions effectively, please consult the [FVM (Flutter Version Management) guide](https://fvm.app/documentation/getting-started/installation) for comprehensive instructions on installing Flutter on your specific operating system. + +This project is currently using FVM 3.x. + +**Pro Tip:** Remember to prepend the 'flutter' prefix when using FVM commands, like this: `fvm flutter [command]`. + +## Lila Server + +By default, the app will target the [Lichess dev server](https://lichess.dev/), +so you can start developing without setting up a local server. + +During development, you may need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) +instance to work on this project. + +If you work with a local lila, you will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) (websocket server). + +### lila-docker + +The fastest and most straight-forward way to get started is using [lila-docker](https://github.com/lichess-org/lila-docker). + +### Local lila server (manual installation) + +Instructions to install both `lila` and `lila-ws` locally can be found in [the lila wiki](https://github.com/lichess-org/lila/wiki/Lichess-Development-Onboarding). + +**Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** + +To run the application with a local server, you can use the following command: + +```bash +flutter run --dart-define=LICHESS_HOST=localhost:9663 --dart-define=LICHESS_WS_HOST=localhost:9664 +``` + +> [!NOTE] +> The hosts above are the default ports for lila, if you have changed them, you +will need to adjust the command accordingly. + ## Setting up the emulators ### iOS @@ -21,7 +69,10 @@ simulator. ### Android -If you are working with a local `lila` server, you need to expose some ports from the emulator. You can do this by running the following command (once the emulator is running): +#### When using a manually installed lila server + +If you are working with a local `lila` server, you need to expose some ports from the emulator. You can do this by +running the following command (once the emulator is running): ```bash adb reverse tcp:9663 tcp:9663 @@ -32,8 +83,10 @@ This will allow the app to communicate with the local server. 9663 is for `http` and 9664 is for `websocket`. It assumes that the server is running on the default ports. -When using [lila-docker](https://github.com/lichess-org/lila-docker), first run `./lila-docker hostname` and select your computer's IP address. -Then, instead of the commands above, use this: +#### When using lila-docker + +When using [lila-docker](https://github.com/lichess-org/lila-docker), first run `./lila-docker hostname` and select your +computer's IP address. Then, instead of the commands above, use this: ```bash adb reverse tcp:9663 tcp:8080 @@ -46,13 +99,14 @@ If Chrome instacrashes, it is likely you need to disable vulkan in emulator sett If you cannot access internet you can try launching the emulator with a DNS address: -``` +```bash $ emulator -avd -dns-server 1.1.1.1 ``` -If you experience high lags or freezes, check the memory settings and be sure to enable hardware acceleration. Also disabling the snapshot storage may help: +If you experience high lags or freezes, check the memory settings and be sure to enable hardware acceleration (`-gpu host`) +Also disabling the snapshot storage may help: -``` +```bash $ emulator -avd -no-snapshot-load -no-snapstorage -no-snapshot -no-snapshot-save' ``` @@ -60,15 +114,17 @@ $ emulator -avd -no-snapshot-load -no-snapstorage -no-snapshot -no-sna This project uses code generation to generate data classes with [freezed](https://pub.dev/packages/freezed) among other things. -You need to run it before anything else otherwise the project won't compile: +We don't commit generated code to the repository, so you need to run it before anything else otherwise the project won't +compile: -``` +```bash +flutter pub get dart run build_runner build ``` While developing you can use the watch command: -``` +```bash dart run build_runner watch ``` @@ -82,7 +138,6 @@ flutter analyze --watch It will run analysis continuously, watching the filesystem for changes. It is important to always check for analysis errors. - ## Run Use the `flutter run` command to run on an emulator or device. If you need to change the lichess host you can do it like so: @@ -91,3 +146,12 @@ Use the `flutter run` command to run on an emulator or device. If you need to ch flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev ``` + +### Logging + +```sh +dart devtools +``` + +Then run the app with the `flutter run` command above, and a link to the logging page will be printed in the console. + diff --git a/fluttericon.json b/fluttericon.json index 28d7a4bff0..e5c483d5b4 100644 --- a/fluttericon.json +++ b/fluttericon.json @@ -537,6 +537,34 @@ "search": [ "book_lichess" ] + }, + { + "uid": "df30e1b667ba2db43d2ad1ca361c21fa", + "css": "study", + "code": 59425, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M892.6 705.1V437.5H500V44.9H232.4Q179.7 44.9 144.5 82 107.4 117.2 107.4 169.9V437.5H500V830.1H767.6Q820.3 830.1 855.5 793 892.6 757.8 892.6 705.1ZM927.7 169.9V705.1Q927.7 771.5 880.9 818.4T767.6 865.2H232.4Q166 865.2 119.1 818.4T72.3 705.1V169.9Q72.3 103.5 119.1 56.6T232.4 9.8H767.6Q834 9.8 880.9 56.6T927.7 169.9Z", + "width": 1020 + }, + "search": [ + "study" + ] + }, + { + "uid": "5666e3cf8e8a53bb514d650488dd9dce", + "css": "body-cut", + "code": 59426, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M445.3 236.3Q453.1 236.3 472.7 232.4 480.5 232.4 482.4 226.6 486.3 218.8 486.3 214.8L439.5-48.8Q435.5-66.4 419.9-62.5 357.4-52.7 322.3-1T298.8 111.3Q306.6 166 348.6 201.2T445.3 236.3ZM410.2-25.4L449.2 201.2 445.3 203.1Q402.3 203.1 371.1 175.8T332 105.5 346.7 25.4 410.2-25.4ZM710.9 232.4Q730.5 236.3 738.3 236.3 793 236.3 835 201.2T884.8 111.3Q896.5 50.8 861.3-1T763.7-62.5Q755.9-62.5 752-58.6 748-56.6 744.1-48.8L699.2 214.8Q695.3 232.4 710.9 232.4ZM773.4-25.4Q814.5-11.7 837.9 25.4T853.5 105.5Q843.8 148.4 810.5 175.8T734.4 201.2ZM966.8 705.1H783.2L794.9 640.6Q814.5 652.3 826.2 654.3 853.5 660.2 877 643.6T904.3 599.6L931.6 423.8Q941.4 378.9 912.1 337.9 884.8 298.8 837.9 289.1L707 265.6Q697.3 265.6 693.4 269.5 687.5 273.4 687.5 279.3L627 625Q623 642.6 638.7 642.6 656.3 646.5 652.3 662.1L646.5 705.1H535.2L531.3 662.1Q527.3 646.5 544.9 642.6 550.8 642.6 554.7 636.7 558.6 632.8 558.6 625L498 279.3Q494.1 261.7 478.5 265.6L345.7 289.1Q298.8 298.8 271.5 337.9T252 423.8L281.3 597.7Q287.1 625 308.6 640.6 332 658.2 359.4 652.3 373 650.4 388.7 638.7L400.4 705.1H316.4V687.5Q316.4 669.9 300.8 669.9 283.2 669.9 283.2 687.5V705.1H33.2Q3.9 705.1 3.9 771.5T33.2 837.9H283.2V886.7Q283.2 904.3 300.8 904.3 316.4 904.3 316.4 886.7V837.9H425.8L433.6 882.8Q437.5 906.3 457 921.9T500 937.5H511.7Q539.1 931.6 554.7 909.2T566.4 859.4L562.5 837.9H623L619.1 859.4Q613.3 884.8 628.9 910.2 644.5 931.6 671.9 937.5H683.6Q709 937.5 727.5 921.9T750 882.8L757.8 837.9H900.4Q933.6 837.9 959 796.9T984.4 720.7Q984.4 705.1 966.8 705.1ZM412.1 578.1V576.2L392.6 462.9Q388.7 445.3 373 449.2 367.2 449.2 362.3 455.1T359.4 466.8L378.9 580.1V582Q382.8 595.7 375 606.4T353.5 619.1 328.1 613.3Q318.4 607.4 314.5 591.8L285.2 418Q279.3 384.8 298.8 356.4T351.6 322.3L466.8 300.8 523.4 617.2Q492.2 632.8 498 668L502 705.1H433.6ZM183.6 738.3H216.8V804.7H183.6V738.3ZM150.4 804.7H117.2V738.3H150.4V804.7ZM39.1 738.3H84V804.7H39.1Q35.2 794.9 35.2 771.5T39.1 738.3ZM250 804.7V738.3H283.2V804.7H250ZM533.2 865.2Q537.1 896.5 505.9 904.3 492.2 906.3 481.4 898.4T466.8 877L460.9 837.9H527.3ZM316.4 804.7V738.3H640.6L628.9 804.7H316.4ZM716.8 877Q709 910.2 677.7 904.3 664.1 900.4 656.3 890.6 650.4 878.9 650.4 865.2L685.5 668Q691.4 632.8 662.1 617.2L716.8 300.8 832 322.3Q867.2 328.1 884.8 355.5 904.3 384.8 898.4 418L871.1 595.7Q863.3 627 832 621.1 820.3 621.1 810.5 607.4 804.7 599.6 804.7 584V582L826.2 466.8Q826.2 451.2 812.5 447.3 796.9 443.4 793 460.9L771.5 580.1V582ZM900.4 804.7H763.7L777.3 738.3H947.3Q943.4 761.7 928.7 783.2T900.4 804.7Z", + "width": 1023 + }, + "search": [ + "body-cut" + ] } ] -} \ No newline at end of file +} diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index e662975a89..4168b7b944 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,20 +10,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.959.0) - aws-sdk-core (3.201.3) + aws-partitions (1.1013.0) + aws-sdk-core (3.213.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.156.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.173.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -65,10 +65,10 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,21 +150,21 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.2) + json (2.8.2) + jwt (2.9.3) base64 mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) public_suffix (6.0.1) @@ -171,8 +174,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.3) - strscan + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,7 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -195,14 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 74671449b0..a98f1319e3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,121 +1,124 @@ PODS: - app_settings (5.1.1): - Flutter - - AppAuth (1.7.4): - - AppAuth/Core (= 1.7.4) - - AppAuth/ExternalUserAgent (= 1.7.4) - - AppAuth/Core (1.7.4) - - AppAuth/ExternalUserAgent (1.7.4): + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): - AppAuth/Core - connectivity_plus (0.0.1): - Flutter - FlutterMacOS - cupertino_http (0.0.1): - Flutter + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.29.0): - - FirebaseCore (= 10.29.0) - - Firebase/Crashlytics (10.29.0): + - Firebase/CoreOnly (11.4.0): + - FirebaseCore (= 11.4.0) + - Firebase/Crashlytics (11.4.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 10.29.0) - - Firebase/Messaging (10.29.0): + - FirebaseCrashlytics (~> 11.4.0) + - Firebase/Messaging (11.4.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.29.0) - - firebase_core (3.3.0): - - Firebase/CoreOnly (= 10.29.0) + - FirebaseMessaging (~> 11.4.0) + - firebase_core (3.8.1): + - Firebase/CoreOnly (= 11.4.0) - Flutter - - firebase_crashlytics (4.0.4): - - Firebase/Crashlytics (= 10.29.0) + - firebase_crashlytics (4.2.0): + - Firebase/Crashlytics (= 11.4.0) - firebase_core - Flutter - - firebase_messaging (15.0.4): - - Firebase/Messaging (= 10.29.0) + - firebase_messaging (15.1.6): + - Firebase/Messaging (= 11.4.0) - firebase_core - Flutter - - FirebaseCore (10.29.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreExtension (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.29.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.29.0): - - FirebaseCore (~> 10.5) - - FirebaseInstallations (~> 10.0) - - FirebaseRemoteConfigInterop (~> 10.23) - - FirebaseSessions (~> 10.5) - - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.29.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.3) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Reachability (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseRemoteConfigInterop (10.29.0) - - FirebaseSessions (10.29.0): - - FirebaseCore (~> 10.5) - - FirebaseCoreExtension (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.13) - - GoogleUtilities/UserDefaults (~> 7.13) - - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseCore (11.4.0): + - FirebaseCoreInternal (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.4.1): + - FirebaseCore (~> 11.0) + - FirebaseCoreInternal (11.6.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseCrashlytics (11.4.0): + - FirebaseCore (~> 11.4) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSessions (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - FirebaseInstallations (11.4.0): + - FirebaseCore (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.4.0): + - FirebaseCore (~> 11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - FirebaseRemoteConfigInterop (11.6.0) + - FirebaseSessions (11.4.0): + - FirebaseCore (~> 11.4) + - FirebaseCoreExtension (~> 11.4) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_appauth (0.0.1): - - AppAuth (= 1.7.4) + - AppAuth (= 1.7.6) + - Flutter + - flutter_local_notifications (0.0.1): - Flutter - - flutter_native_splash (0.0.1): + - flutter_native_splash (2.4.3): - Flutter - flutter_secure_storage (6.0.0): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.3)": + - "GoogleUtilities/NSData+zlib (8.0.2)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - objective_c (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -131,10 +134,10 @@ PODS: - FlutterMacOS - sound_effect (0.0.2): - Flutter - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - stockfish (1.6.1): + - stockfish (1.6.2): - Flutter - url_launcher_ios (0.0.1): - Flutter @@ -144,21 +147,23 @@ PODS: DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) + - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sound_effect (from `.symlinks/plugins/sound_effect/ios`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - stockfish (from `.symlinks/plugins/stockfish/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -187,7 +192,7 @@ EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/darwin" cupertino_http: - :path: ".symlinks/plugins/cupertino_http/ios" + :path: ".symlinks/plugins/cupertino_http/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_core: @@ -200,10 +205,14 @@ EXTERNAL SOURCES: :path: Flutter flutter_appauth: :path: ".symlinks/plugins/flutter_appauth/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -214,8 +223,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sound_effect: :path: ".symlinks/plugins/sound_effect/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" stockfish: :path: ".symlinks/plugins/stockfish/ios" url_launcher_ios: @@ -224,42 +233,44 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - AppAuth: 182c5b88630569df5acb672720534756c29b3358 - connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db - cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d - firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb - firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f - firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 - FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 - FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f - FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 - FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774 - FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd - FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 - FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d - FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc + app_settings: 3507c575c2b18a462c99948f61d5de21d4420999 + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 + firebase_core: 222ad1787f0b9c145a06af2fdaa265a366576c0d + firebase_crashlytics: 1c2e2091a0b06bf6e3d6535010469a98c44c4d67 + firebase_messaging: a538130cb2bca3ea0ff0892b8c948bd7d20ecaed + FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 + FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e + FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 + FirebaseCrashlytics: 41bbdd2b514a8523cede0c217aee6ef7ecf38401 + FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 + FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 + FirebaseRemoteConfigInterop: e75e348953352a000331eb77caf01e424248e176 + FirebaseSessions: 3f56f177d9e53a85021d16b31f9a111849d1dd8b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 - flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + flutter_appauth: 914057fda669db5073d3ca9d94ea932e7df3c964 + flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 + flutter_native_splash: 576fbd69b830a63594ae678396fa17e43abbc5f8 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - stockfish: 9e398e73bfb36580f16b79e8b9b45568b9e1dcd9 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sound_effect: 7d4273d90e6c3357ca7951ea227c993723c20485 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + stockfish: 3125c5e5fdd6789c398bb2d1a29a58ba3c9e5577 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03 PODFILE CHECKSUM: 76a583f8d75b3a8c6e4bdc97ae8783ef36cc7984 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d342..c53e2b314e 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index da4a3e1265..329a693791 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import flutter_local_notifications @main @objc class AppDelegate: FlutterAppDelegate { @@ -41,6 +42,16 @@ import Flutter result(self.getPhysicalMemory()) }) + // Cf: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications#notification-actions + // This is required to make any communication available in the action isolate. + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index db6cb9b752..fb3991392e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -133,5 +133,7 @@ UIViewControllerBasedStatusBarAppearance + FirebaseMessagingAutoInitEnabled + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 83000377fe..9f4613589e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,62 +1,61 @@ { - "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Puzzles", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Watch", - "mobileSettingsTab": "Settings", - "mobileMustBeLoggedIn": "You must be logged in to view this page.", - "mobileSystemColors": "System colors", - "mobileFeedbackButton": "Feedback", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Haptic feedback", - "mobileSettingsImmersiveMode": "Immersive mode", - "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", - "mobileNotFollowingAnyUser": "You are not following any user.", "mobileAllGames": "All games", - "mobileRecentSearches": "Recent searches", + "mobileAreYouSure": "Are you sure?", + "mobileCancelTakebackOffer": "Cancel takeback offer", "mobileClearButton": "Clear", - "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", - "@mobilePlayersMatchingSearchTerm": { + "mobileCorrespondenceClearSavedMove": "Clear saved move", + "mobileCustomGameJoinAGame": "Join a game", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hello, {param}", + "@mobileGreeting": { "placeholders": { "param": { "type": "String" } } }, - "mobileNoSearchResults": "No results", - "mobileAreYouSure": "Are you sure?", - "mobilePuzzleStreakAbortWarning": "You will lose your current streak and your score will be saved.", - "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", - "mobileSharePuzzle": "Share this puzzle", - "mobileShareGameURL": "Share game URL", - "mobileShareGamePGN": "Share PGN", - "mobileSharePositionAsFEN": "Share position as FEN", - "mobileShowVariations": "Show variations", + "mobileGreetingWithoutName": "Hello", "mobileHideVariation": "Hide variation", - "mobileShowComments": "Show comments", - "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", - "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", - "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileCancelDrawOffer": "Cancel draw offer", - "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", - "mobileBlindfoldMode": "Blindfold", + "mobileHomeTab": "Home", "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Join a game", - "mobileCorrespondenceClearSavedMove": "Clear saved move", - "mobileSomethingWentWrong": "Something went wrong.", - "mobileShowResult": "Show result", - "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", - "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", - "mobileGreeting": "Hello, {param}", - "@mobileGreeting": { + "mobileMustBeLoggedIn": "You must be logged in to view this page.", + "mobileNoSearchResults": "No results", + "mobileNotFollowingAnyUser": "You are not following any user.", + "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", + "@mobilePlayersMatchingSearchTerm": { "placeholders": { "param": { "type": "String" } } }, - "mobileGreetingWithoutName": "Hello", "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", + "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", + "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", + "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", + "mobilePuzzleStreakAbortWarning": "You will lose your current streak and your score will be saved.", + "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", + "mobilePuzzlesTab": "Puzzles", + "mobileRecentSearches": "Recent searches", + "mobileSettingsHapticFeedback": "Haptic feedback", + "mobileSettingsImmersiveMode": "Immersive mode", + "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", + "mobileSettingsTab": "Settings", + "mobileShareGamePGN": "Share PGN", + "mobileShareGameURL": "Share game URL", + "mobileSharePositionAsFEN": "Share position as FEN", + "mobileSharePuzzle": "Share this puzzle", + "mobileShowComments": "Show comments", + "mobileShowResult": "Show result", + "mobileShowVariations": "Show variations", + "mobileSomethingWentWrong": "Something went wrong.", + "mobileSystemColors": "System colors", + "mobileTheme": "Theme", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", + "mobileWatchTab": "Watch", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", @@ -147,6 +146,17 @@ } } }, + "activityCompletedNbVariantGames": "{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}", + "@activityCompletedNbVariantGames": { + "placeholders": { + "count": { + "type": "int" + }, + "param2": { + "type": "String" + } + } + }, "activityFollowedNbPlayers": "{count, plural, =1{Started following {count} player} other{Started following {count} players}}", "@activityFollowedNbPlayers": { "placeholders": { @@ -229,7 +239,141 @@ } }, "broadcastBroadcasts": "Broadcasts", + "broadcastMyBroadcasts": "My broadcasts", "broadcastLiveBroadcasts": "Live tournament broadcasts", + "broadcastBroadcastCalendar": "Broadcast calendar", + "broadcastNewBroadcast": "New live broadcast", + "broadcastSubscribedBroadcasts": "Subscribed broadcasts", + "broadcastAboutBroadcasts": "About broadcasts", + "broadcastHowToUseLichessBroadcasts": "How to use Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "The new round will have the same members and contributors as the previous one.", + "broadcastAddRound": "Add a round", + "broadcastOngoing": "Ongoing", + "broadcastUpcoming": "Upcoming", + "broadcastCompleted": "Completed", + "broadcastCompletedHelp": "Lichess detects round completion, but can get it wrong. Use this to set it manually.", + "broadcastRoundName": "Round name", + "broadcastRoundNumber": "Round number", + "broadcastTournamentName": "Tournament name", + "broadcastTournamentDescription": "Short tournament description", + "broadcastFullDescription": "Full tournament description", + "broadcastFullDescriptionHelp": "Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.", + "@broadcastFullDescriptionHelp": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.", + "broadcastSourceGameIds": "Up to 64 Lichess game IDs, separated by spaces.", + "broadcastStartDateTimeZone": "Start date in the tournament local timezone: {param}", + "@broadcastStartDateTimeZone": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartDateHelp": "Optional, if you know when the event starts", + "broadcastCurrentGameUrl": "Current game URL", + "broadcastDownloadAllRounds": "Download all rounds", + "broadcastResetRound": "Reset this round", + "broadcastDeleteRound": "Delete this round", + "broadcastDefinitivelyDeleteRound": "Definitively delete the round and all its games.", + "broadcastDeleteAllGamesOfThisRound": "Delete all games of this round. The source will need to be active in order to re-create them.", + "broadcastEditRoundStudy": "Edit round study", + "broadcastDeleteTournament": "Delete this tournament", + "broadcastDefinitivelyDeleteTournament": "Definitively delete the entire tournament, all its rounds and all its games.", + "broadcastShowScores": "Show players scores based on game results", + "broadcastReplacePlayerTags": "Optional: replace player names, ratings and titles", + "broadcastFideFederations": "FIDE federations", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE players", + "broadcastFidePlayerNotFound": "FIDE player not found", + "broadcastFideProfile": "FIDE profile", + "broadcastFederation": "Federation", + "broadcastAgeThisYear": "Age this year", + "broadcastUnrated": "Unrated", + "broadcastRecentTournaments": "Recent tournaments", + "broadcastOpenLichess": "Open in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Boards", + "broadcastOverview": "Overview", + "broadcastSubscribeTitle": "Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.", + "broadcastUploadImage": "Upload tournament image", + "broadcastNoBoardsYet": "No boards yet. These will appear once games are uploaded.", + "broadcastBoardsCanBeLoaded": "Boards can be loaded with a source or via the {param}", + "@broadcastBoardsCanBeLoaded": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartsAfter": "Starts after {param}", + "@broadcastStartsAfter": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartVerySoon": "The broadcast will start very soon.", + "broadcastNotYetStarted": "The broadcast has not yet started.", + "broadcastOfficialWebsite": "Official website", + "broadcastStandings": "Standings", + "broadcastOfficialStandings": "Official Standings", + "broadcastIframeHelp": "More options on the {param}", + "@broadcastIframeHelp": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastWebmastersPage": "webmasters page", + "broadcastPgnSourceHelp": "A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronisation.", + "@broadcastPgnSourceHelp": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastEmbedThisBroadcast": "Embed this broadcast in your website", + "broadcastEmbedThisRound": "Embed {param} in your website", + "@broadcastEmbedThisRound": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastRatingDiff": "Rating diff", + "broadcastGamesThisTournament": "Games in this tournament", + "broadcastScore": "Score", + "broadcastAllTeams": "All teams", + "broadcastTournamentFormat": "Tournament format", + "broadcastTournamentLocation": "Tournament Location", + "broadcastTopPlayers": "Top players", + "broadcastTimezone": "Time zone", + "broadcastFideRatingCategory": "FIDE rating category", + "broadcastOptionalDetails": "Optional details", + "broadcastPastBroadcasts": "Past broadcasts", + "broadcastAllBroadcastsByMonth": "View all broadcasts by month", + "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", + "@broadcastNbBroadcasts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "challengeChallengesX": "Challenges: {param1}", "@challengeChallengesX": { "placeholders": { @@ -423,6 +567,7 @@ "preferencesDisplayBoardResizeHandle": "Show board resize handle", "preferencesOnlyOnInitialPosition": "Only on initial position", "preferencesInGameOnly": "In-game only", + "preferencesExceptInGame": "Except in-game", "preferencesChessClock": "Chess clock", "preferencesTenthsOfSeconds": "Tenths of seconds", "preferencesWhenTimeRemainingLessThanTenSeconds": "When time remaining < 10 seconds", @@ -470,6 +615,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Device", "preferencesBellNotificationSound": "Bell notification sound", + "preferencesBlindfold": "Blindfold", "puzzlePuzzles": "Puzzles", "puzzlePuzzleThemes": "Puzzle Themes", "puzzleRecommended": "Recommended", @@ -752,8 +898,8 @@ "puzzleThemeXRayAttackDescription": "A piece attacks or defends a square, through an enemy piece.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "The opponent is limited in the moves they can make, and all moves worsen their position.", - "puzzleThemeHealthyMix": "Healthy mix", - "puzzleThemeHealthyMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", + "puzzleThemeMix": "Healthy mix", + "puzzleThemeMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", "puzzleThemePlayerGames": "Player games", "puzzleThemePlayerGamesDescription": "Lookup puzzles generated from your games, or from another player's games.", "puzzleThemePuzzleDownloadInformation": "These puzzles are in the public domain, and can be downloaded from {param}.", @@ -925,7 +1071,6 @@ "replayMode": "Replay mode", "realtimeReplay": "Realtime", "byCPL": "By CPL", - "openStudy": "Open study", "enable": "Enable", "bestMoveArrow": "Best move arrow", "showVariationArrows": "Show variation arrows", @@ -935,7 +1080,6 @@ "memory": "Memory", "infiniteAnalysis": "Infinite analysis", "removesTheDepthLimit": "Removes the depth limit, and keeps your computer warm", - "engineManager": "Engine manager", "blunder": "Blunder", "mistake": "Mistake", "inaccuracy": "Inaccuracy", @@ -1090,6 +1234,7 @@ } }, "gamesPlayed": "Games played", + "ok": "OK", "cancel": "Cancel", "whiteTimeOut": "White time out", "blackTimeOut": "Black time out", @@ -1318,7 +1463,6 @@ "block": "Block", "blocked": "Blocked", "unblock": "Unblock", - "followsYou": "Follows you", "xStartedFollowingY": "{param1} started following {param2}", "@xStartedFollowingY": { "placeholders": { @@ -1483,7 +1627,9 @@ "cheat": "Cheat", "troll": "Troll", "other": "Other", - "reportDescriptionHelp": "Paste the link to the game(s) and explain what is wrong about this user's behaviour. Don't just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.", + "reportCheatBoostHelp": "Paste the link to the game(s) and explain what is wrong about this user's behaviour. Don't just say \"they cheat\", but tell us how you came to this conclusion.", + "reportUsernameHelp": "Explain what about this username is offensive. Don't just say \"it's offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.", + "reportProcessedFasterInEnglish": "Your report will be processed faster if written in English.", "error_provideOneCheatedGameLink": "Please provide at least one link to a cheated game.", "by": "by {param}", "@by": { @@ -2333,6 +2479,7 @@ "showMeEverything": "Show me everything", "lichessPatronInfo": "Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.", "nothingToSeeHere": "Nothing to see here at the moment.", + "stats": "Stats", "opponentLeftCounter": "{count, plural, =1{Your opponent left the game. You can claim victory in {count} second.} other{Your opponent left the game. You can claim victory in {count} seconds.}}", "@opponentLeftCounter": { "placeholders": { @@ -2773,6 +2920,373 @@ } }, "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Private", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies I contribute to", + "studyMyPublicStudies": "My public studies", + "studyMyPrivateStudies": "My private studies", + "studyMyFavoriteStudies": "My favourite studies", + "studyWhatAreStudies": "What are studies?", + "studyAllStudies": "All studies", + "studyStudiesCreatedByX": "Studies created by {param}", + "@studyStudiesCreatedByX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyNoneYet": "None yet.", + "studyHot": "Hot", + "studyDateAddedNewest": "Date added (newest)", + "studyDateAddedOldest": "Date added (oldest)", + "studyRecentlyUpdated": "Recently updated", + "studyMostPopular": "Most popular", + "studyAlphabetical": "Alphabetical", + "studyAddNewChapter": "Add a new chapter", + "studyAddMembers": "Add members", + "studyInviteToTheStudy": "Invite to the study", + "studyPleaseOnlyInvitePeopleYouKnow": "Please only invite people who know you, and who actively want to join this study.", + "studySearchByUsername": "Search by username", + "studySpectator": "Spectator", + "studyContributor": "Contributor", + "studyKick": "Kick", + "studyLeaveTheStudy": "Leave the study", + "studyYouAreNowAContributor": "You are now a contributor", + "studyYouAreNowASpectator": "You are now a spectator", + "studyPgnTags": "PGN tags", + "studyLike": "Like", + "studyUnlike": "Unlike", + "studyNewTag": "New tag", + "studyCommentThisPosition": "Comment on this position", + "studyCommentThisMove": "Comment on this move", + "studyAnnotateWithGlyphs": "Annotate with glyphs", + "studyTheChapterIsTooShortToBeAnalysed": "The chapter is too short to be analysed.", + "studyOnlyContributorsCanRequestAnalysis": "Only the study contributors can request a computer analysis.", + "studyGetAFullComputerAnalysis": "Get a full server-side computer analysis of the mainline.", + "studyMakeSureTheChapterIsComplete": "Make sure the chapter is complete. You can only request analysis once.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC members remain on the same position", + "studyShareChanges": "Share changes with spectators and save them on the server", + "studyPlaying": "Playing", + "studyShowEvalBar": "Evaluation bars", + "studyFirst": "First", + "studyPrevious": "Previous", + "studyNext": "Next", + "studyLast": "Last", "studyShareAndExport": "Share & export", - "studyStart": "Start" + "studyCloneStudy": "Clone", + "studyStudyPgn": "Study PGN", + "studyDownloadAllGames": "Download all games", + "studyChapterPgn": "Chapter PGN", + "studyCopyChapterPgn": "Copy PGN", + "studyDownloadGame": "Download game", + "studyStudyUrl": "Study URL", + "studyCurrentChapterUrl": "Current chapter URL", + "studyYouCanPasteThisInTheForumToEmbed": "You can paste this in the forum or your Lichess blog to embed", + "studyStartAtInitialPosition": "Start at initial position", + "studyStartAtX": "Start at {param}", + "@studyStartAtX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyEmbedInYourWebsite": "Embed in your website", + "studyReadMoreAboutEmbedding": "Read more about embedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Only public studies can be embedded!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1}, brought to you by {param2}", + "@studyXBroughtToYouByY": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "studyStudyNotFound": "Study not found", + "studyEditChapter": "Edit chapter", + "studyNewChapter": "New chapter", + "studyImportFromChapterX": "Import from {param}", + "@studyImportFromChapterX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyOrientation": "Orientation", + "studyAnalysisMode": "Analysis mode", + "studyPinnedChapterComment": "Pinned chapter comment", + "studySaveChapter": "Save chapter", + "studyClearAnnotations": "Clear annotations", + "studyClearVariations": "Clear variations", + "studyDeleteChapter": "Delete chapter", + "studyDeleteThisChapter": "Delete this chapter. There is no going back!", + "studyClearAllCommentsInThisChapter": "Clear all comments, glyphs and drawn shapes in this chapter", + "studyRightUnderTheBoard": "Right under the board", + "studyNoPinnedComment": "None", + "studyNormalAnalysis": "Normal analysis", + "studyHideNextMoves": "Hide next moves", + "studyInteractiveLesson": "Interactive lesson", + "studyChapterX": "Chapter {param}", + "@studyChapterX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyEmpty": "Empty", + "studyStartFromInitialPosition": "Start from initial position", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start from custom position", + "studyLoadAGameByUrl": "Load games by URLs", + "studyLoadAPositionFromFen": "Load a position from FEN", + "studyLoadAGameFromPgn": "Load games from PGN", + "studyAutomatic": "Automatic", + "studyUrlOfTheGame": "URL of the games, one per line", + "studyLoadAGameFromXOrY": "Load games from {param1} or {param2}", + "@studyLoadAGameFromXOrY": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "studyCreateChapter": "Create chapter", + "studyCreateStudy": "Create study", + "studyEditStudy": "Edit study", + "studyVisibility": "Visibility", + "studyPublic": "Public", + "studyUnlisted": "Unlisted", + "studyInviteOnly": "Invite only", + "studyAllowCloning": "Allow cloning", + "studyNobody": "Nobody", + "studyOnlyMe": "Only me", + "studyContributors": "Contributors", + "studyMembers": "Members", + "studyEveryone": "Everyone", + "studyEnableSync": "Enable sync", + "studyYesKeepEveryoneOnTheSamePosition": "Yes: keep everyone on the same position", + "studyNoLetPeopleBrowseFreely": "No: let people browse freely", + "studyPinnedStudyComment": "Pinned study comment", + "studyStart": "Start", + "studySave": "Save", + "studyClearChat": "Clear chat", + "studyDeleteTheStudyChatHistory": "Delete the study chat history? There is no going back!", + "studyDeleteStudy": "Delete study", + "studyConfirmDeleteStudy": "Delete the entire study? There is no going back! Type the name of the study to confirm: {param}", + "@studyConfirmDeleteStudy": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyWhereDoYouWantToStudyThat": "Where do you want to study that?", + "studyGoodMove": "Good move", + "studyMistake": "Mistake", + "studyBrilliantMove": "Brilliant move", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interesting move", + "studyDubiousMove": "Dubious move", + "studyOnlyMove": "Only move", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Equal position", + "studyUnclearPosition": "Unclear position", + "studyWhiteIsSlightlyBetter": "White is slightly better", + "studyBlackIsSlightlyBetter": "Black is slightly better", + "studyWhiteIsBetter": "White is better", + "studyBlackIsBetter": "Black is better", + "studyWhiteIsWinning": "White is winning", + "studyBlackIsWinning": "Black is winning", + "studyNovelty": "Novelty", + "studyDevelopment": "Development", + "studyInitiative": "Initiative", + "studyAttack": "Attack", + "studyCounterplay": "Counterplay", + "studyTimeTrouble": "Time trouble", + "studyWithCompensation": "With compensation", + "studyWithTheIdea": "With the idea", + "studyNextChapter": "Next chapter", + "studyPrevChapter": "Previous chapter", + "studyStudyActions": "Study actions", + "studyTopics": "Topics", + "studyMyTopics": "My topics", + "studyPopularTopics": "Popular topics", + "studyManageTopics": "Manage topics", + "studyBack": "Back", + "studyPlayAgain": "Play again", + "studyWhatWouldYouPlay": "What would you play in this position?", + "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyPerPage": "{param} per page", + "@studyPerPage": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", + "@studyNbChapters": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", + "@studyNbGames": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", + "@studyNbMembers": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}", + "@studyPasteYourPgnTextHereUpToNbGames": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoJustNow": "just now", + "timeagoRightNow": "right now", + "timeagoCompleted": "completed", + "timeagoInNbSeconds": "{count, plural, =1{in {count} second} other{in {count} seconds}}", + "@timeagoInNbSeconds": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbMinutes": "{count, plural, =1{in {count} minute} other{in {count} minutes}}", + "@timeagoInNbMinutes": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbHours": "{count, plural, =1{in {count} hour} other{in {count} hours}}", + "@timeagoInNbHours": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbDays": "{count, plural, =1{in {count} day} other{in {count} days}}", + "@timeagoInNbDays": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weeks}}", + "@timeagoInNbWeeks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbMonths": "{count, plural, =1{in {count} month} other{in {count} months}}", + "@timeagoInNbMonths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbYears": "{count, plural, =1{in {count} year} other{in {count} years}}", + "@timeagoInNbYears": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minute ago} other{{count} minutes ago}}", + "@timeagoNbMinutesAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbHoursAgo": "{count, plural, =1{{count} hour ago} other{{count} hours ago}}", + "@timeagoNbHoursAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbDaysAgo": "{count, plural, =1{{count} day ago} other{{count} days ago}}", + "@timeagoNbDaysAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week ago} other{{count} weeks ago}}", + "@timeagoNbWeeksAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMonthsAgo": "{count, plural, =1{{count} month ago} other{{count} months ago}}", + "@timeagoNbMonthsAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbYearsAgo": "{count, plural, =1{{count} year ago} other{{count} years ago}}", + "@timeagoNbYearsAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}", + "@timeagoNbMinutesRemaining": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}", + "@timeagoNbHoursRemaining": { + "placeholders": { + "count": { + "type": "int" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 341eb51750..0873ea0921 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -204,47 +204,41 @@ abstract class AppLocalizations { Locale('zh', 'TW') ]; - /// No description provided for @mobileHomeTab. - /// - /// In en, this message translates to: - /// **'Home'** - String get mobileHomeTab; - - /// No description provided for @mobilePuzzlesTab. + /// No description provided for @mobileAllGames. /// /// In en, this message translates to: - /// **'Puzzles'** - String get mobilePuzzlesTab; + /// **'All games'** + String get mobileAllGames; - /// No description provided for @mobileToolsTab. + /// No description provided for @mobileAreYouSure. /// /// In en, this message translates to: - /// **'Tools'** - String get mobileToolsTab; + /// **'Are you sure?'** + String get mobileAreYouSure; - /// No description provided for @mobileWatchTab. + /// No description provided for @mobileCancelTakebackOffer. /// /// In en, this message translates to: - /// **'Watch'** - String get mobileWatchTab; + /// **'Cancel takeback offer'** + String get mobileCancelTakebackOffer; - /// No description provided for @mobileSettingsTab. + /// No description provided for @mobileClearButton. /// /// In en, this message translates to: - /// **'Settings'** - String get mobileSettingsTab; + /// **'Clear'** + String get mobileClearButton; - /// No description provided for @mobileMustBeLoggedIn. + /// No description provided for @mobileCorrespondenceClearSavedMove. /// /// In en, this message translates to: - /// **'You must be logged in to view this page.'** - String get mobileMustBeLoggedIn; + /// **'Clear saved move'** + String get mobileCorrespondenceClearSavedMove; - /// No description provided for @mobileSystemColors. + /// No description provided for @mobileCustomGameJoinAGame. /// /// In en, this message translates to: - /// **'System colors'** - String get mobileSystemColors; + /// **'Join a game'** + String get mobileCustomGameJoinAGame; /// No description provided for @mobileFeedbackButton. /// @@ -252,53 +246,59 @@ abstract class AppLocalizations { /// **'Feedback'** String get mobileFeedbackButton; - /// No description provided for @mobileOkButton. + /// No description provided for @mobileGreeting. /// /// In en, this message translates to: - /// **'OK'** - String get mobileOkButton; + /// **'Hello, {param}'** + String mobileGreeting(String param); - /// No description provided for @mobileSettingsHapticFeedback. + /// No description provided for @mobileGreetingWithoutName. /// /// In en, this message translates to: - /// **'Haptic feedback'** - String get mobileSettingsHapticFeedback; + /// **'Hello'** + String get mobileGreetingWithoutName; - /// No description provided for @mobileSettingsImmersiveMode. + /// No description provided for @mobileHideVariation. /// /// In en, this message translates to: - /// **'Immersive mode'** - String get mobileSettingsImmersiveMode; + /// **'Hide variation'** + String get mobileHideVariation; - /// No description provided for @mobileSettingsImmersiveModeSubtitle. + /// No description provided for @mobileHomeTab. /// /// In en, this message translates to: - /// **'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'** - String get mobileSettingsImmersiveModeSubtitle; + /// **'Home'** + String get mobileHomeTab; - /// No description provided for @mobileNotFollowingAnyUser. + /// No description provided for @mobileLiveStreamers. /// /// In en, this message translates to: - /// **'You are not following any user.'** - String get mobileNotFollowingAnyUser; + /// **'Live streamers'** + String get mobileLiveStreamers; - /// No description provided for @mobileAllGames. + /// No description provided for @mobileMustBeLoggedIn. /// /// In en, this message translates to: - /// **'All games'** - String get mobileAllGames; + /// **'You must be logged in to view this page.'** + String get mobileMustBeLoggedIn; - /// No description provided for @mobileRecentSearches. + /// No description provided for @mobileNoSearchResults. /// /// In en, this message translates to: - /// **'Recent searches'** - String get mobileRecentSearches; + /// **'No results'** + String get mobileNoSearchResults; - /// No description provided for @mobileClearButton. + /// No description provided for @mobileNotFollowingAnyUser. /// /// In en, this message translates to: - /// **'Clear'** - String get mobileClearButton; + /// **'You are not following any user.'** + String get mobileNotFollowingAnyUser; + + /// No description provided for @mobileOkButton. + /// + /// In en, this message translates to: + /// **'OK'** + String get mobileOkButton; /// No description provided for @mobilePlayersMatchingSearchTerm. /// @@ -306,23 +306,23 @@ abstract class AppLocalizations { /// **'Players with \"{param}\"'** String mobilePlayersMatchingSearchTerm(String param); - /// No description provided for @mobileNoSearchResults. + /// No description provided for @mobilePrefMagnifyDraggedPiece. /// /// In en, this message translates to: - /// **'No results'** - String get mobileNoSearchResults; + /// **'Magnify dragged piece'** + String get mobilePrefMagnifyDraggedPiece; - /// No description provided for @mobileAreYouSure. + /// No description provided for @mobilePuzzleStormConfirmEndRun. /// /// In en, this message translates to: - /// **'Are you sure?'** - String get mobileAreYouSure; + /// **'Do you want to end this run?'** + String get mobilePuzzleStormConfirmEndRun; - /// No description provided for @mobilePuzzleStreakAbortWarning. + /// No description provided for @mobilePuzzleStormFilterNothingToShow. /// /// In en, this message translates to: - /// **'You will lose your current streak and your score will be saved.'** - String get mobilePuzzleStreakAbortWarning; + /// **'Nothing to show, please change the filters'** + String get mobilePuzzleStormFilterNothingToShow; /// No description provided for @mobilePuzzleStormNothingToShow. /// @@ -330,101 +330,101 @@ abstract class AppLocalizations { /// **'Nothing to show. Play some runs of Puzzle Storm.'** String get mobilePuzzleStormNothingToShow; - /// No description provided for @mobileSharePuzzle. + /// No description provided for @mobilePuzzleStormSubtitle. /// /// In en, this message translates to: - /// **'Share this puzzle'** - String get mobileSharePuzzle; + /// **'Solve as many puzzles as possible in 3 minutes.'** + String get mobilePuzzleStormSubtitle; - /// No description provided for @mobileShareGameURL. + /// No description provided for @mobilePuzzleStreakAbortWarning. /// /// In en, this message translates to: - /// **'Share game URL'** - String get mobileShareGameURL; + /// **'You will lose your current streak and your score will be saved.'** + String get mobilePuzzleStreakAbortWarning; - /// No description provided for @mobileShareGamePGN. + /// No description provided for @mobilePuzzleThemesSubtitle. /// /// In en, this message translates to: - /// **'Share PGN'** - String get mobileShareGamePGN; + /// **'Play puzzles from your favorite openings, or choose a theme.'** + String get mobilePuzzleThemesSubtitle; - /// No description provided for @mobileSharePositionAsFEN. + /// No description provided for @mobilePuzzlesTab. /// /// In en, this message translates to: - /// **'Share position as FEN'** - String get mobileSharePositionAsFEN; + /// **'Puzzles'** + String get mobilePuzzlesTab; - /// No description provided for @mobileShowVariations. + /// No description provided for @mobileRecentSearches. /// /// In en, this message translates to: - /// **'Show variations'** - String get mobileShowVariations; + /// **'Recent searches'** + String get mobileRecentSearches; - /// No description provided for @mobileHideVariation. + /// No description provided for @mobileSettingsHapticFeedback. /// /// In en, this message translates to: - /// **'Hide variation'** - String get mobileHideVariation; + /// **'Haptic feedback'** + String get mobileSettingsHapticFeedback; - /// No description provided for @mobileShowComments. + /// No description provided for @mobileSettingsImmersiveMode. /// /// In en, this message translates to: - /// **'Show comments'** - String get mobileShowComments; + /// **'Immersive mode'** + String get mobileSettingsImmersiveMode; - /// No description provided for @mobilePuzzleStormConfirmEndRun. + /// No description provided for @mobileSettingsImmersiveModeSubtitle. /// /// In en, this message translates to: - /// **'Do you want to end this run?'** - String get mobilePuzzleStormConfirmEndRun; + /// **'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'** + String get mobileSettingsImmersiveModeSubtitle; - /// No description provided for @mobilePuzzleStormFilterNothingToShow. + /// No description provided for @mobileSettingsTab. /// /// In en, this message translates to: - /// **'Nothing to show, please change the filters'** - String get mobilePuzzleStormFilterNothingToShow; + /// **'Settings'** + String get mobileSettingsTab; - /// No description provided for @mobileCancelTakebackOffer. + /// No description provided for @mobileShareGamePGN. /// /// In en, this message translates to: - /// **'Cancel takeback offer'** - String get mobileCancelTakebackOffer; + /// **'Share PGN'** + String get mobileShareGamePGN; - /// No description provided for @mobileCancelDrawOffer. + /// No description provided for @mobileShareGameURL. /// /// In en, this message translates to: - /// **'Cancel draw offer'** - String get mobileCancelDrawOffer; + /// **'Share game URL'** + String get mobileShareGameURL; - /// No description provided for @mobileWaitingForOpponentToJoin. + /// No description provided for @mobileSharePositionAsFEN. /// /// In en, this message translates to: - /// **'Waiting for opponent to join...'** - String get mobileWaitingForOpponentToJoin; + /// **'Share position as FEN'** + String get mobileSharePositionAsFEN; - /// No description provided for @mobileBlindfoldMode. + /// No description provided for @mobileSharePuzzle. /// /// In en, this message translates to: - /// **'Blindfold'** - String get mobileBlindfoldMode; + /// **'Share this puzzle'** + String get mobileSharePuzzle; - /// No description provided for @mobileLiveStreamers. + /// No description provided for @mobileShowComments. /// /// In en, this message translates to: - /// **'Live streamers'** - String get mobileLiveStreamers; + /// **'Show comments'** + String get mobileShowComments; - /// No description provided for @mobileCustomGameJoinAGame. + /// No description provided for @mobileShowResult. /// /// In en, this message translates to: - /// **'Join a game'** - String get mobileCustomGameJoinAGame; + /// **'Show result'** + String get mobileShowResult; - /// No description provided for @mobileCorrespondenceClearSavedMove. + /// No description provided for @mobileShowVariations. /// /// In en, this message translates to: - /// **'Clear saved move'** - String get mobileCorrespondenceClearSavedMove; + /// **'Show variations'** + String get mobileShowVariations; /// No description provided for @mobileSomethingWentWrong. /// @@ -432,41 +432,35 @@ abstract class AppLocalizations { /// **'Something went wrong.'** String get mobileSomethingWentWrong; - /// No description provided for @mobileShowResult. - /// - /// In en, this message translates to: - /// **'Show result'** - String get mobileShowResult; - - /// No description provided for @mobilePuzzleThemesSubtitle. + /// No description provided for @mobileSystemColors. /// /// In en, this message translates to: - /// **'Play puzzles from your favorite openings, or choose a theme.'** - String get mobilePuzzleThemesSubtitle; + /// **'System colors'** + String get mobileSystemColors; - /// No description provided for @mobilePuzzleStormSubtitle. + /// No description provided for @mobileTheme. /// /// In en, this message translates to: - /// **'Solve as many puzzles as possible in 3 minutes.'** - String get mobilePuzzleStormSubtitle; + /// **'Theme'** + String get mobileTheme; - /// No description provided for @mobileGreeting. + /// No description provided for @mobileToolsTab. /// /// In en, this message translates to: - /// **'Hello, {param}'** - String mobileGreeting(String param); + /// **'Tools'** + String get mobileToolsTab; - /// No description provided for @mobileGreetingWithoutName. + /// No description provided for @mobileWaitingForOpponentToJoin. /// /// In en, this message translates to: - /// **'Hello'** - String get mobileGreetingWithoutName; + /// **'Waiting for opponent to join...'** + String get mobileWaitingForOpponentToJoin; - /// No description provided for @mobilePrefMagnifyDraggedPiece. + /// No description provided for @mobileWatchTab. /// /// In en, this message translates to: - /// **'Magnify dragged piece'** - String get mobilePrefMagnifyDraggedPiece; + /// **'Watch'** + String get mobileWatchTab; /// No description provided for @activityActivity. /// @@ -540,6 +534,12 @@ abstract class AppLocalizations { /// **'{count, plural, =1{Completed {count} correspondence game} other{Completed {count} correspondence games}}'** String activityCompletedNbGames(int count); + /// No description provided for @activityCompletedNbVariantGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}'** + String activityCompletedNbVariantGames(int count, String param2); + /// No description provided for @activityFollowedNbPlayers. /// /// In en, this message translates to: @@ -600,35 +600,485 @@ abstract class AppLocalizations { /// **'Broadcasts'** String get broadcastBroadcasts; + /// No description provided for @broadcastMyBroadcasts. + /// + /// In en, this message translates to: + /// **'My broadcasts'** + String get broadcastMyBroadcasts; + /// No description provided for @broadcastLiveBroadcasts. /// /// In en, this message translates to: /// **'Live tournament broadcasts'** String get broadcastLiveBroadcasts; - /// No description provided for @challengeChallengesX. + /// No description provided for @broadcastBroadcastCalendar. /// /// In en, this message translates to: - /// **'Challenges: {param1}'** - String challengeChallengesX(String param1); + /// **'Broadcast calendar'** + String get broadcastBroadcastCalendar; - /// No description provided for @challengeChallengeToPlay. + /// No description provided for @broadcastNewBroadcast. /// /// In en, this message translates to: - /// **'Challenge to a game'** - String get challengeChallengeToPlay; + /// **'New live broadcast'** + String get broadcastNewBroadcast; - /// No description provided for @challengeChallengeDeclined. + /// No description provided for @broadcastSubscribedBroadcasts. /// /// In en, this message translates to: - /// **'Challenge declined.'** - String get challengeChallengeDeclined; + /// **'Subscribed broadcasts'** + String get broadcastSubscribedBroadcasts; - /// No description provided for @challengeChallengeAccepted. + /// No description provided for @broadcastAboutBroadcasts. /// /// In en, this message translates to: - /// **'Challenge accepted!'** - String get challengeChallengeAccepted; + /// **'About broadcasts'** + String get broadcastAboutBroadcasts; + + /// No description provided for @broadcastHowToUseLichessBroadcasts. + /// + /// In en, this message translates to: + /// **'How to use Lichess Broadcasts.'** + String get broadcastHowToUseLichessBroadcasts; + + /// No description provided for @broadcastTheNewRoundHelp. + /// + /// In en, this message translates to: + /// **'The new round will have the same members and contributors as the previous one.'** + String get broadcastTheNewRoundHelp; + + /// No description provided for @broadcastAddRound. + /// + /// In en, this message translates to: + /// **'Add a round'** + String get broadcastAddRound; + + /// No description provided for @broadcastOngoing. + /// + /// In en, this message translates to: + /// **'Ongoing'** + String get broadcastOngoing; + + /// No description provided for @broadcastUpcoming. + /// + /// In en, this message translates to: + /// **'Upcoming'** + String get broadcastUpcoming; + + /// No description provided for @broadcastCompleted. + /// + /// In en, this message translates to: + /// **'Completed'** + String get broadcastCompleted; + + /// No description provided for @broadcastCompletedHelp. + /// + /// In en, this message translates to: + /// **'Lichess detects round completion, but can get it wrong. Use this to set it manually.'** + String get broadcastCompletedHelp; + + /// No description provided for @broadcastRoundName. + /// + /// In en, this message translates to: + /// **'Round name'** + String get broadcastRoundName; + + /// No description provided for @broadcastRoundNumber. + /// + /// In en, this message translates to: + /// **'Round number'** + String get broadcastRoundNumber; + + /// No description provided for @broadcastTournamentName. + /// + /// In en, this message translates to: + /// **'Tournament name'** + String get broadcastTournamentName; + + /// No description provided for @broadcastTournamentDescription. + /// + /// In en, this message translates to: + /// **'Short tournament description'** + String get broadcastTournamentDescription; + + /// No description provided for @broadcastFullDescription. + /// + /// In en, this message translates to: + /// **'Full tournament description'** + String get broadcastFullDescription; + + /// No description provided for @broadcastFullDescriptionHelp. + /// + /// In en, this message translates to: + /// **'Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.'** + String broadcastFullDescriptionHelp(String param1, String param2); + + /// No description provided for @broadcastSourceSingleUrl. + /// + /// In en, this message translates to: + /// **'PGN Source URL'** + String get broadcastSourceSingleUrl; + + /// No description provided for @broadcastSourceUrlHelp. + /// + /// In en, this message translates to: + /// **'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'** + String get broadcastSourceUrlHelp; + + /// No description provided for @broadcastSourceGameIds. + /// + /// In en, this message translates to: + /// **'Up to 64 Lichess game IDs, separated by spaces.'** + String get broadcastSourceGameIds; + + /// No description provided for @broadcastStartDateTimeZone. + /// + /// In en, this message translates to: + /// **'Start date in the tournament local timezone: {param}'** + String broadcastStartDateTimeZone(String param); + + /// No description provided for @broadcastStartDateHelp. + /// + /// In en, this message translates to: + /// **'Optional, if you know when the event starts'** + String get broadcastStartDateHelp; + + /// No description provided for @broadcastCurrentGameUrl. + /// + /// In en, this message translates to: + /// **'Current game URL'** + String get broadcastCurrentGameUrl; + + /// No description provided for @broadcastDownloadAllRounds. + /// + /// In en, this message translates to: + /// **'Download all rounds'** + String get broadcastDownloadAllRounds; + + /// No description provided for @broadcastResetRound. + /// + /// In en, this message translates to: + /// **'Reset this round'** + String get broadcastResetRound; + + /// No description provided for @broadcastDeleteRound. + /// + /// In en, this message translates to: + /// **'Delete this round'** + String get broadcastDeleteRound; + + /// No description provided for @broadcastDefinitivelyDeleteRound. + /// + /// In en, this message translates to: + /// **'Definitively delete the round and all its games.'** + String get broadcastDefinitivelyDeleteRound; + + /// No description provided for @broadcastDeleteAllGamesOfThisRound. + /// + /// In en, this message translates to: + /// **'Delete all games of this round. The source will need to be active in order to re-create them.'** + String get broadcastDeleteAllGamesOfThisRound; + + /// No description provided for @broadcastEditRoundStudy. + /// + /// In en, this message translates to: + /// **'Edit round study'** + String get broadcastEditRoundStudy; + + /// No description provided for @broadcastDeleteTournament. + /// + /// In en, this message translates to: + /// **'Delete this tournament'** + String get broadcastDeleteTournament; + + /// No description provided for @broadcastDefinitivelyDeleteTournament. + /// + /// In en, this message translates to: + /// **'Definitively delete the entire tournament, all its rounds and all its games.'** + String get broadcastDefinitivelyDeleteTournament; + + /// No description provided for @broadcastShowScores. + /// + /// In en, this message translates to: + /// **'Show players scores based on game results'** + String get broadcastShowScores; + + /// No description provided for @broadcastReplacePlayerTags. + /// + /// In en, this message translates to: + /// **'Optional: replace player names, ratings and titles'** + String get broadcastReplacePlayerTags; + + /// No description provided for @broadcastFideFederations. + /// + /// In en, this message translates to: + /// **'FIDE federations'** + String get broadcastFideFederations; + + /// No description provided for @broadcastTop10Rating. + /// + /// In en, this message translates to: + /// **'Top 10 rating'** + String get broadcastTop10Rating; + + /// No description provided for @broadcastFidePlayers. + /// + /// In en, this message translates to: + /// **'FIDE players'** + String get broadcastFidePlayers; + + /// No description provided for @broadcastFidePlayerNotFound. + /// + /// In en, this message translates to: + /// **'FIDE player not found'** + String get broadcastFidePlayerNotFound; + + /// No description provided for @broadcastFideProfile. + /// + /// In en, this message translates to: + /// **'FIDE profile'** + String get broadcastFideProfile; + + /// No description provided for @broadcastFederation. + /// + /// In en, this message translates to: + /// **'Federation'** + String get broadcastFederation; + + /// No description provided for @broadcastAgeThisYear. + /// + /// In en, this message translates to: + /// **'Age this year'** + String get broadcastAgeThisYear; + + /// No description provided for @broadcastUnrated. + /// + /// In en, this message translates to: + /// **'Unrated'** + String get broadcastUnrated; + + /// No description provided for @broadcastRecentTournaments. + /// + /// In en, this message translates to: + /// **'Recent tournaments'** + String get broadcastRecentTournaments; + + /// No description provided for @broadcastOpenLichess. + /// + /// In en, this message translates to: + /// **'Open in Lichess'** + String get broadcastOpenLichess; + + /// No description provided for @broadcastTeams. + /// + /// In en, this message translates to: + /// **'Teams'** + String get broadcastTeams; + + /// No description provided for @broadcastBoards. + /// + /// In en, this message translates to: + /// **'Boards'** + String get broadcastBoards; + + /// No description provided for @broadcastOverview. + /// + /// In en, this message translates to: + /// **'Overview'** + String get broadcastOverview; + + /// No description provided for @broadcastSubscribeTitle. + /// + /// In en, this message translates to: + /// **'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'** + String get broadcastSubscribeTitle; + + /// No description provided for @broadcastUploadImage. + /// + /// In en, this message translates to: + /// **'Upload tournament image'** + String get broadcastUploadImage; + + /// No description provided for @broadcastNoBoardsYet. + /// + /// In en, this message translates to: + /// **'No boards yet. These will appear once games are uploaded.'** + String get broadcastNoBoardsYet; + + /// No description provided for @broadcastBoardsCanBeLoaded. + /// + /// In en, this message translates to: + /// **'Boards can be loaded with a source or via the {param}'** + String broadcastBoardsCanBeLoaded(String param); + + /// No description provided for @broadcastStartsAfter. + /// + /// In en, this message translates to: + /// **'Starts after {param}'** + String broadcastStartsAfter(String param); + + /// No description provided for @broadcastStartVerySoon. + /// + /// In en, this message translates to: + /// **'The broadcast will start very soon.'** + String get broadcastStartVerySoon; + + /// No description provided for @broadcastNotYetStarted. + /// + /// In en, this message translates to: + /// **'The broadcast has not yet started.'** + String get broadcastNotYetStarted; + + /// No description provided for @broadcastOfficialWebsite. + /// + /// In en, this message translates to: + /// **'Official website'** + String get broadcastOfficialWebsite; + + /// No description provided for @broadcastStandings. + /// + /// In en, this message translates to: + /// **'Standings'** + String get broadcastStandings; + + /// No description provided for @broadcastOfficialStandings. + /// + /// In en, this message translates to: + /// **'Official Standings'** + String get broadcastOfficialStandings; + + /// No description provided for @broadcastIframeHelp. + /// + /// In en, this message translates to: + /// **'More options on the {param}'** + String broadcastIframeHelp(String param); + + /// No description provided for @broadcastWebmastersPage. + /// + /// In en, this message translates to: + /// **'webmasters page'** + String get broadcastWebmastersPage; + + /// No description provided for @broadcastPgnSourceHelp. + /// + /// In en, this message translates to: + /// **'A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronisation.'** + String broadcastPgnSourceHelp(String param); + + /// No description provided for @broadcastEmbedThisBroadcast. + /// + /// In en, this message translates to: + /// **'Embed this broadcast in your website'** + String get broadcastEmbedThisBroadcast; + + /// No description provided for @broadcastEmbedThisRound. + /// + /// In en, this message translates to: + /// **'Embed {param} in your website'** + String broadcastEmbedThisRound(String param); + + /// No description provided for @broadcastRatingDiff. + /// + /// In en, this message translates to: + /// **'Rating diff'** + String get broadcastRatingDiff; + + /// No description provided for @broadcastGamesThisTournament. + /// + /// In en, this message translates to: + /// **'Games in this tournament'** + String get broadcastGamesThisTournament; + + /// No description provided for @broadcastScore. + /// + /// In en, this message translates to: + /// **'Score'** + String get broadcastScore; + + /// No description provided for @broadcastAllTeams. + /// + /// In en, this message translates to: + /// **'All teams'** + String get broadcastAllTeams; + + /// No description provided for @broadcastTournamentFormat. + /// + /// In en, this message translates to: + /// **'Tournament format'** + String get broadcastTournamentFormat; + + /// No description provided for @broadcastTournamentLocation. + /// + /// In en, this message translates to: + /// **'Tournament Location'** + String get broadcastTournamentLocation; + + /// No description provided for @broadcastTopPlayers. + /// + /// In en, this message translates to: + /// **'Top players'** + String get broadcastTopPlayers; + + /// No description provided for @broadcastTimezone. + /// + /// In en, this message translates to: + /// **'Time zone'** + String get broadcastTimezone; + + /// No description provided for @broadcastFideRatingCategory. + /// + /// In en, this message translates to: + /// **'FIDE rating category'** + String get broadcastFideRatingCategory; + + /// No description provided for @broadcastOptionalDetails. + /// + /// In en, this message translates to: + /// **'Optional details'** + String get broadcastOptionalDetails; + + /// No description provided for @broadcastPastBroadcasts. + /// + /// In en, this message translates to: + /// **'Past broadcasts'** + String get broadcastPastBroadcasts; + + /// No description provided for @broadcastAllBroadcastsByMonth. + /// + /// In en, this message translates to: + /// **'View all broadcasts by month'** + String get broadcastAllBroadcastsByMonth; + + /// No description provided for @broadcastNbBroadcasts. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} broadcast} other{{count} broadcasts}}'** + String broadcastNbBroadcasts(int count); + + /// No description provided for @challengeChallengesX. + /// + /// In en, this message translates to: + /// **'Challenges: {param1}'** + String challengeChallengesX(String param1); + + /// No description provided for @challengeChallengeToPlay. + /// + /// In en, this message translates to: + /// **'Challenge to a game'** + String get challengeChallengeToPlay; + + /// No description provided for @challengeChallengeDeclined. + /// + /// In en, this message translates to: + /// **'Challenge declined.'** + String get challengeChallengeDeclined; + + /// No description provided for @challengeChallengeAccepted. + /// + /// In en, this message translates to: + /// **'Challenge accepted!'** + String get challengeChallengeAccepted; /// No description provided for @challengeChallengeCanceled. /// @@ -1062,6 +1512,12 @@ abstract class AppLocalizations { /// **'In-game only'** String get preferencesInGameOnly; + /// No description provided for @preferencesExceptInGame. + /// + /// In en, this message translates to: + /// **'Except in-game'** + String get preferencesExceptInGame; + /// No description provided for @preferencesChessClock. /// /// In en, this message translates to: @@ -1344,6 +1800,12 @@ abstract class AppLocalizations { /// **'Bell notification sound'** String get preferencesBellNotificationSound; + /// No description provided for @preferencesBlindfold. + /// + /// In en, this message translates to: + /// **'Blindfold'** + String get preferencesBlindfold; + /// No description provided for @puzzlePuzzles. /// /// In en, this message translates to: @@ -2514,17 +2976,17 @@ abstract class AppLocalizations { /// **'The opponent is limited in the moves they can make, and all moves worsen their position.'** String get puzzleThemeZugzwangDescription; - /// No description provided for @puzzleThemeHealthyMix. + /// No description provided for @puzzleThemeMix. /// /// In en, this message translates to: /// **'Healthy mix'** - String get puzzleThemeHealthyMix; + String get puzzleThemeMix; - /// No description provided for @puzzleThemeHealthyMixDescription. + /// No description provided for @puzzleThemeMixDescription. /// /// In en, this message translates to: /// **'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'** - String get puzzleThemeHealthyMixDescription; + String get puzzleThemeMixDescription; /// No description provided for @puzzleThemePlayerGames. /// @@ -3246,12 +3708,6 @@ abstract class AppLocalizations { /// **'By CPL'** String get byCPL; - /// No description provided for @openStudy. - /// - /// In en, this message translates to: - /// **'Open study'** - String get openStudy; - /// No description provided for @enable. /// /// In en, this message translates to: @@ -3306,12 +3762,6 @@ abstract class AppLocalizations { /// **'Removes the depth limit, and keeps your computer warm'** String get removesTheDepthLimit; - /// No description provided for @engineManager. - /// - /// In en, this message translates to: - /// **'Engine manager'** - String get engineManager; - /// No description provided for @blunder. /// /// In en, this message translates to: @@ -3798,6 +4248,12 @@ abstract class AppLocalizations { /// **'Games played'** String get gamesPlayed; + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'OK'** + String get ok; + /// No description provided for @cancel. /// /// In en, this message translates to: @@ -4494,12 +4950,6 @@ abstract class AppLocalizations { /// **'Unblock'** String get unblock; - /// No description provided for @followsYou. - /// - /// In en, this message translates to: - /// **'Follows you'** - String get followsYou; - /// No description provided for @xStartedFollowingY. /// /// In en, this message translates to: @@ -5130,13 +5580,25 @@ abstract class AppLocalizations { /// **'Other'** String get other; - /// No description provided for @reportDescriptionHelp. + /// No description provided for @reportCheatBoostHelp. /// /// In en, this message translates to: - /// **'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'** - String get reportDescriptionHelp; + /// **'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'** + String get reportCheatBoostHelp; - /// No description provided for @error_provideOneCheatedGameLink. + /// No description provided for @reportUsernameHelp. + /// + /// In en, this message translates to: + /// **'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'** + String get reportUsernameHelp; + + /// No description provided for @reportProcessedFasterInEnglish. + /// + /// In en, this message translates to: + /// **'Your report will be processed faster if written in English.'** + String get reportProcessedFasterInEnglish; + + /// No description provided for @error_provideOneCheatedGameLink. /// /// In en, this message translates to: /// **'Please provide at least one link to a cheated game.'** @@ -7518,6 +7980,12 @@ abstract class AppLocalizations { /// **'Nothing to see here at the moment.'** String get nothingToSeeHere; + /// No description provided for @stats. + /// + /// In en, this message translates to: + /// **'Stats'** + String get stats; + /// No description provided for @opponentLeftCounter. /// /// In en, this message translates to: @@ -8094,17 +8562,1049 @@ abstract class AppLocalizations { /// **'Lichess streamers'** String get streamerLichessStreamers; + /// No description provided for @studyPrivate. + /// + /// In en, this message translates to: + /// **'Private'** + String get studyPrivate; + + /// No description provided for @studyMyStudies. + /// + /// In en, this message translates to: + /// **'My studies'** + String get studyMyStudies; + + /// No description provided for @studyStudiesIContributeTo. + /// + /// In en, this message translates to: + /// **'Studies I contribute to'** + String get studyStudiesIContributeTo; + + /// No description provided for @studyMyPublicStudies. + /// + /// In en, this message translates to: + /// **'My public studies'** + String get studyMyPublicStudies; + + /// No description provided for @studyMyPrivateStudies. + /// + /// In en, this message translates to: + /// **'My private studies'** + String get studyMyPrivateStudies; + + /// No description provided for @studyMyFavoriteStudies. + /// + /// In en, this message translates to: + /// **'My favourite studies'** + String get studyMyFavoriteStudies; + + /// No description provided for @studyWhatAreStudies. + /// + /// In en, this message translates to: + /// **'What are studies?'** + String get studyWhatAreStudies; + + /// No description provided for @studyAllStudies. + /// + /// In en, this message translates to: + /// **'All studies'** + String get studyAllStudies; + + /// No description provided for @studyStudiesCreatedByX. + /// + /// In en, this message translates to: + /// **'Studies created by {param}'** + String studyStudiesCreatedByX(String param); + + /// No description provided for @studyNoneYet. + /// + /// In en, this message translates to: + /// **'None yet.'** + String get studyNoneYet; + + /// No description provided for @studyHot. + /// + /// In en, this message translates to: + /// **'Hot'** + String get studyHot; + + /// No description provided for @studyDateAddedNewest. + /// + /// In en, this message translates to: + /// **'Date added (newest)'** + String get studyDateAddedNewest; + + /// No description provided for @studyDateAddedOldest. + /// + /// In en, this message translates to: + /// **'Date added (oldest)'** + String get studyDateAddedOldest; + + /// No description provided for @studyRecentlyUpdated. + /// + /// In en, this message translates to: + /// **'Recently updated'** + String get studyRecentlyUpdated; + + /// No description provided for @studyMostPopular. + /// + /// In en, this message translates to: + /// **'Most popular'** + String get studyMostPopular; + + /// No description provided for @studyAlphabetical. + /// + /// In en, this message translates to: + /// **'Alphabetical'** + String get studyAlphabetical; + + /// No description provided for @studyAddNewChapter. + /// + /// In en, this message translates to: + /// **'Add a new chapter'** + String get studyAddNewChapter; + + /// No description provided for @studyAddMembers. + /// + /// In en, this message translates to: + /// **'Add members'** + String get studyAddMembers; + + /// No description provided for @studyInviteToTheStudy. + /// + /// In en, this message translates to: + /// **'Invite to the study'** + String get studyInviteToTheStudy; + + /// No description provided for @studyPleaseOnlyInvitePeopleYouKnow. + /// + /// In en, this message translates to: + /// **'Please only invite people who know you, and who actively want to join this study.'** + String get studyPleaseOnlyInvitePeopleYouKnow; + + /// No description provided for @studySearchByUsername. + /// + /// In en, this message translates to: + /// **'Search by username'** + String get studySearchByUsername; + + /// No description provided for @studySpectator. + /// + /// In en, this message translates to: + /// **'Spectator'** + String get studySpectator; + + /// No description provided for @studyContributor. + /// + /// In en, this message translates to: + /// **'Contributor'** + String get studyContributor; + + /// No description provided for @studyKick. + /// + /// In en, this message translates to: + /// **'Kick'** + String get studyKick; + + /// No description provided for @studyLeaveTheStudy. + /// + /// In en, this message translates to: + /// **'Leave the study'** + String get studyLeaveTheStudy; + + /// No description provided for @studyYouAreNowAContributor. + /// + /// In en, this message translates to: + /// **'You are now a contributor'** + String get studyYouAreNowAContributor; + + /// No description provided for @studyYouAreNowASpectator. + /// + /// In en, this message translates to: + /// **'You are now a spectator'** + String get studyYouAreNowASpectator; + + /// No description provided for @studyPgnTags. + /// + /// In en, this message translates to: + /// **'PGN tags'** + String get studyPgnTags; + + /// No description provided for @studyLike. + /// + /// In en, this message translates to: + /// **'Like'** + String get studyLike; + + /// No description provided for @studyUnlike. + /// + /// In en, this message translates to: + /// **'Unlike'** + String get studyUnlike; + + /// No description provided for @studyNewTag. + /// + /// In en, this message translates to: + /// **'New tag'** + String get studyNewTag; + + /// No description provided for @studyCommentThisPosition. + /// + /// In en, this message translates to: + /// **'Comment on this position'** + String get studyCommentThisPosition; + + /// No description provided for @studyCommentThisMove. + /// + /// In en, this message translates to: + /// **'Comment on this move'** + String get studyCommentThisMove; + + /// No description provided for @studyAnnotateWithGlyphs. + /// + /// In en, this message translates to: + /// **'Annotate with glyphs'** + String get studyAnnotateWithGlyphs; + + /// No description provided for @studyTheChapterIsTooShortToBeAnalysed. + /// + /// In en, this message translates to: + /// **'The chapter is too short to be analysed.'** + String get studyTheChapterIsTooShortToBeAnalysed; + + /// No description provided for @studyOnlyContributorsCanRequestAnalysis. + /// + /// In en, this message translates to: + /// **'Only the study contributors can request a computer analysis.'** + String get studyOnlyContributorsCanRequestAnalysis; + + /// No description provided for @studyGetAFullComputerAnalysis. + /// + /// In en, this message translates to: + /// **'Get a full server-side computer analysis of the mainline.'** + String get studyGetAFullComputerAnalysis; + + /// No description provided for @studyMakeSureTheChapterIsComplete. + /// + /// In en, this message translates to: + /// **'Make sure the chapter is complete. You can only request analysis once.'** + String get studyMakeSureTheChapterIsComplete; + + /// No description provided for @studyAllSyncMembersRemainOnTheSamePosition. + /// + /// In en, this message translates to: + /// **'All SYNC members remain on the same position'** + String get studyAllSyncMembersRemainOnTheSamePosition; + + /// No description provided for @studyShareChanges. + /// + /// In en, this message translates to: + /// **'Share changes with spectators and save them on the server'** + String get studyShareChanges; + + /// No description provided for @studyPlaying. + /// + /// In en, this message translates to: + /// **'Playing'** + String get studyPlaying; + + /// No description provided for @studyShowEvalBar. + /// + /// In en, this message translates to: + /// **'Evaluation bars'** + String get studyShowEvalBar; + + /// No description provided for @studyFirst. + /// + /// In en, this message translates to: + /// **'First'** + String get studyFirst; + + /// No description provided for @studyPrevious. + /// + /// In en, this message translates to: + /// **'Previous'** + String get studyPrevious; + + /// No description provided for @studyNext. + /// + /// In en, this message translates to: + /// **'Next'** + String get studyNext; + + /// No description provided for @studyLast. + /// + /// In en, this message translates to: + /// **'Last'** + String get studyLast; + /// No description provided for @studyShareAndExport. /// /// In en, this message translates to: /// **'Share & export'** String get studyShareAndExport; - /// No description provided for @studyStart. + /// No description provided for @studyCloneStudy. /// /// In en, this message translates to: - /// **'Start'** - String get studyStart; + /// **'Clone'** + String get studyCloneStudy; + + /// No description provided for @studyStudyPgn. + /// + /// In en, this message translates to: + /// **'Study PGN'** + String get studyStudyPgn; + + /// No description provided for @studyDownloadAllGames. + /// + /// In en, this message translates to: + /// **'Download all games'** + String get studyDownloadAllGames; + + /// No description provided for @studyChapterPgn. + /// + /// In en, this message translates to: + /// **'Chapter PGN'** + String get studyChapterPgn; + + /// No description provided for @studyCopyChapterPgn. + /// + /// In en, this message translates to: + /// **'Copy PGN'** + String get studyCopyChapterPgn; + + /// No description provided for @studyDownloadGame. + /// + /// In en, this message translates to: + /// **'Download game'** + String get studyDownloadGame; + + /// No description provided for @studyStudyUrl. + /// + /// In en, this message translates to: + /// **'Study URL'** + String get studyStudyUrl; + + /// No description provided for @studyCurrentChapterUrl. + /// + /// In en, this message translates to: + /// **'Current chapter URL'** + String get studyCurrentChapterUrl; + + /// No description provided for @studyYouCanPasteThisInTheForumToEmbed. + /// + /// In en, this message translates to: + /// **'You can paste this in the forum or your Lichess blog to embed'** + String get studyYouCanPasteThisInTheForumToEmbed; + + /// No description provided for @studyStartAtInitialPosition. + /// + /// In en, this message translates to: + /// **'Start at initial position'** + String get studyStartAtInitialPosition; + + /// No description provided for @studyStartAtX. + /// + /// In en, this message translates to: + /// **'Start at {param}'** + String studyStartAtX(String param); + + /// No description provided for @studyEmbedInYourWebsite. + /// + /// In en, this message translates to: + /// **'Embed in your website'** + String get studyEmbedInYourWebsite; + + /// No description provided for @studyReadMoreAboutEmbedding. + /// + /// In en, this message translates to: + /// **'Read more about embedding'** + String get studyReadMoreAboutEmbedding; + + /// No description provided for @studyOnlyPublicStudiesCanBeEmbedded. + /// + /// In en, this message translates to: + /// **'Only public studies can be embedded!'** + String get studyOnlyPublicStudiesCanBeEmbedded; + + /// No description provided for @studyOpen. + /// + /// In en, this message translates to: + /// **'Open'** + String get studyOpen; + + /// No description provided for @studyXBroughtToYouByY. + /// + /// In en, this message translates to: + /// **'{param1}, brought to you by {param2}'** + String studyXBroughtToYouByY(String param1, String param2); + + /// No description provided for @studyStudyNotFound. + /// + /// In en, this message translates to: + /// **'Study not found'** + String get studyStudyNotFound; + + /// No description provided for @studyEditChapter. + /// + /// In en, this message translates to: + /// **'Edit chapter'** + String get studyEditChapter; + + /// No description provided for @studyNewChapter. + /// + /// In en, this message translates to: + /// **'New chapter'** + String get studyNewChapter; + + /// No description provided for @studyImportFromChapterX. + /// + /// In en, this message translates to: + /// **'Import from {param}'** + String studyImportFromChapterX(String param); + + /// No description provided for @studyOrientation. + /// + /// In en, this message translates to: + /// **'Orientation'** + String get studyOrientation; + + /// No description provided for @studyAnalysisMode. + /// + /// In en, this message translates to: + /// **'Analysis mode'** + String get studyAnalysisMode; + + /// No description provided for @studyPinnedChapterComment. + /// + /// In en, this message translates to: + /// **'Pinned chapter comment'** + String get studyPinnedChapterComment; + + /// No description provided for @studySaveChapter. + /// + /// In en, this message translates to: + /// **'Save chapter'** + String get studySaveChapter; + + /// No description provided for @studyClearAnnotations. + /// + /// In en, this message translates to: + /// **'Clear annotations'** + String get studyClearAnnotations; + + /// No description provided for @studyClearVariations. + /// + /// In en, this message translates to: + /// **'Clear variations'** + String get studyClearVariations; + + /// No description provided for @studyDeleteChapter. + /// + /// In en, this message translates to: + /// **'Delete chapter'** + String get studyDeleteChapter; + + /// No description provided for @studyDeleteThisChapter. + /// + /// In en, this message translates to: + /// **'Delete this chapter. There is no going back!'** + String get studyDeleteThisChapter; + + /// No description provided for @studyClearAllCommentsInThisChapter. + /// + /// In en, this message translates to: + /// **'Clear all comments, glyphs and drawn shapes in this chapter'** + String get studyClearAllCommentsInThisChapter; + + /// No description provided for @studyRightUnderTheBoard. + /// + /// In en, this message translates to: + /// **'Right under the board'** + String get studyRightUnderTheBoard; + + /// No description provided for @studyNoPinnedComment. + /// + /// In en, this message translates to: + /// **'None'** + String get studyNoPinnedComment; + + /// No description provided for @studyNormalAnalysis. + /// + /// In en, this message translates to: + /// **'Normal analysis'** + String get studyNormalAnalysis; + + /// No description provided for @studyHideNextMoves. + /// + /// In en, this message translates to: + /// **'Hide next moves'** + String get studyHideNextMoves; + + /// No description provided for @studyInteractiveLesson. + /// + /// In en, this message translates to: + /// **'Interactive lesson'** + String get studyInteractiveLesson; + + /// No description provided for @studyChapterX. + /// + /// In en, this message translates to: + /// **'Chapter {param}'** + String studyChapterX(String param); + + /// No description provided for @studyEmpty. + /// + /// In en, this message translates to: + /// **'Empty'** + String get studyEmpty; + + /// No description provided for @studyStartFromInitialPosition. + /// + /// In en, this message translates to: + /// **'Start from initial position'** + String get studyStartFromInitialPosition; + + /// No description provided for @studyEditor. + /// + /// In en, this message translates to: + /// **'Editor'** + String get studyEditor; + + /// No description provided for @studyStartFromCustomPosition. + /// + /// In en, this message translates to: + /// **'Start from custom position'** + String get studyStartFromCustomPosition; + + /// No description provided for @studyLoadAGameByUrl. + /// + /// In en, this message translates to: + /// **'Load games by URLs'** + String get studyLoadAGameByUrl; + + /// No description provided for @studyLoadAPositionFromFen. + /// + /// In en, this message translates to: + /// **'Load a position from FEN'** + String get studyLoadAPositionFromFen; + + /// No description provided for @studyLoadAGameFromPgn. + /// + /// In en, this message translates to: + /// **'Load games from PGN'** + String get studyLoadAGameFromPgn; + + /// No description provided for @studyAutomatic. + /// + /// In en, this message translates to: + /// **'Automatic'** + String get studyAutomatic; + + /// No description provided for @studyUrlOfTheGame. + /// + /// In en, this message translates to: + /// **'URL of the games, one per line'** + String get studyUrlOfTheGame; + + /// No description provided for @studyLoadAGameFromXOrY. + /// + /// In en, this message translates to: + /// **'Load games from {param1} or {param2}'** + String studyLoadAGameFromXOrY(String param1, String param2); + + /// No description provided for @studyCreateChapter. + /// + /// In en, this message translates to: + /// **'Create chapter'** + String get studyCreateChapter; + + /// No description provided for @studyCreateStudy. + /// + /// In en, this message translates to: + /// **'Create study'** + String get studyCreateStudy; + + /// No description provided for @studyEditStudy. + /// + /// In en, this message translates to: + /// **'Edit study'** + String get studyEditStudy; + + /// No description provided for @studyVisibility. + /// + /// In en, this message translates to: + /// **'Visibility'** + String get studyVisibility; + + /// No description provided for @studyPublic. + /// + /// In en, this message translates to: + /// **'Public'** + String get studyPublic; + + /// No description provided for @studyUnlisted. + /// + /// In en, this message translates to: + /// **'Unlisted'** + String get studyUnlisted; + + /// No description provided for @studyInviteOnly. + /// + /// In en, this message translates to: + /// **'Invite only'** + String get studyInviteOnly; + + /// No description provided for @studyAllowCloning. + /// + /// In en, this message translates to: + /// **'Allow cloning'** + String get studyAllowCloning; + + /// No description provided for @studyNobody. + /// + /// In en, this message translates to: + /// **'Nobody'** + String get studyNobody; + + /// No description provided for @studyOnlyMe. + /// + /// In en, this message translates to: + /// **'Only me'** + String get studyOnlyMe; + + /// No description provided for @studyContributors. + /// + /// In en, this message translates to: + /// **'Contributors'** + String get studyContributors; + + /// No description provided for @studyMembers. + /// + /// In en, this message translates to: + /// **'Members'** + String get studyMembers; + + /// No description provided for @studyEveryone. + /// + /// In en, this message translates to: + /// **'Everyone'** + String get studyEveryone; + + /// No description provided for @studyEnableSync. + /// + /// In en, this message translates to: + /// **'Enable sync'** + String get studyEnableSync; + + /// No description provided for @studyYesKeepEveryoneOnTheSamePosition. + /// + /// In en, this message translates to: + /// **'Yes: keep everyone on the same position'** + String get studyYesKeepEveryoneOnTheSamePosition; + + /// No description provided for @studyNoLetPeopleBrowseFreely. + /// + /// In en, this message translates to: + /// **'No: let people browse freely'** + String get studyNoLetPeopleBrowseFreely; + + /// No description provided for @studyPinnedStudyComment. + /// + /// In en, this message translates to: + /// **'Pinned study comment'** + String get studyPinnedStudyComment; + + /// No description provided for @studyStart. + /// + /// In en, this message translates to: + /// **'Start'** + String get studyStart; + + /// No description provided for @studySave. + /// + /// In en, this message translates to: + /// **'Save'** + String get studySave; + + /// No description provided for @studyClearChat. + /// + /// In en, this message translates to: + /// **'Clear chat'** + String get studyClearChat; + + /// No description provided for @studyDeleteTheStudyChatHistory. + /// + /// In en, this message translates to: + /// **'Delete the study chat history? There is no going back!'** + String get studyDeleteTheStudyChatHistory; + + /// No description provided for @studyDeleteStudy. + /// + /// In en, this message translates to: + /// **'Delete study'** + String get studyDeleteStudy; + + /// No description provided for @studyConfirmDeleteStudy. + /// + /// In en, this message translates to: + /// **'Delete the entire study? There is no going back! Type the name of the study to confirm: {param}'** + String studyConfirmDeleteStudy(String param); + + /// No description provided for @studyWhereDoYouWantToStudyThat. + /// + /// In en, this message translates to: + /// **'Where do you want to study that?'** + String get studyWhereDoYouWantToStudyThat; + + /// No description provided for @studyGoodMove. + /// + /// In en, this message translates to: + /// **'Good move'** + String get studyGoodMove; + + /// No description provided for @studyMistake. + /// + /// In en, this message translates to: + /// **'Mistake'** + String get studyMistake; + + /// No description provided for @studyBrilliantMove. + /// + /// In en, this message translates to: + /// **'Brilliant move'** + String get studyBrilliantMove; + + /// No description provided for @studyBlunder. + /// + /// In en, this message translates to: + /// **'Blunder'** + String get studyBlunder; + + /// No description provided for @studyInterestingMove. + /// + /// In en, this message translates to: + /// **'Interesting move'** + String get studyInterestingMove; + + /// No description provided for @studyDubiousMove. + /// + /// In en, this message translates to: + /// **'Dubious move'** + String get studyDubiousMove; + + /// No description provided for @studyOnlyMove. + /// + /// In en, this message translates to: + /// **'Only move'** + String get studyOnlyMove; + + /// No description provided for @studyZugzwang. + /// + /// In en, this message translates to: + /// **'Zugzwang'** + String get studyZugzwang; + + /// No description provided for @studyEqualPosition. + /// + /// In en, this message translates to: + /// **'Equal position'** + String get studyEqualPosition; + + /// No description provided for @studyUnclearPosition. + /// + /// In en, this message translates to: + /// **'Unclear position'** + String get studyUnclearPosition; + + /// No description provided for @studyWhiteIsSlightlyBetter. + /// + /// In en, this message translates to: + /// **'White is slightly better'** + String get studyWhiteIsSlightlyBetter; + + /// No description provided for @studyBlackIsSlightlyBetter. + /// + /// In en, this message translates to: + /// **'Black is slightly better'** + String get studyBlackIsSlightlyBetter; + + /// No description provided for @studyWhiteIsBetter. + /// + /// In en, this message translates to: + /// **'White is better'** + String get studyWhiteIsBetter; + + /// No description provided for @studyBlackIsBetter. + /// + /// In en, this message translates to: + /// **'Black is better'** + String get studyBlackIsBetter; + + /// No description provided for @studyWhiteIsWinning. + /// + /// In en, this message translates to: + /// **'White is winning'** + String get studyWhiteIsWinning; + + /// No description provided for @studyBlackIsWinning. + /// + /// In en, this message translates to: + /// **'Black is winning'** + String get studyBlackIsWinning; + + /// No description provided for @studyNovelty. + /// + /// In en, this message translates to: + /// **'Novelty'** + String get studyNovelty; + + /// No description provided for @studyDevelopment. + /// + /// In en, this message translates to: + /// **'Development'** + String get studyDevelopment; + + /// No description provided for @studyInitiative. + /// + /// In en, this message translates to: + /// **'Initiative'** + String get studyInitiative; + + /// No description provided for @studyAttack. + /// + /// In en, this message translates to: + /// **'Attack'** + String get studyAttack; + + /// No description provided for @studyCounterplay. + /// + /// In en, this message translates to: + /// **'Counterplay'** + String get studyCounterplay; + + /// No description provided for @studyTimeTrouble. + /// + /// In en, this message translates to: + /// **'Time trouble'** + String get studyTimeTrouble; + + /// No description provided for @studyWithCompensation. + /// + /// In en, this message translates to: + /// **'With compensation'** + String get studyWithCompensation; + + /// No description provided for @studyWithTheIdea. + /// + /// In en, this message translates to: + /// **'With the idea'** + String get studyWithTheIdea; + + /// No description provided for @studyNextChapter. + /// + /// In en, this message translates to: + /// **'Next chapter'** + String get studyNextChapter; + + /// No description provided for @studyPrevChapter. + /// + /// In en, this message translates to: + /// **'Previous chapter'** + String get studyPrevChapter; + + /// No description provided for @studyStudyActions. + /// + /// In en, this message translates to: + /// **'Study actions'** + String get studyStudyActions; + + /// No description provided for @studyTopics. + /// + /// In en, this message translates to: + /// **'Topics'** + String get studyTopics; + + /// No description provided for @studyMyTopics. + /// + /// In en, this message translates to: + /// **'My topics'** + String get studyMyTopics; + + /// No description provided for @studyPopularTopics. + /// + /// In en, this message translates to: + /// **'Popular topics'** + String get studyPopularTopics; + + /// No description provided for @studyManageTopics. + /// + /// In en, this message translates to: + /// **'Manage topics'** + String get studyManageTopics; + + /// No description provided for @studyBack. + /// + /// In en, this message translates to: + /// **'Back'** + String get studyBack; + + /// No description provided for @studyPlayAgain. + /// + /// In en, this message translates to: + /// **'Play again'** + String get studyPlayAgain; + + /// No description provided for @studyWhatWouldYouPlay. + /// + /// In en, this message translates to: + /// **'What would you play in this position?'** + String get studyWhatWouldYouPlay; + + /// No description provided for @studyYouCompletedThisLesson. + /// + /// In en, this message translates to: + /// **'Congratulations! You completed this lesson.'** + String get studyYouCompletedThisLesson; + + /// No description provided for @studyPerPage. + /// + /// In en, this message translates to: + /// **'{param} per page'** + String studyPerPage(String param); + + /// No description provided for @studyNbChapters. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Chapter} other{{count} Chapters}}'** + String studyNbChapters(int count); + + /// No description provided for @studyNbGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Game} other{{count} Games}}'** + String studyNbGames(int count); + + /// No description provided for @studyNbMembers. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Member} other{{count} Members}}'** + String studyNbMembers(int count); + + /// No description provided for @studyPasteYourPgnTextHereUpToNbGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}'** + String studyPasteYourPgnTextHereUpToNbGames(int count); + + /// No description provided for @timeagoJustNow. + /// + /// In en, this message translates to: + /// **'just now'** + String get timeagoJustNow; + + /// No description provided for @timeagoRightNow. + /// + /// In en, this message translates to: + /// **'right now'** + String get timeagoRightNow; + + /// No description provided for @timeagoCompleted. + /// + /// In en, this message translates to: + /// **'completed'** + String get timeagoCompleted; + + /// No description provided for @timeagoInNbSeconds. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} second} other{in {count} seconds}}'** + String timeagoInNbSeconds(int count); + + /// No description provided for @timeagoInNbMinutes. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} minute} other{in {count} minutes}}'** + String timeagoInNbMinutes(int count); + + /// No description provided for @timeagoInNbHours. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} hour} other{in {count} hours}}'** + String timeagoInNbHours(int count); + + /// No description provided for @timeagoInNbDays. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} day} other{in {count} days}}'** + String timeagoInNbDays(int count); + + /// No description provided for @timeagoInNbWeeks. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} week} other{in {count} weeks}}'** + String timeagoInNbWeeks(int count); + + /// No description provided for @timeagoInNbMonths. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} month} other{in {count} months}}'** + String timeagoInNbMonths(int count); + + /// No description provided for @timeagoInNbYears. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} year} other{in {count} years}}'** + String timeagoInNbYears(int count); + + /// No description provided for @timeagoNbMinutesAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} minute ago} other{{count} minutes ago}}'** + String timeagoNbMinutesAgo(int count); + + /// No description provided for @timeagoNbHoursAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} hour ago} other{{count} hours ago}}'** + String timeagoNbHoursAgo(int count); + + /// No description provided for @timeagoNbDaysAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} day ago} other{{count} days ago}}'** + String timeagoNbDaysAgo(int count); + + /// No description provided for @timeagoNbWeeksAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} week ago} other{{count} weeks ago}}'** + String timeagoNbWeeksAgo(int count); + + /// No description provided for @timeagoNbMonthsAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} month ago} other{{count} months ago}}'** + String timeagoNbMonthsAgo(int count); + + /// No description provided for @timeagoNbYearsAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} year ago} other{{count} years ago}}'** + String timeagoNbYearsAgo(int count); + + /// No description provided for @timeagoNbMinutesRemaining. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}'** + String timeagoNbMinutesRemaining(int count); + + /// No description provided for @timeagoNbHoursRemaining. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}'** + String timeagoNbHoursRemaining(int count); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 4e5ee67e73..eeadbe5be6 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsAf extends AppLocalizations { AppLocalizationsAf([String locale = 'af']) : super(locale); @override - String get mobileHomeTab => 'Tuis'; + String get mobileAllGames => 'Alle spelle'; @override - String get mobilePuzzlesTab => 'Kopkrappers'; + String get mobileAreYouSure => 'Is jy seker?'; @override - String get mobileToolsTab => 'Hulpmiddels'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Hou dop'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Instellings'; + String get mobileCorrespondenceClearSavedMove => 'Vee gestoorde skuif uit'; @override - String get mobileMustBeLoggedIn => 'Jy moet ingeteken wees om hierdie bladsy te kan sien.'; + String get mobileCustomGameJoinAGame => 'Sluit aan by \'n spel'; @override - String get mobileSystemColors => 'Stelselkleure'; + String get mobileFeedbackButton => 'Terugvoer'; @override - String get mobileFeedbackButton => 'Terugvoer'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileOkButton => 'Reg'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsHapticFeedback => 'Vibrasieterugvoer'; + String get mobileHideVariation => 'Verberg variasie'; @override - String get mobileSettingsImmersiveMode => 'Volskermmodus'; + String get mobileHomeTab => 'Tuis'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'Jy volg nie enige gebruikers nie.'; + String get mobileMustBeLoggedIn => 'Jy moet ingeteken wees om hierdie bladsy te kan sien.'; @override - String get mobileAllGames => 'Alle spelle'; + String get mobileNoSearchResults => 'Geen resultate nie'; @override - String get mobileRecentSearches => 'Onlangse soektogte'; + String get mobileNotFollowingAnyUser => 'Jy volg nie enige gebruikers nie.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'Reg'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsAf extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Geen resultate nie'; + String get mobilePrefMagnifyDraggedPiece => 'Vergroot gesleepte stuk'; @override - String get mobileAreYouSure => 'Is jy seker?'; + String get mobilePuzzleStormConfirmEndRun => 'Wil jy hierdie lopie beëindig?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niks om te wys nie; verander asb. die filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Deel hierdie kopkrapper'; + String get mobilePuzzleStormSubtitle => 'Los soveel kopkrappers moontlik op in 3 minute.'; @override - String get mobileShareGameURL => 'Deel spel se bronadres'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Deel PGN'; + String get mobilePuzzleThemesSubtitle => 'Doen kopkrappers van jou gunstelingopenings, of kies \'n tema.'; @override - String get mobileSharePositionAsFEN => 'Deel posisie as FEN'; + String get mobilePuzzlesTab => 'Kopkrappers'; @override - String get mobileShowVariations => 'Wys variasies'; + String get mobileRecentSearches => 'Onlangse soektogte'; @override - String get mobileHideVariation => 'Verberg variasie'; + String get mobileSettingsHapticFeedback => 'Vibrasieterugvoer'; @override - String get mobileShowComments => 'Wys kommentaar'; + String get mobileSettingsImmersiveMode => 'Volskermmodus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wil jy hierdie lopie beëindig?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Niks om te wys nie; verander asb. die filters'; + String get mobileSettingsTab => 'Instellings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Deel PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Deel spel se bronadres'; @override - String get mobileWaitingForOpponentToJoin => 'Wag vir opponent om aan te sluit...'; + String get mobileSharePositionAsFEN => 'Deel posisie as FEN'; @override - String get mobileBlindfoldMode => 'Geblinddoek'; + String get mobileSharePuzzle => 'Deel hierdie kopkrapper'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Wys kommentaar'; @override - String get mobileCustomGameJoinAGame => 'Sluit aan by \'n spel'; + String get mobileShowResult => 'Wys resultaat'; @override - String get mobileCorrespondenceClearSavedMove => 'Vee gestoorde skuif uit'; + String get mobileShowVariations => 'Wys variasies'; @override String get mobileSomethingWentWrong => 'Iets het skeefgeloop.'; @override - String get mobileShowResult => 'Wys resultaat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Doen kopkrappers van jou gunstelingopenings, of kies \'n tema.'; + String get mobileSystemColors => 'Stelselkleure'; @override - String get mobilePuzzleStormSubtitle => 'Los soveel kopkrappers moontlik op in 3 minute.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Hulpmiddels'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Wag vir opponent om aan te sluit...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Hou dop'; @override String get activityActivity => 'Aktiwiteite'; @@ -246,6 +243,17 @@ class AppLocalizationsAf extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsAf extends AppLocalizations { @override String get broadcastBroadcasts => 'Uitsendings'; + @override + String get broadcastMyBroadcasts => 'My uitsendings'; + @override String get broadcastLiveBroadcasts => 'Regstreekse toernooi uitsendings'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nuwe regstreekse uitsendings'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Voeg \'n ronde by'; + + @override + String get broadcastOngoing => 'Deurlopend'; + + @override + String get broadcastUpcoming => 'Opkomend'; + + @override + String get broadcastCompleted => 'Voltooi'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ronde se naam'; + + @override + String get broadcastRoundNumber => 'Ronde getal'; + + @override + String get broadcastTournamentName => 'Toernooi se naam'; + + @override + String get broadcastTournamentDescription => 'Kort beskrywing van die toernooi'; + + @override + String get broadcastFullDescription => 'Volle geleentheid beskrywing'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opsionele lang beskrywing van die uitsending. $param1 is beskikbaar. Lengte moet minder as $param2 karakters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN-Bronskakel'; + + @override + String get broadcastSourceUrlHelp => 'URL wat Lichess sal nagaan vir PGN opdaterings. Dit moet openbaar beskikbaar wees vanaf die Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optioneel, indien jy weet wanner die geleentheid begin'; + + @override + String get broadcastCurrentGameUrl => 'Huidige spel se bronadres'; + + @override + String get broadcastDownloadAllRounds => 'Laai al die rondes af'; + + @override + String get broadcastResetRound => 'Herstel die ronde'; + + @override + String get broadcastDeleteRound => 'Skrap die ronde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Skrap die rondte en sy spelle beslis uit.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Skrap alle spelle van hierdie rondte. Die bron sal aktief moet wees om hulle te kan herskep.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Vee hierdie toernooi uit'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Vee beslis die hele toernooi uit, met al sy rondtes en spelle.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Opsioneel: vervang spelername, graderings en titels'; + + @override + String get broadcastFideFederations => 'FIDE-federasies'; + + @override + String get broadcastTop10Rating => 'Top 10 gradering'; + + @override + String get broadcastFidePlayers => 'FIDE-deelnemers'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-deelnemer nie gevind nie'; + + @override + String get broadcastFideProfile => 'FIDE-profiel'; + + @override + String get broadcastFederation => 'Federasie'; + + @override + String get broadcastAgeThisYear => 'Ouderdom vanjaar'; + + @override + String get broadcastUnrated => 'Ongegradeerd'; + + @override + String get broadcastRecentTournaments => 'Onlangse toernooie'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Uitdagings: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Skaakklok'; @@ -750,6 +1008,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klokkie kennisgewing klank'; + @override + String get preferencesBlindfold => 'Blinddoek'; + @override String get puzzlePuzzles => 'Raaisels'; @@ -1390,10 +1651,10 @@ class AppLocalizationsAf extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Die opponent is beperk in die bewegings wat hulle kan maak, en alle bewegings vererger hul posisie.'; @override - String get puzzleThemeHealthyMix => 'Gesonde mengsel'; + String get puzzleThemeMix => 'Gesonde mengsel'; @override - String get puzzleThemeHealthyMixDescription => '\'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.'; + String get puzzleThemeMixDescription => '\'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.'; @override String get puzzleThemePlayerGames => 'Speler se spelle'; @@ -1767,9 +2028,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get byCPL => 'Met CPL'; - @override - String get openStudy => 'Open studie'; - @override String get enable => 'Aktief'; @@ -1797,9 +2055,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get removesTheDepthLimit => 'Verwyder dieptelimiet, en hou jou rekenaar warm'; - @override - String get engineManager => 'Enjinbestuurder'; - @override String get blunder => 'Flater'; @@ -2063,6 +2318,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get gamesPlayed => 'Spelle gespeel'; + @override + String get ok => 'OK'; + @override String get cancel => 'Kanseleer'; @@ -2121,7 +2379,7 @@ class AppLocalizationsAf extends AppLocalizations { String get standard => 'Standaard'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Gebruiklike Posisie'; @override String get unlimited => 'Oneindig'; @@ -2404,7 +2662,7 @@ class AppLocalizationsAf extends AppLocalizations { String get reconnecting => 'Konnekteer weer'; @override - String get noNetwork => 'Offline'; + String get noNetwork => 'Vanlyn af'; @override String get favoriteOpponents => 'Gunsteling opponente'; @@ -2437,9 +2695,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get unblock => 'Ontblok'; - @override - String get followsYou => 'Volg jou'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 het begin om $param2 te volg'; @@ -2631,13 +2886,13 @@ class AppLocalizationsAf extends AppLocalizations { String get editProfile => 'Verander profiel'; @override - String get realName => 'Real name'; + String get realName => 'Regte naam'; @override - String get setFlair => 'Set your flair'; + String get setFlair => 'Stel jou Vlam'; @override - String get flair => 'Flair'; + String get flair => 'Vlam'; @override String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; @@ -2772,7 +3027,13 @@ class AppLocalizationsAf extends AppLocalizations { String get other => 'Ander'; @override - String get reportDescriptionHelp => 'Plak skakel na die spel(le) en verduidelik wat skort met die lid se gedrag. Moenie net sê hulle kroek nie, maar verduidelik hoe daardie gevolgtrekking bereik is. Jou verslag sal vinniger geantwoord word as dit in Engels geskryf is.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Verskaf asseblief ten minste een skakel na \'n spel waar hulle gekroek het.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4335,8 +4599,8 @@ class AppLocalizationsAf extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count simuls', - one: '$count simul', + other: '$count simulasies', + one: '$count simulasie', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsAf extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess aanbieders'; + @override + String get studyPrivate => 'Privaat'; + + @override + String get studyMyStudies => 'My studies'; + + @override + String get studyStudiesIContributeTo => 'Studies waartoe ek bydra'; + + @override + String get studyMyPublicStudies => 'My publieke studies'; + + @override + String get studyMyPrivateStudies => 'My privaat studies'; + + @override + String get studyMyFavoriteStudies => 'My gunsteling studies'; + + @override + String get studyWhatAreStudies => 'Wat is studies?'; + + @override + String get studyAllStudies => 'Alle studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies gemaak deur $param'; + } + + @override + String get studyNoneYet => 'Nog geen.'; + + @override + String get studyHot => 'Gewild'; + + @override + String get studyDateAddedNewest => 'Datum bygevoeg (nuutste)'; + + @override + String get studyDateAddedOldest => 'Datum bygevoeg (oudste)'; + + @override + String get studyRecentlyUpdated => 'Onlangs opgedateer'; + + @override + String get studyMostPopular => 'Mees gewilde'; + + @override + String get studyAlphabetical => 'Alfabeties'; + + @override + String get studyAddNewChapter => 'Voeg \'n nuwe hoofstuk by'; + + @override + String get studyAddMembers => 'Voeg iemand by'; + + @override + String get studyInviteToTheStudy => 'Nooi uit om deel te wees van die studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Nooi asseblief net mense uit wat jy ken of wat aktief wil deelneem aan die studie.'; + + @override + String get studySearchByUsername => 'Soek vir gebruikersnaam'; + + @override + String get studySpectator => 'Toeskouer'; + + @override + String get studyContributor => 'Bydraer'; + + @override + String get studyKick => 'Verwyder'; + + @override + String get studyLeaveTheStudy => 'Verlaat die studie'; + + @override + String get studyYouAreNowAContributor => 'Jy is nou \'n bydraer'; + + @override + String get studyYouAreNowASpectator => 'Jy is nou \'n toeskouer'; + + @override + String get studyPgnTags => 'PGN etikette'; + + @override + String get studyLike => 'Hou van'; + + @override + String get studyUnlike => 'Afkeur'; + + @override + String get studyNewTag => 'Nuwe etiket'; + + @override + String get studyCommentThisPosition => 'Lewer kommentaar op hierdie posisie'; + + @override + String get studyCommentThisMove => 'Lewer kommentaar op hierdie skuif'; + + @override + String get studyAnnotateWithGlyphs => 'Annoteer met karakters'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Die hoofstuk is te kort om geanaliseer te word.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Slegs die studie bydraers kan versoek om \'n rekenaar analise te doen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Kry \'n vol-bediener rekenaar analise van die hooflyn.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Maak seker dat die hoofstuk volledig is. Jy kan slegs eenkeer \'n analise versoek.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC lede bly op dieselfde posisie'; + + @override + String get studyShareChanges => 'Deel veranderinge met toeskouers en stoor dit op die bediener'; + + @override + String get studyPlaying => 'Besig om te speel'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Eerste'; + + @override + String get studyPrevious => 'Vorige'; + + @override + String get studyNext => 'Volgende'; + + @override + String get studyLast => 'Laaste'; + @override String get studyShareAndExport => 'Deel & voer uit'; + @override + String get studyCloneStudy => 'Kloneer'; + + @override + String get studyStudyPgn => 'Studie PGN'; + + @override + String get studyDownloadAllGames => 'Laai alle speletjies af'; + + @override + String get studyChapterPgn => 'Hoofstuk PGN'; + + @override + String get studyCopyChapterPgn => 'Kopieer PGN'; + + @override + String get studyDownloadGame => 'Aflaai spel'; + + @override + String get studyStudyUrl => 'Bestudeer URL'; + + @override + String get studyCurrentChapterUrl => 'Huidige hoofstuk URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'U kan dit in die forum plak om in te bed'; + + @override + String get studyStartAtInitialPosition => 'Begin by die oorspronklike posisie'; + + @override + String studyStartAtX(String param) { + return 'Begin by $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Bed in u webwerf of blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Lees meer oor inbedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Slegs openbare studies kan ingebed word!'; + + @override + String get studyOpen => 'Maak oop'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, aan jou beskikbaar gestel deur $param2'; + } + + @override + String get studyStudyNotFound => 'Studie kon nie gevind word nie'; + + @override + String get studyEditChapter => 'Verander die hoofstuk'; + + @override + String get studyNewChapter => 'Nuwe hoofstuk'; + + @override + String studyImportFromChapterX(String param) { + return 'Voer in vanaf $param'; + } + + @override + String get studyOrientation => 'Oriëntasie'; + + @override + String get studyAnalysisMode => 'Analiseer mode'; + + @override + String get studyPinnedChapterComment => 'Vasgepende hoofstuk kommentaar'; + + @override + String get studySaveChapter => 'Stoor hoofstuk'; + + @override + String get studyClearAnnotations => 'Vee annotasies uit'; + + @override + String get studyClearVariations => 'Verwyder variasies'; + + @override + String get studyDeleteChapter => 'Vee hoofstuk uit'; + + @override + String get studyDeleteThisChapter => 'Vee die hoofstuk uit? Jy gaan dit nie kan terugvat nie!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vee al die kommentaar, karakters en getekende vorms in die hoofstuk uit?'; + + @override + String get studyRightUnderTheBoard => 'Reg onder die bord'; + + @override + String get studyNoPinnedComment => 'Geen'; + + @override + String get studyNormalAnalysis => 'Normale analise'; + + @override + String get studyHideNextMoves => 'Versteek die volgende skuiwe'; + + @override + String get studyInteractiveLesson => 'Interaktiewe les'; + + @override + String studyChapterX(String param) { + return 'Hoofstuk $param'; + } + + @override + String get studyEmpty => 'Leeg'; + + @override + String get studyStartFromInitialPosition => 'Begin vanaf oorspronklike posisie'; + + @override + String get studyEditor => 'Redakteur'; + + @override + String get studyStartFromCustomPosition => 'Begin vanaf eie posisie'; + + @override + String get studyLoadAGameByUrl => 'Laai \'n wedstryd op deur die URL'; + + @override + String get studyLoadAPositionFromFen => 'Laai posisie vanaf FEN'; + + @override + String get studyLoadAGameFromPgn => 'Laai wedstryd vanaf PGN'; + + @override + String get studyAutomatic => 'Outomaties'; + + @override + String get studyUrlOfTheGame => 'URL van die wedstryd'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Laai \'n wedstryd van $param1 of $param2'; + } + + @override + String get studyCreateChapter => 'Skep \'n hoofstuk'; + + @override + String get studyCreateStudy => 'Skep \'n studie'; + + @override + String get studyEditStudy => 'Verander studie'; + + @override + String get studyVisibility => 'Sigbaarheid'; + + @override + String get studyPublic => 'Publiek'; + + @override + String get studyUnlisted => 'Ongelys'; + + @override + String get studyInviteOnly => 'Slegs op uitnodiging'; + + @override + String get studyAllowCloning => 'Laat kloning toe'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Net ek'; + + @override + String get studyContributors => 'Bydraers'; + + @override + String get studyMembers => 'Lede'; + + @override + String get studyEveryone => 'Almal'; + + @override + String get studyEnableSync => 'Maak sync beskikbaar'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: hou almal op dieselfde posisie'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: laat mense toe om vrylik deur te gaan'; + + @override + String get studyPinnedStudyComment => 'Vasgepende studie opmerking'; + @override String get studyStart => 'Begin'; + + @override + String get studySave => 'Stoor'; + + @override + String get studyClearChat => 'Maak die gesprek skoon'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vee die gesprek uit? Onthou, jy kan dit nie terug kry nie!'; + + @override + String get studyDeleteStudy => 'Vee die studie uit'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Skrap die hele studie? Daar is geen terugkeer nie! Tik die naam van die studie om te bevesting: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Waar wil jy dit bestudeer?'; + + @override + String get studyGoodMove => 'Goeie skuif'; + + @override + String get studyMistake => 'Fout'; + + @override + String get studyBrilliantMove => 'Skitterende skuif'; + + @override + String get studyBlunder => 'Flater'; + + @override + String get studyInterestingMove => 'Interesante skuif'; + + @override + String get studyDubiousMove => 'Twyfelagte skuif'; + + @override + String get studyOnlyMove => 'Eenigste skuif'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Gelyke posisie'; + + @override + String get studyUnclearPosition => 'Onduidelike posise'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wit is effens beter'; + + @override + String get studyBlackIsSlightlyBetter => 'Swart is effens beter'; + + @override + String get studyWhiteIsBetter => 'Wit is beter'; + + @override + String get studyBlackIsBetter => 'Swart is beter'; + + @override + String get studyWhiteIsWinning => 'Wit is beter'; + + @override + String get studyBlackIsWinning => 'Swart is beter'; + + @override + String get studyNovelty => 'Nuwigheid'; + + @override + String get studyDevelopment => 'Ontwikkeling'; + + @override + String get studyInitiative => 'Inisiatief'; + + @override + String get studyAttack => 'Aanval'; + + @override + String get studyCounterplay => 'Teenstoot'; + + @override + String get studyTimeTrouble => 'Tydskommer'; + + @override + String get studyWithCompensation => 'Met vergoeding'; + + @override + String get studyWithTheIdea => 'Met die idee'; + + @override + String get studyNextChapter => 'Volgende hoofstuk'; + + @override + String get studyPrevChapter => 'Vorige hoofstuk'; + + @override + String get studyStudyActions => 'Studie aksie'; + + @override + String get studyTopics => 'Onderwerpe'; + + @override + String get studyMyTopics => 'My onderwerpe'; + + @override + String get studyPopularTopics => 'Gewilde onderwerpe'; + + @override + String get studyManageTopics => 'Bestuur onderwerpe'; + + @override + String get studyBack => 'Terug'; + + @override + String get studyPlayAgain => 'Speel weer'; + + @override + String get studyWhatWouldYouPlay => 'Wat sal jy in hierdie posisie speel?'; + + @override + String get studyYouCompletedThisLesson => 'Geluk! Jy het hierdie les voltooi.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Hoofstukke', + one: '$count Hoofstuk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Wedstryde', + one: '$count Wedstryd', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Lede', + one: '$count Lid', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Plak jou PGN teks hier, tot by $count spelle', + one: 'Plak jou PGN teks hier, tot by $count spel', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'sopas'; + + @override + String get timeagoRightNow => 'nou'; + + @override + String get timeagoCompleted => 'voltooi'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count sekondes', + one: 'in $count sekonde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minute', + one: 'in $count minuut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count ure', + one: 'in $count uur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count dae', + one: 'in $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weke', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count maande', + one: 'in $count maand', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count jare', + one: 'in $count jaar', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minute gelede', + one: '$count minuut gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ure gelede', + one: '$count uur gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dae gelede', + one: '$count dag gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weke gelede', + one: '$count week gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count maande gelede', + one: '$count maand gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jare gelede', + one: '$count jaar gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nog $count minute oor', + one: 'nog $count minuut oor', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nog $count ure oor', + one: 'nog $count uur oor', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 472b3a4125..63db2cd5a2 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); @override - String get mobileHomeTab => 'الرئيسية'; + String get mobileAllGames => 'جميع الألعاب'; @override - String get mobilePuzzlesTab => 'ألغاز'; + String get mobileAreYouSure => 'هل أنت واثق؟'; @override - String get mobileToolsTab => 'أدوات'; + String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; @override - String get mobileWatchTab => 'شاهد'; + String get mobileClearButton => 'مسح'; @override - String get mobileSettingsTab => 'الإعدادات'; + String get mobileCorrespondenceClearSavedMove => 'مسح النقل المحفوظ'; @override - String get mobileMustBeLoggedIn => 'لعرض هذه الصفحة، قم بتسجيل الدخول.'; + String get mobileCustomGameJoinAGame => 'الانضمام إلى لُعْبَة'; @override - String get mobileSystemColors => 'ألوان النظام'; + String get mobileFeedbackButton => 'الملاحظات'; @override - String get mobileFeedbackButton => 'الملاحظات'; + String mobileGreeting(String param) { + return 'مرحبا، $param'; + } @override - String get mobileOkButton => 'موافق'; + String get mobileGreetingWithoutName => 'مرحبا'; @override - String get mobileSettingsHapticFeedback => 'التعليقات اللمسية'; + String get mobileHideVariation => 'إخفاء سلسلة النقلات المرشحة'; @override - String get mobileSettingsImmersiveMode => 'وضع ملء الشاشة'; + String get mobileHomeTab => 'الرئيسية'; @override - String get mobileSettingsImmersiveModeSubtitle => 'إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.'; + String get mobileLiveStreamers => 'البث المباشر'; @override - String get mobileNotFollowingAnyUser => 'أنت لا تتبع أي مستخدم.'; + String get mobileMustBeLoggedIn => 'سجل الدخول لعرض هذه الصفحة.'; @override - String get mobileAllGames => 'جميع الألعاب'; + String get mobileNoSearchResults => 'لا توجد نتائج'; @override - String get mobileRecentSearches => 'عمليات البحث الأخيرة'; + String get mobileNotFollowingAnyUser => 'أنت لا تتبع أي مستخدم.'; @override - String get mobileClearButton => 'مسح'; + String get mobileOkButton => 'موافق'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsAr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'لا توجد نتائج'; + String get mobilePrefMagnifyDraggedPiece => 'تكبير القطعة المسحوبة'; @override - String get mobileAreYouSure => 'هل أنت متأكد؟'; + String get mobilePuzzleStormConfirmEndRun => 'هل تريد إنهاء هذا التشغيل؟'; @override - String get mobilePuzzleStreakAbortWarning => 'سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.'; + String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير المرشح'; @override String get mobilePuzzleStormNothingToShow => 'لا شيء لإظهاره. العب بعض الألغاز.'; @override - String get mobileSharePuzzle => 'شارك هذا اللغز'; + String get mobilePuzzleStormSubtitle => 'حل أكبر عدد ممكن من الألغاز في 3 دقائق.'; @override - String get mobileShareGameURL => 'شارك رابط المباراة'; + String get mobilePuzzleStreakAbortWarning => 'سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.'; @override - String get mobileShareGamePGN => 'شارك الPGN'; + String get mobilePuzzleThemesSubtitle => 'حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.'; @override - String get mobileSharePositionAsFEN => 'مشاركة الموضع كFEN'; + String get mobilePuzzlesTab => 'ألغاز'; @override - String get mobileShowVariations => 'أظهر سلسلة النقلات المرشحة'; + String get mobileRecentSearches => 'عمليات البحث الأخيرة'; @override - String get mobileHideVariation => 'إخفاء سلسلة النقلات المرشحة'; + String get mobileSettingsHapticFeedback => 'التعليقات اللمسية'; @override - String get mobileShowComments => 'عرض التعليقات'; + String get mobileSettingsImmersiveMode => 'وضع ملء الشاشة'; @override - String get mobilePuzzleStormConfirmEndRun => 'هل تريد إنهاء هذا التشغيل؟'; + String get mobileSettingsImmersiveModeSubtitle => 'إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير الفلاتر'; + String get mobileSettingsTab => 'الإعدادات'; @override - String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; + String get mobileShareGamePGN => 'شارك الPGN'; @override - String get mobileCancelDrawOffer => 'إلغاء عرض التعادل'; + String get mobileShareGameURL => 'شارك رابط المباراة'; @override - String get mobileWaitingForOpponentToJoin => 'في انتظار انضمام الطرف الآخر...'; + String get mobileSharePositionAsFEN => 'مشاركة الموضع كFEN'; @override - String get mobileBlindfoldMode => 'عصب العينين'; + String get mobileSharePuzzle => 'شارك هذا اللغز'; @override - String get mobileLiveStreamers => 'البثوث المباشرة'; + String get mobileShowComments => 'عرض التعليقات'; @override - String get mobileCustomGameJoinAGame => 'الانضمام إلى لعبة'; + String get mobileShowResult => 'إظهار النتيجة'; @override - String get mobileCorrespondenceClearSavedMove => 'مسح النقل المحفوظ'; + String get mobileShowVariations => 'أظهر سلسلة النقلات المرشحة'; @override String get mobileSomethingWentWrong => 'لقد حدث خطأ ما.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'ألوان النظام'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'أدوات'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'في انتظار انضمام الطرف الآخر...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'شاهد'; @override String get activityActivity => 'الأنشطة'; @@ -278,6 +275,17 @@ class AppLocalizationsAr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -416,9 +424,260 @@ class AppLocalizationsAr extends AppLocalizations { @override String get broadcastBroadcasts => 'البثوث'; + @override + String get broadcastMyBroadcasts => 'بثي'; + @override String get broadcastLiveBroadcasts => 'بث البطولة المباشرة'; + @override + String get broadcastBroadcastCalendar => 'تقويم البث'; + + @override + String get broadcastNewBroadcast => 'بث مباشر جديد'; + + @override + String get broadcastSubscribedBroadcasts => 'البث المُشترك به'; + + @override + String get broadcastAboutBroadcasts => 'حول البثوث'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'كيفية استخدام بث ليتشيس.'; + + @override + String get broadcastTheNewRoundHelp => 'ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق.'; + + @override + String get broadcastAddRound => 'إضافة جولة'; + + @override + String get broadcastOngoing => 'الجارية'; + + @override + String get broadcastUpcoming => 'القادمة'; + + @override + String get broadcastCompleted => 'المكتملة'; + + @override + String get broadcastCompletedHelp => 'يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر.'; + + @override + String get broadcastRoundName => 'اسم الجولة'; + + @override + String get broadcastRoundNumber => 'رقم الجولة (الشوط)'; + + @override + String get broadcastTournamentName => 'اسم البطولة'; + + @override + String get broadcastTournamentDescription => 'وصف موجز للبطولة'; + + @override + String get broadcastFullDescription => 'الوصف الكامل'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'الوصف الاختياري الطويل للبث. $param1 متوفر. يجب أن لا يتجاوز طول النص $param2 حرفاً.'; + } + + @override + String get broadcastSourceSingleUrl => 'رابط مصدر PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL الذي سيتحقق منه Lichess للحصول على تحديثات PGN. يجب أن يكون متاحًا للجميع على الإنترنت.'; + + @override + String get broadcastSourceGameIds => 'حتى 64 معرف لُعْبَة ليتشيس، مفصولة بمسافات.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'موعد البداية بتوقيت البطولة المحلي: $param'; + } + + @override + String get broadcastStartDateHelp => 'اختياري، إذا كنت تعرف متى يبدأ الحدث'; + + @override + String get broadcastCurrentGameUrl => 'رابط المباراة الحالية'; + + @override + String get broadcastDownloadAllRounds => 'تحميل جميع المباريات'; + + @override + String get broadcastResetRound => 'إعادة ضبط هذه الجولة'; + + @override + String get broadcastDeleteRound => 'حذف هذه الجولة'; + + @override + String get broadcastDefinitivelyDeleteRound => 'قم بحذف الجولة وألعابها نهائيا.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'احذف جميع ألعاب هذه الجولة. سوف يحتاج المصدر إلى أن يكون نشطا من أجل إعادة إنشائها.'; + + @override + String get broadcastEditRoundStudy => 'تعديل دراسة الجولة'; + + @override + String get broadcastDeleteTournament => 'حذف هذه المسابقة'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'قم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها.'; + + @override + String get broadcastShowScores => 'اظهر نقاط اللاعبين بناءً على نتائج اللعبة'; + + @override + String get broadcastReplacePlayerTags => 'اختياري: استبدل أسماء اللاعبين وتقييماتهم وألقابهم'; + + @override + String get broadcastFideFederations => 'الاتحاد الدولي للشطرنج'; + + @override + String get broadcastTop10Rating => 'تقييم أعلى 10'; + + @override + String get broadcastFidePlayers => 'لاعبين FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'لم يتم العثور على لاعب الاتحاد الدولي (FIDE)'; + + @override + String get broadcastFideProfile => 'مِلَفّ FIDE'; + + @override + String get broadcastFederation => 'إتحاد'; + + @override + String get broadcastAgeThisYear => 'العمر هذا العام'; + + @override + String get broadcastUnrated => 'غير مقيم'; + + @override + String get broadcastRecentTournaments => 'البطولات الأخيرة'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بثوث', + many: '$count بثوث', + few: '$count بثوث', + two: 'بثين', + one: '$count بث', + zero: '$count بث', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'التحديات: $param1'; @@ -677,6 +936,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get preferencesInGameOnly => 'في اللعبة فقط'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'مؤقت الشطرنج'; @@ -818,6 +1080,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'صوت التنبيه'; + @override + String get preferencesBlindfold => 'معصوب العينين'; + @override String get puzzlePuzzles => 'الألغاز'; @@ -1478,10 +1743,10 @@ class AppLocalizationsAr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'حركات الخصم محدودة و كل الحركات تؤدي إلى تفاقم الوضع نحو الأسوء.'; @override - String get puzzleThemeHealthyMix => 'خليط'; + String get puzzleThemeMix => 'خليط'; @override - String get puzzleThemeHealthyMixDescription => 'القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.'; + String get puzzleThemeMixDescription => 'القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.'; @override String get puzzleThemePlayerGames => 'مبارايات اللاعب'; @@ -1855,9 +2120,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get byCPL => 'بالاثارة'; - @override - String get openStudy => 'فتح دراسة'; - @override String get enable => 'تفعيل'; @@ -1885,9 +2147,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get removesTheDepthLimit => 'التحليل لأبعد عمق، وابقاء حاسوبك نشطًا'; - @override - String get engineManager => 'مدير المحركات'; - @override String get blunder => 'خطأ فادح'; @@ -2151,6 +2410,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get gamesPlayed => 'المباريات الملعوبة'; + @override + String get ok => 'OK'; + @override String get cancel => 'إلغاء'; @@ -2525,9 +2787,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get unblock => 'إلغاء الحظر'; - @override - String get followsYou => 'يتابعك'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 بدأ متابعة $param2'; @@ -2860,7 +3119,13 @@ class AppLocalizationsAr extends AppLocalizations { String get other => 'أخرى'; @override - String get reportDescriptionHelp => 'الصق رابط المباراة (المباريات) واشرح بالتفصيل المشكلة في تصرف هذا المستحدم. لا تقل فقط \"انهم يغشون\"، ولكن اشرح لنا سبب استنتاجك. سيكون الرد أسرع إن كتبت بالإنكليزية.'; + String get reportCheatBoostHelp => 'هذه رسالة عامية وليست مخصصة لبلاغات الغش. وهي تحاول تعليم اللاعب كيفية كتابة بلاغ مفيد لفريق لي-تشيس. و أيضا تطلب إثبات.\n\nتظهر على صفحة \"بلغ مستخدم\"\nhttps://lichess. org/report.'; + + @override + String get reportUsernameHelp => 'اشرح ما المسيء في اسم المستخدم هذا. لا تقل فقط \"إنه مسيء/غير مناسب\"، بل أخبرنا كيف توصلت إلى هذا الاستنتاج، خاصة إذا كانت الإهانة غير واضحة، أو ليست باللغة الإنجليزية، أو كانت باللغة العامية، أو كانت إشارة تاريخية/ثقافية.'; + + @override + String get reportProcessedFasterInEnglish => 'سيتم معالجة بلاغك بشكل أسرع إذا تمت كتابته باللغة الإنجليزية.'; @override String get error_provideOneCheatedGameLink => 'برجاء تقديم رابط واحد علي الأقل لمباراة حدث فيها غش.'; @@ -4165,6 +4430,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get nothingToSeeHere => 'لا يوجد شيء يمكن رؤيته هنا في الوقت الحالي.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4988,8 +5256,768 @@ class AppLocalizationsAr extends AppLocalizations { String get streamerLichessStreamers => 'بثوث ليشس'; @override - String get studyShareAndExport => 'مشاركة و تصدير'; + String get studyPrivate => 'خاص'; @override - String get studyStart => 'ابدأ'; + String get studyMyStudies => 'دراستي'; + + @override + String get studyStudiesIContributeTo => 'الدراسات المساهم بها'; + + @override + String get studyMyPublicStudies => 'دراسات العامة'; + + @override + String get studyMyPrivateStudies => 'دراساتي الخاصة'; + + @override + String get studyMyFavoriteStudies => 'دراساتي المفضلة'; + + @override + String get studyWhatAreStudies => 'ما هي الدراسات؟'; + + @override + String get studyAllStudies => 'كل الدراسات'; + + @override + String studyStudiesCreatedByX(String param) { + return 'الدراسات التي أنشئها $param'; + } + + @override + String get studyNoneYet => 'لا يوجد.'; + + @override + String get studyHot => 'ذات شعبية'; + + @override + String get studyDateAddedNewest => 'تاريخ الإضافة (الأحدث)'; + + @override + String get studyDateAddedOldest => 'تاريخ الإضافة (الأقدم)'; + + @override + String get studyRecentlyUpdated => 'تم تحديثه مؤخرا'; + + @override + String get studyMostPopular => 'الاكثر شعبية'; + + @override + String get studyAlphabetical => 'أبجدي'; + + @override + String get studyAddNewChapter => 'أضف فصلاً جديدا'; + + @override + String get studyAddMembers => 'إضافة أعضاء'; + + @override + String get studyInviteToTheStudy => 'دعوة الى دراسة'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'يرجى فقط إضافة اشخاص تعرفهم، ويريدون المشاركة في هذه الدراسة'; + + @override + String get studySearchByUsername => 'البحث بواسطة اسم المستخدم'; + + @override + String get studySpectator => 'مشاهد'; + + @override + String get studyContributor => 'مساهم'; + + @override + String get studyKick => 'طرد'; + + @override + String get studyLeaveTheStudy => 'مغادرة الدراسة'; + + @override + String get studyYouAreNowAContributor => 'انت الان اصبحت مساهم'; + + @override + String get studyYouAreNowASpectator => 'انت الان اصبحت مشاهد'; + + @override + String get studyPgnTags => 'وسم PGN'; + + @override + String get studyLike => 'إعجاب'; + + @override + String get studyUnlike => 'إلغاء الإعجاب'; + + @override + String get studyNewTag => 'علامة جديدة'; + + @override + String get studyCommentThisPosition => 'التعليق على هذا الوضع'; + + @override + String get studyCommentThisMove => 'التعليق على هذه النقلة'; + + @override + String get studyAnnotateWithGlyphs => 'التعليق مع الحروف الرسومية'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'الفصل جداً قصير لكي يتم تحليله'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'فقط المساهمون في هذا الدراسة يمكنهم طلب تحليل الحاسوب'; + + @override + String get studyGetAFullComputerAnalysis => 'احصل على تحليل حاسوب كامل للتفريع الرئيسي من قبل الخادم'; + + @override + String get studyMakeSureTheChapterIsComplete => 'كن متأكداً ان الفصل مكتمل، يمكنك طلب تحليل الحاسوب مره واحده فحسب'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'يظل جميع ألاعضاء الذين تمت مزامنة معلوماتهم في نفس الترتيب'; + + @override + String get studyShareChanges => 'شارك التغيبرات مع المشاهدين وإحفظهن الى الخادم'; + + @override + String get studyPlaying => 'يلعب الان'; + + @override + String get studyShowEvalBar => 'شرائط التقييم'; + + @override + String get studyFirst => 'الأولى'; + + @override + String get studyPrevious => 'السابق'; + + @override + String get studyNext => 'التالي'; + + @override + String get studyLast => 'الأخير'; + + @override + String get studyShareAndExport => 'مشاركة و تصدير'; + + @override + String get studyCloneStudy => 'استنساخ'; + + @override + String get studyStudyPgn => 'PGN الدراسة'; + + @override + String get studyDownloadAllGames => 'حمل جميع الألعاب'; + + @override + String get studyChapterPgn => 'PGN الفصل'; + + @override + String get studyCopyChapterPgn => 'نسخ PGN'; + + @override + String get studyDownloadGame => 'حمل لعبة'; + + @override + String get studyStudyUrl => 'رابط الدراسة'; + + @override + String get studyCurrentChapterUrl => 'رابط الفصل الحالي'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'يمكنك لصق هذا في المنتدى لتضمينه'; + + @override + String get studyStartAtInitialPosition => 'البدء من وضع البداية'; + + @override + String studyStartAtX(String param) { + return 'البدء من $param'; + } + + @override + String get studyEmbedInYourWebsite => 'ضمنه في موقع أو مدونة'; + + @override + String get studyReadMoreAboutEmbedding => 'راجع المزيد عن التضمين'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'يمكن تضمين الدراسات العامة فقط!'; + + @override + String get studyOpen => 'فتح'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 مقدمة من $param2'; + } + + @override + String get studyStudyNotFound => 'لم يتم العثور على الدراسة'; + + @override + String get studyEditChapter => 'تحرير الفصل'; + + @override + String get studyNewChapter => 'فصل جديد'; + + @override + String studyImportFromChapterX(String param) { + return 'استيراد من $param'; + } + + @override + String get studyOrientation => 'اتجاه الرقعة'; + + @override + String get studyAnalysisMode => 'وضع التحليل'; + + @override + String get studyPinnedChapterComment => 'التعليق المثبت على الفصل'; + + @override + String get studySaveChapter => 'حفظ الفصل'; + + @override + String get studyClearAnnotations => 'مسح العلامات'; + + @override + String get studyClearVariations => 'مسح اللاينات'; + + @override + String get studyDeleteChapter => 'حذف الفصل'; + + @override + String get studyDeleteThisChapter => 'هل تريد حذف الفصل ؟ لايمكنك التراجع عن ذلك لاحقاً!'; + + @override + String get studyClearAllCommentsInThisChapter => 'مسح جميع التعليقات والغلافات والأشكال المرسومة في هذا الفصل؟'; + + @override + String get studyRightUnderTheBoard => 'تحت الرقعة مباشرة'; + + @override + String get studyNoPinnedComment => 'بدون'; + + @override + String get studyNormalAnalysis => 'تحليل عادي'; + + @override + String get studyHideNextMoves => 'أخفي النقلة التالية'; + + @override + String get studyInteractiveLesson => 'درس تفاعلي'; + + @override + String studyChapterX(String param) { + return 'الفصل $param'; + } + + @override + String get studyEmpty => 'فارغ'; + + @override + String get studyStartFromInitialPosition => 'البدء من وضعية البداية'; + + @override + String get studyEditor => 'المحرر'; + + @override + String get studyStartFromCustomPosition => 'البدء من وضع مخصص'; + + @override + String get studyLoadAGameByUrl => 'تحميل لعبة من رابط'; + + @override + String get studyLoadAPositionFromFen => 'تحميل موقف من FEN'; + + @override + String get studyLoadAGameFromPgn => 'استرد لعبة من PGN'; + + @override + String get studyAutomatic => 'تلقائي'; + + @override + String get studyUrlOfTheGame => 'رابط اللعبة'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'استيراد لعبة من $param1 او $param2'; + } + + @override + String get studyCreateChapter => 'أنشئ الفصل'; + + @override + String get studyCreateStudy => 'أنشى الدراسة'; + + @override + String get studyEditStudy => 'حرر الدراسة'; + + @override + String get studyVisibility => 'الظهور'; + + @override + String get studyPublic => 'عامة'; + + @override + String get studyUnlisted => 'غير مدرجة'; + + @override + String get studyInviteOnly => 'دعوة فقط'; + + @override + String get studyAllowCloning => 'السماح بالاستنساخ'; + + @override + String get studyNobody => 'لا أحد'; + + @override + String get studyOnlyMe => 'أنا فقط'; + + @override + String get studyContributors => 'المساهمون'; + + @override + String get studyMembers => 'اعضاء'; + + @override + String get studyEveryone => 'الجميع'; + + @override + String get studyEnableSync => 'مكن المزامنة'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'نعم: إبقاء الجميع في نفس الوضعية'; + + @override + String get studyNoLetPeopleBrowseFreely => 'لا: دع الناس يتصفحون بحرية'; + + @override + String get studyPinnedStudyComment => 'تعليق الدراسة المثبتة'; + + @override + String get studyStart => 'ابدأ'; + + @override + String get studySave => 'حفظ'; + + @override + String get studyClearChat => 'مسح المحادثة'; + + @override + String get studyDeleteTheStudyChatHistory => 'هل تريد حذف سجل الدردشة الدراسية؟ لا يمكن إرجاعها!'; + + @override + String get studyDeleteStudy => 'حذف الدراسة'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'حذف الدراسة بأكملها؟ لا يمكنك التراجع عن هذه الخطوة! اكتب اسم الدراسة لتأكيد عملية الحذف: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'أين تريد دراسة ذلك؟'; + + @override + String get studyGoodMove => 'نقلة جيدة'; + + @override + String get studyMistake => 'خطأ'; + + @override + String get studyBrilliantMove => 'نقلة رائعة'; + + @override + String get studyBlunder => 'غلطة'; + + @override + String get studyInterestingMove => 'نقلة مثيرة للاهتمام'; + + @override + String get studyDubiousMove => 'نقلة مشبوهة'; + + @override + String get studyOnlyMove => 'نقلة وحيدة'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'وضع متساوي'; + + @override + String get studyUnclearPosition => 'وضعية غير واضح'; + + @override + String get studyWhiteIsSlightlyBetter => 'الأبيض أفضل بقليل'; + + @override + String get studyBlackIsSlightlyBetter => 'الأسود أفضل بقليل'; + + @override + String get studyWhiteIsBetter => 'الأبيض أفضل'; + + @override + String get studyBlackIsBetter => 'الأسود أفضل'; + + @override + String get studyWhiteIsWinning => 'الأبيض يفوز'; + + @override + String get studyBlackIsWinning => 'الأسود يفوز'; + + @override + String get studyNovelty => 'جديد'; + + @override + String get studyDevelopment => 'تطوير'; + + @override + String get studyInitiative => 'مبادرة'; + + @override + String get studyAttack => 'هجوم'; + + @override + String get studyCounterplay => 'هجوم مضاد'; + + @override + String get studyTimeTrouble => 'مشكلة وقت'; + + @override + String get studyWithCompensation => 'مع تعويض'; + + @override + String get studyWithTheIdea => 'مع فكرة'; + + @override + String get studyNextChapter => 'الفصل التالي'; + + @override + String get studyPrevChapter => 'الفصل السابق'; + + @override + String get studyStudyActions => 'خيارات الدراسة'; + + @override + String get studyTopics => 'المواضيع'; + + @override + String get studyMyTopics => 'المواضيع الخاصة بي'; + + @override + String get studyPopularTopics => 'المواضيع الشائعة'; + + @override + String get studyManageTopics => 'إدارة المواضيع'; + + @override + String get studyBack => 'رجوع'; + + @override + String get studyPlayAgain => 'اللعب مجددا'; + + @override + String get studyWhatWouldYouPlay => 'ماذا ستلعب في هذا الموقف؟'; + + @override + String get studyYouCompletedThisLesson => 'تهانينا! لقد أكملت هذا الدرس.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count فصول', + many: '$count فصل', + few: '$count فصول', + two: 'فصلان', + one: '$count فصل', + zero: '$count فصل', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count مباراة', + many: '$count مباراة', + few: '$count مبارايات', + two: 'مبارتان', + one: '$count مباراة', + zero: '$count مباراة', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count أعضاء', + many: '$count عضو', + few: '$count عضو', + two: '$count عضو', + one: '$count عضو', + zero: '$count عضو', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'الصق الPGN هنا، حتى $count العاب', + many: 'ألصق نص PGN هنا، حتى $count مباراة', + few: 'ألصق نص PGN هنا، حتى $count مباراة', + two: 'ألصق نص PGN هنا، حتى $count مباراة', + one: 'الصق نص الPGN هنا، حتى $count لعبة واحدة', + zero: 'ألصق نص PGN هنا، حتى $count مباراة', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'الان'; + + @override + String get timeagoRightNow => 'حاليا'; + + @override + String get timeagoCompleted => 'مكتمل'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count ثانية', + many: 'خلال $count ثانية', + few: 'خلال $count ثوانٍ', + two: 'خلال ثانيتين', + one: 'خلال ثانية', + zero: 'خلال $count ثانية', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count دقائق', + many: 'خلال $count دقائق', + few: 'خلال $count دقائق', + two: 'خلال $count دقيقتين', + one: 'خلال دقيقة', + zero: 'خلال $count دقيقة', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count ساعة', + many: 'خلال $count ساعة', + few: 'خلال $count ساعات', + two: 'خلال ساعتين', + one: 'خلال ساعة', + zero: 'خلال $count ساعة', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count يوم', + many: 'خلال $count يوم', + few: 'خلال $count أيام', + two: 'خلال $count يوم', + one: 'خلال $count يوم', + zero: 'خلال $count يوم', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count اسبوع', + many: 'خلال $count اسبوع', + few: 'خلال $count اسبوع', + two: 'خلال $count اسبوع', + one: 'خلال $count اسبوع', + zero: 'خلال $count أسبوع', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count شهر', + many: 'خلال $count شهر', + few: 'خلال $count شهر', + two: 'خلال $count شهر', + one: 'خلال $count شهر', + zero: 'خلال $count شهر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count سنة', + many: 'خلال $count سنة', + few: 'خلال $count سنة', + two: 'خلال $count سنة', + one: 'خلال $count سنة', + zero: 'خلال $count سنة', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count دقيقة/دقائق مضت', + many: 'منذ $count دقيقة/دقائق مضت', + few: 'منذ $count دقيقة/دقائق مضت', + two: 'منذ $count دقيقتين', + one: 'منذ $count دقيقة', + zero: 'منذ $count دقيقة/دقائق مضت', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count ساعة/ساعات مضت', + many: 'منذ $count ساعة/ساعات مضت', + few: 'منذ $count ساعة/ساعات مضت', + two: 'منذ $count ساعة/ساعات مضت', + one: 'منذ $count ساعة', + zero: 'منذ $count ساعة/ساعات مضت', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count يوم/أيام', + many: 'منذ $count يوم/أيام', + few: 'منذ $count يوم/أيام', + two: 'منذ $count يوم/أيام', + one: 'منذ $count يوم', + zero: 'منذ $count يوم/أيام', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count أسابيع', + many: 'منذ $count أسابيع', + few: 'منذ $count أسابيع', + two: 'منذ $count أسابيع', + one: 'منذ $count أسابيع', + zero: 'منذ $count أسابيع', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count شهر', + many: 'منذ $count شهر', + few: 'منذ $count شهر', + two: 'منذ $count شهر', + one: 'منذ $count شهر', + zero: 'منذ $count شهر', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count سنة', + many: 'منذ $count سنة', + few: 'منذ $count سنة', + two: 'منذ $count سنة', + one: 'منذ $count سنة', + zero: 'منذ $count سنة', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countدقائق متبقية', + many: '$countدقيقة متبقية', + few: '$countدقائق متبقية', + two: '$countدقيقتان متبقيتان', + one: '$countدقيقة متبقية', + zero: '$countدقيقة متبقية', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countساعة متبقية', + many: '$countساعة متبقية', + few: '$count ساعات متبقية', + two: '$countساعتان متبقيتان', + one: '$countساعة واحدة متبقية', + zero: '$countساعة متبقية', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index 8ab787334a..60b6f82133 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsAz extends AppLocalizations { AppLocalizationsAz([String locale = 'az']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsAz extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivlik'; @@ -246,6 +243,17 @@ class AppLocalizationsAz extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsAz extends AppLocalizations { @override String get broadcastBroadcasts => 'Yayım'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Canlı turnir yayımları'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Yeni canlı yayım'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Tur əlavə et'; + + @override + String get broadcastOngoing => 'Davam edən'; + + @override + String get broadcastUpcoming => 'Yaxınlaşan'; + + @override + String get broadcastCompleted => 'Tamamlanan'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Tur adı'; + + @override + String get broadcastRoundNumber => 'Tur sayı'; + + @override + String get broadcastTournamentName => 'Turnir adı'; + + @override + String get broadcastTournamentDescription => 'Qısa turnir açıqlaması'; + + @override + String get broadcastFullDescription => 'Tədbirin tam açıqlaması'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Tədbirin istəyə bağlı təfsilatlı açıqlaması. $param1 seçimi mövcuddur. Mətnin uzunluğu $param2 simvoldan az olmalıdır.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess, verdiyiniz URL ilə PGN-i yeniləyəcək. Bu internetdə hamı tərəfindən əldə edilə bilən olmalıdır.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'İstəyə bağlı, tədbirin başlama vaxtını bilirsinizsə'; + + @override + String get broadcastCurrentGameUrl => 'Hazırkı oyun URL-i'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Bu turu sıfırla'; + + @override + String get broadcastDeleteRound => 'Bu turu sil'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Şahmat saatı'; @@ -750,6 +1008,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Tapmacalar'; @@ -1388,10 +1649,10 @@ class AppLocalizationsAz extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Rəqibin edə biləcəyi gediş sayı məhduddur və istənilən gediş vəziyyəti daha da pisləşdirir.'; @override - String get puzzleThemeHealthyMix => 'Həftəbecər'; + String get puzzleThemeMix => 'Həftəbecər'; @override - String get puzzleThemeHealthyMixDescription => 'Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.'; + String get puzzleThemeMixDescription => 'Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1765,9 +2026,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get byCPL => 'CPL üzrə'; - @override - String get openStudy => 'Çalışmanı aç'; - @override String get enable => 'Aktiv et'; @@ -1795,9 +2053,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get removesTheDepthLimit => 'Dərinlik limitini ləğv edir və kompüterinizi isti saxlayır'; - @override - String get engineManager => 'Mühərrik meneceri'; - @override String get blunder => 'Kobud Səhv'; @@ -2061,6 +2316,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get gamesPlayed => 'Oynadığı oyunlar'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ləğv et'; @@ -2435,9 +2693,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get unblock => 'Blokdan çıxart'; - @override - String get followsYou => 'Səni izləyir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 adlı oyunçunu izləməyə başladı'; @@ -2770,7 +3025,13 @@ class AppLocalizationsAz extends AppLocalizations { String get other => 'Digər'; @override - String get reportDescriptionHelp => 'Oyunun və ya oyunların linkini yapışdırın və bu istifadəçinin davranışında nəyin səhv olduğunu izah edin. Yalnız \"hiylə edirlər\" deməyin, necə bu nəticəyə gəldiyinizi bizə deyin. İngilis dilində yazıldığı təqdirdə hesabat daha sürətli işlənəcəkdir.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Lütfən ən azı bir hiyləli oyun linki daxil edin.'; @@ -4075,6 +4336,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4721,9 +4985,693 @@ class AppLocalizationsAz extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess yayımçıları'; + @override + String get studyPrivate => 'Özəl'; + + @override + String get studyMyStudies => 'Çalışmalarım'; + + @override + String get studyStudiesIContributeTo => 'Töhfə verdiyim çalışmalar'; + + @override + String get studyMyPublicStudies => 'Hərkəsə açıq çalışmalarım'; + + @override + String get studyMyPrivateStudies => 'Özəl çalışmalarım'; + + @override + String get studyMyFavoriteStudies => 'Sevimli çalışmalarım'; + + @override + String get studyWhatAreStudies => 'Çalışmalar nədir?'; + + @override + String get studyAllStudies => 'Bütün çalışmalar'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param tərəfindən yaradılan çalışmalar'; + } + + @override + String get studyNoneYet => 'Hələ ki, yoxdur.'; + + @override + String get studyHot => 'Məşhur'; + + @override + String get studyDateAddedNewest => 'Əlavə edilmə tarixi (yenidən köhnəyə)'; + + @override + String get studyDateAddedOldest => 'Əlavə edilmə tarixi (köhnədən yeniyə)'; + + @override + String get studyRecentlyUpdated => 'Ən son yenilənən'; + + @override + String get studyMostPopular => 'Ən məşhur'; + + @override + String get studyAlphabetical => 'Əlifbaya görə'; + + @override + String get studyAddNewChapter => 'Yeni bir fəsil əlavə et'; + + @override + String get studyAddMembers => 'Üzv əlavə et'; + + @override + String get studyInviteToTheStudy => 'Çalışmaya dəvət et'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Zəhmət olmasa yalnız tanıdığınız və bu çalışmaya aktiv olaraq qoşulmaq istəyən insanları dəvət edin.'; + + @override + String get studySearchByUsername => 'İstifadəçi adına görə axtar'; + + @override + String get studySpectator => 'Tamaşaçı'; + + @override + String get studyContributor => 'Töhfə verən'; + + @override + String get studyKick => 'Qov'; + + @override + String get studyLeaveTheStudy => 'Çalışmanı tərk et'; + + @override + String get studyYouAreNowAContributor => 'İndi iştirakçısınız'; + + @override + String get studyYouAreNowASpectator => 'İndi tamaşaçısınız'; + + @override + String get studyPgnTags => 'PGN etiketləri'; + + @override + String get studyLike => 'Bəyən'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Yeni etiket'; + + @override + String get studyCommentThisPosition => 'Bu pozisiyaya rəy bildirin'; + + @override + String get studyCommentThisMove => 'Bu gedişə rəy bildirin'; + + @override + String get studyAnnotateWithGlyphs => 'Simvol ilə izah et'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Fəsil təhlil edilməsi üçün çox qısadır.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Yalnız çalışma iştirakçıları kompüter təhlili tələb edə bilər.'; + + @override + String get studyGetAFullComputerAnalysis => 'Ana variant üçün serverdən hərtərəfli kompüter təhlilini alın.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Fəslin tamamlandığına əmin olun. Yalnız bir dəfə təhlil tələbi edə bilərsiniz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'EYNİLƏŞDİRİLMİŞ bütün üzvlər eyni pozisiyada qalır'; + + @override + String get studyShareChanges => 'Dəyişiklikləri tamaşaçılarla paylaşın və onları serverdə saxlayın'; + + @override + String get studyPlaying => 'Oynanılan'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'İlk'; + + @override + String get studyPrevious => 'Əvvəlki'; + + @override + String get studyNext => 'Növbəti'; + + @override + String get studyLast => 'Son'; + @override String get studyShareAndExport => 'Paylaş və yüklə'; + @override + String get studyCloneStudy => 'Klonla'; + + @override + String get studyStudyPgn => 'Çalışma PGN-i'; + + @override + String get studyDownloadAllGames => 'Bütün oyunları endir'; + + @override + String get studyChapterPgn => 'Fəsil PGN-i'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Oyunu endir'; + + @override + String get studyStudyUrl => 'Çalışma URL-i'; + + @override + String get studyCurrentChapterUrl => 'Cari fəsil URL-ii'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Pərçimləmək üçün bunu forumda paylaşa bilərsiniz'; + + @override + String get studyStartAtInitialPosition => 'Başlanğıc pozisiyada başlasın'; + + @override + String studyStartAtX(String param) { + return 'buradan başla: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Veb sayt və ya bloqunuzda pərçimləyin'; + + @override + String get studyReadMoreAboutEmbedding => 'Pərçimləmə haqqında daha ətraflı'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Yalnız hərkəsə açıq çalışmalar pərçimlənə bilər!'; + + @override + String get studyOpen => 'Aç'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 sizə $param1 tərəfindən gətirildi'; + } + + @override + String get studyStudyNotFound => 'Çalışma tapılmadı'; + + @override + String get studyEditChapter => 'Fəslə düzəliş et'; + + @override + String get studyNewChapter => 'Yeni fəsil'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'İstiqamət'; + + @override + String get studyAnalysisMode => 'Təhlil rejimi'; + + @override + String get studyPinnedChapterComment => 'Sancaqlanmış fəsil rəyləri'; + + @override + String get studySaveChapter => 'Fəsli yadda saxla'; + + @override + String get studyClearAnnotations => 'İzahları təmizlə'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Fəsli sil'; + + @override + String get studyDeleteThisChapter => 'Bu fəsil silinsin? Bunun geri dönüşü yoxdur!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Bu fəsildəki bütün rəylər, simvollar və çəkilmiş formalar təmizlənsin?'; + + @override + String get studyRightUnderTheBoard => 'Lövhənin altında'; + + @override + String get studyNoPinnedComment => 'Görünməsin'; + + @override + String get studyNormalAnalysis => 'Normal təhlil'; + + @override + String get studyHideNextMoves => 'Növbəti gedişləri gizlət'; + + @override + String get studyInteractiveLesson => 'İnteraktiv dərs'; + + @override + String studyChapterX(String param) { + return '$param. Fəsil'; + } + + @override + String get studyEmpty => 'Boş'; + + @override + String get studyStartFromInitialPosition => 'Başlanğıc pozisiyadan başlasın'; + + @override + String get studyEditor => 'Redaktor'; + + @override + String get studyStartFromCustomPosition => 'Özəl pozisiyadan başlasın'; + + @override + String get studyLoadAGameByUrl => 'URL ilə oyun yüklə'; + + @override + String get studyLoadAPositionFromFen => 'FEN ilə pozisiya yüklə'; + + @override + String get studyLoadAGameFromPgn => 'PGN ilə oyun yüklə'; + + @override + String get studyAutomatic => 'Avtomatik'; + + @override + String get studyUrlOfTheGame => 'Oyun URL-i'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 və ya $param2 ilə oyun yükləyin'; + } + + @override + String get studyCreateChapter => 'Fəsil yarat'; + + @override + String get studyCreateStudy => 'Çalışma yarat'; + + @override + String get studyEditStudy => 'Çalışmaya düzəliş et'; + + @override + String get studyVisibility => 'Görünmə'; + + @override + String get studyPublic => 'Hərkəsə açıq'; + + @override + String get studyUnlisted => 'Siyahıya alınmamış'; + + @override + String get studyInviteOnly => 'Yalnız dəvətlə'; + + @override + String get studyAllowCloning => 'Klonlamağa icazə ver'; + + @override + String get studyNobody => 'Heç kim'; + + @override + String get studyOnlyMe => 'Yalnız mən'; + + @override + String get studyContributors => 'Töhfə verənlər'; + + @override + String get studyMembers => 'Üzvlər'; + + @override + String get studyEveryone => 'Hamı'; + + @override + String get studyEnableSync => 'Eyniləşdirməni aktivləşdir'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Bəli: hər kəsi eyni pozisiyada saxla'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Xeyr: sərbəst gəzməyə icazə ver'; + + @override + String get studyPinnedStudyComment => 'Sancaqlanmış çalışma rəyləri'; + @override String get studyStart => 'Başlat'; + + @override + String get studySave => 'Saxla'; + + @override + String get studyClearChat => 'Söhbəti təmizlə'; + + @override + String get studyDeleteTheStudyChatHistory => 'Çalışmanın söhbət tarixçəsi silinsin? Bunun geri dönüşü yoxdur!'; + + @override + String get studyDeleteStudy => 'Çalışmanı sil'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Harada çalışmaq istəyirsən?'; + + @override + String get studyGoodMove => 'Yaxşı gediş'; + + @override + String get studyMistake => 'Səhv'; + + @override + String get studyBrilliantMove => 'Brilyant gediş'; + + @override + String get studyBlunder => 'Kobud səhv'; + + @override + String get studyInterestingMove => 'Maraqlı gediş'; + + @override + String get studyDubiousMove => 'Şübhəli gediş'; + + @override + String get studyOnlyMove => 'Tək gediş'; + + @override + String get studyZugzwang => 'Suqsvanq'; + + @override + String get studyEqualPosition => 'Bərabər mövqe'; + + @override + String get studyUnclearPosition => 'Qeyri-müəyyən mövqe'; + + @override + String get studyWhiteIsSlightlyBetter => 'Ağlar biraz öndədir'; + + @override + String get studyBlackIsSlightlyBetter => 'Qaralar biraz öndədir'; + + @override + String get studyWhiteIsBetter => 'Ağlar üstündür'; + + @override + String get studyBlackIsBetter => 'Qaralar üstündür'; + + @override + String get studyWhiteIsWinning => 'Ağlar qalib gəlir'; + + @override + String get studyBlackIsWinning => 'Qaralar qalib gəlir'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Fəsil', + one: '$count Fəsil', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Oyun', + one: '$count Oyun', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Üzv', + one: '$count Üzv', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN mətninizi bura yapışdırın, ən çox $count oyuna qədər', + one: 'PGN mətninizi bura yapışdırın, ən çox $count oyuna qədər', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'elə indi'; + + @override + String get timeagoRightNow => 'indicə'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 4881fc737d..81181c25f2 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsBe extends AppLocalizations { AppLocalizationsBe([String locale = 'be']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Вы ўпэўнены?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Ачысціць'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Галоўная'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'Няма вынікаў'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'Добра'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Гульцы з «$param»'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Задачы'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Нядаўнія пошукі'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Поўнаэкранны рэжым'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Налады'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Актыўнасць'; @@ -262,6 +259,17 @@ class AppLocalizationsBe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +390,256 @@ class AppLocalizationsBe extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляцыі'; + @override + String get broadcastMyBroadcasts => 'Мае трансляцыі'; + @override String get broadcastLiveBroadcasts => 'Прамыя трансляцыі турніраў'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Новая прамая трансляцыя'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Пра трансляцыіі'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Як карыстацца трансляцыямі Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Дадаць тур'; + + @override + String get broadcastOngoing => 'Бягучыя'; + + @override + String get broadcastUpcoming => 'Надыходзячыя'; + + @override + String get broadcastCompleted => 'Завершаныя'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Назва туру'; + + @override + String get broadcastRoundNumber => 'Нумар туру'; + + @override + String get broadcastTournamentName => 'Назва турніру'; + + @override + String get broadcastTournamentDescription => 'Сціслае апісанне турніру'; + + @override + String get broadcastFullDescription => 'Поўнае апісанне турніру'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Неабавязковая дасканалае апісанне турніру. Даступны $param1. Даўжыня павінна быць менш за $param2 сімвалаў.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Спасылка, з якой Lichess паспрабуе атрымоўваць абнаўленні PGN. Яны павінна быць даступнай для кожнай ва Інтэрнэце.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Па жаданні, калі вы ведаеце пачатак падзеі'; + + @override + String get broadcastCurrentGameUrl => 'Спасылка на бягучую гульню'; + + @override + String get broadcastDownloadAllRounds => 'Спампаваць усе туры'; + + @override + String get broadcastResetRound => 'Скасаваць гэты тур'; + + @override + String get broadcastDeleteRound => 'Выдаліць гэты тур'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Канчаткова выдаліць ​​тур і ўсе яго гульні.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай.'; + + @override + String get broadcastEditRoundStudy => 'Рэдагаваць навучанне туру'; + + @override + String get broadcastDeleteTournament => 'Выдаліць гэты турнір'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'Гульцы FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'Профіль FIDE'; + + @override + String get broadcastFederation => 'Федэрацыя'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Выклікаў: $param1'; @@ -643,6 +898,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get preferencesInGameOnly => 'Выключна ў партыі'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматны гадзіннік'; @@ -784,6 +1042,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Гукавое паведамленне'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Задачы'; @@ -874,7 +1135,7 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzleByOpenings => 'By openings'; @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String get puzzlePuzzlesByOpenings => 'Задачы за дэбютамі'; @override String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; @@ -1434,10 +1695,10 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Супернік абмежаваны ў хадах і ўсе магчымыя хады пагаршаюць яго пазіцыю.'; @override - String get puzzleThemeHealthyMix => 'Здаровая сумесь'; + String get puzzleThemeMix => 'Здаровая сумесь'; @override - String get puzzleThemeHealthyMixDescription => 'Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.'; + String get puzzleThemeMixDescription => 'Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.'; @override String get puzzleThemePlayerGames => 'З партый гульца'; @@ -1647,7 +1908,7 @@ class AppLocalizationsBe extends AppLocalizations { String get usingServerAnalysis => 'Выкарыстоўваецца серверны аналіз'; @override - String get loadingEngine => 'Загружаем шахматную праграму...'; + String get loadingEngine => 'Загружаем рухавічок...'; @override String get calculatingMoves => 'Пралічваем хады...'; @@ -1680,7 +1941,7 @@ class AppLocalizationsBe extends AppLocalizations { String get deleteFromHere => 'Выдаліць з гэтага месца'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Ачысціць варыянты'; @override String get expandVariations => 'Expand variations'; @@ -1811,9 +2072,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get byCPL => 'Цікавае'; - @override - String get openStudy => 'Адкрыць навучанне'; - @override String get enable => 'Уключыць'; @@ -1841,9 +2099,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get removesTheDepthLimit => 'Здымае абмежаванне на глыбіню. Асцярожна, ваш камп’ютар можа перагрэцца!'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Грубая памылка'; @@ -2107,6 +2362,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get gamesPlayed => 'Партый згуляна'; + @override + String get ok => 'OK'; + @override String get cancel => 'Скасаваць'; @@ -2481,9 +2739,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get unblock => 'Разблакіраваць'; - @override - String get followsYou => 'Падпісаны на вас'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 падпісаўся на $param2'; @@ -2675,7 +2930,7 @@ class AppLocalizationsBe extends AppLocalizations { String get editProfile => 'Рэдагаваць профіль'; @override - String get realName => 'Real name'; + String get realName => 'Сапраўднае імя'; @override String get setFlair => 'Set your flair'; @@ -2735,7 +2990,7 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzles => 'Задачы'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Анлайн боты'; @override String get name => 'Назва'; @@ -2756,7 +3011,7 @@ class AppLocalizationsBe extends AppLocalizations { String get yes => 'Так'; @override - String get website => 'Website'; + String get website => 'Вэб-сайт'; @override String get mobile => 'Mobile'; @@ -2816,7 +3071,13 @@ class AppLocalizationsBe extends AppLocalizations { String get other => 'Іншае'; @override - String get reportDescriptionHelp => 'Пакіньце ніжэй спасылку на гульню (ці гульні) і патлумачце, што вас непакоіць у паводзінах гэтага карыстальніка. Не пішыце нешта кшталту «ён чмут!» – патлумачце, як вы прыйшлі да гэтага выніку. Мы хутчэй разбярэмся ў сітуацыі, калі вы напішаце нам па-англійску.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Калі ласка, дадайце спасылку хаця б на адну гульню, дзе былі парушаны правілы.'; @@ -2919,7 +3180,7 @@ class AppLocalizationsBe extends AppLocalizations { String get outsideTheBoard => 'Па-за дошкай'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Усе клеткі на дошцы'; @override String get onSlowGames => 'У павольных гульнях'; @@ -3515,19 +3776,19 @@ class AppLocalizationsBe extends AppLocalizations { String get backgroundImageUrl => 'Спасылка на фон:'; @override - String get board => 'Board'; + String get board => 'Дошка'; @override - String get size => 'Size'; + String get size => 'Размер'; @override - String get opacity => 'Opacity'; + String get opacity => 'Празрыстасць'; @override - String get brightness => 'Brightness'; + String get brightness => 'Яркасць'; @override - String get hue => 'Hue'; + String get hue => 'Адценне'; @override String get boardReset => 'Reset colours to default'; @@ -4121,6 +4382,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4851,9 +5115,731 @@ class AppLocalizationsBe extends AppLocalizations { @override String get streamerLichessStreamers => 'Стрымеры на Lichess'; + @override + String get studyPrivate => 'Прыватны'; + + @override + String get studyMyStudies => 'Мае навучанні'; + + @override + String get studyStudiesIContributeTo => 'Навучанні, якія я рэдагую'; + + @override + String get studyMyPublicStudies => 'Мае публічныя навучанні'; + + @override + String get studyMyPrivateStudies => 'Мае прыватные навучанні'; + + @override + String get studyMyFavoriteStudies => 'Мае ўлюбленые навучанні'; + + @override + String get studyWhatAreStudies => 'Што такое навучанні?'; + + @override + String get studyAllStudies => 'Усе навучанні'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Навучанні, створаныя $param'; + } + + @override + String get studyNoneYet => 'Пакуль нічога няма.'; + + @override + String get studyHot => 'Гарачыя'; + + @override + String get studyDateAddedNewest => 'Дата дадання (навейшыя)'; + + @override + String get studyDateAddedOldest => 'Дата дадання (старэйшыя)'; + + @override + String get studyRecentlyUpdated => 'Нядаўна абноўленыя'; + + @override + String get studyMostPopular => 'Найбольш папулярныя'; + + @override + String get studyAlphabetical => 'Па алфавіце'; + + @override + String get studyAddNewChapter => 'Дадаць новы раздзел'; + + @override + String get studyAddMembers => 'Дадаць удзельнікаў'; + + @override + String get studyInviteToTheStudy => 'Закліцца да навучання'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Калі ласка, заклікайце толькі людзей, якіх вы ведаеце, та тых хто актыўна хоча далучыцца да навучання.'; + + @override + String get studySearchByUsername => 'Шукаць па імені карыстальніка'; + + @override + String get studySpectator => 'Глядач'; + + @override + String get studyContributor => 'Рэдактар'; + + @override + String get studyKick => 'Выдаліць'; + + @override + String get studyLeaveTheStudy => 'Пакінуць навучанне'; + + @override + String get studyYouAreNowAContributor => 'Вы цяпер рэдактар'; + + @override + String get studyYouAreNowASpectator => 'Вы цяпер глядач'; + + @override + String get studyPgnTags => 'Тэгі PGN'; + + @override + String get studyLike => 'Упадабаць'; + + @override + String get studyUnlike => 'Разпадабаць'; + + @override + String get studyNewTag => 'Новы тэг'; + + @override + String get studyCommentThisPosition => 'Каментаваць пазіцыю'; + + @override + String get studyCommentThisMove => 'Каментаваць гэты ход'; + + @override + String get studyAnnotateWithGlyphs => 'Дадаць знакавую анатацыю'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Раздел занадта кароткі для аналізу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Толькі рэдактары навучання могуць запрасіць камп\'ютарны аналіз.'; + + @override + String get studyGetAFullComputerAnalysis => 'Атрымайце поўны серверны кампутарны аналіз галоўнай лініі.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Пераканайцеся, што раздзел гатоў. Вы можаце запрасіць аналіз толькі адзін раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Усе сінхранізаваныя ўдзельнікі застаюцца на аднолькавай пазіцыі'; + + @override + String get studyShareChanges => 'Падзяліцца зменамі з гледачамі та захаваць іх на серверы'; + + @override + String get studyPlaying => 'Гуляецца'; + + @override + String get studyShowEvalBar => 'Шкалы ацэнкі'; + + @override + String get studyFirst => 'На першую'; + + @override + String get studyPrevious => 'Папярэдняя'; + + @override + String get studyNext => 'Наступная'; + + @override + String get studyLast => 'На апошнюю'; + @override String get studyShareAndExport => 'Падзяліцца & экспартаваць'; + @override + String get studyCloneStudy => 'Кланаваць'; + + @override + String get studyStudyPgn => 'PGN навучання'; + + @override + String get studyDownloadAllGames => 'Спампаваць усе гульні'; + + @override + String get studyChapterPgn => 'PGN раздзелу'; + + @override + String get studyCopyChapterPgn => 'Скапіраваць PGN'; + + @override + String get studyDownloadGame => 'Спампаваць гульню'; + + @override + String get studyStudyUrl => 'URL навучання'; + + @override + String get studyCurrentChapterUrl => 'URL бягучага раздзелу'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Вы можаце ўставіць гэта на форум, каб убудаваць'; + + @override + String get studyStartAtInitialPosition => 'Пачынаць у пачатковай пазіцыі'; + + @override + String studyStartAtX(String param) { + return 'Пачынаць з $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Убудаваць у свой сайт або блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Пачытаць больш пра ўбудаванне'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Толькі публічныя навучанні могуць быць убудаваны!'; + + @override + String get studyOpen => 'Адкрыць'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 зрабіў для вас $param1'; + } + + @override + String get studyStudyNotFound => 'Навучанне не знойдзена'; + + @override + String get studyEditChapter => 'Рэдагаваць раздзел'; + + @override + String get studyNewChapter => 'Новы раздзел'; + + @override + String studyImportFromChapterX(String param) { + return 'Імпартаваць з $param'; + } + + @override + String get studyOrientation => 'Арыентацыя дошкі'; + + @override + String get studyAnalysisMode => 'Рэжым аналізу'; + + @override + String get studyPinnedChapterComment => 'Замацаваны каментар раздзелу'; + + @override + String get studySaveChapter => 'Захаваць раздзел'; + + @override + String get studyClearAnnotations => 'Ачысціць анатацыі'; + + @override + String get studyClearVariations => 'Ачысціць варыянты'; + + @override + String get studyDeleteChapter => 'Выдаліць раздзел'; + + @override + String get studyDeleteThisChapter => 'Выдаліць гэты раздел? Гэта нельга будзе адмяніць!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Выдаліць усе каментары, знакавыя анатацыі і намаляваныя фігуры ў гэтым раздзеле?'; + + @override + String get studyRightUnderTheBoard => 'Адразу пад дошкай'; + + @override + String get studyNoPinnedComment => 'Ніякіх'; + + @override + String get studyNormalAnalysis => 'Звычайны аналіз'; + + @override + String get studyHideNextMoves => 'Схаваць наступныя хады'; + + @override + String get studyInteractiveLesson => 'Інтэрактыўны занятак'; + + @override + String studyChapterX(String param) { + return 'Раздзел $param'; + } + + @override + String get studyEmpty => 'Пуста'; + + @override + String get studyStartFromInitialPosition => 'Пачынаць з пачатковай пазіцыі'; + + @override + String get studyEditor => 'Рэдактар'; + + @override + String get studyStartFromCustomPosition => 'Пачынаць з абранай пазіцыі'; + + @override + String get studyLoadAGameByUrl => 'Загрузіць гульні па URLs'; + + @override + String get studyLoadAPositionFromFen => 'Загрузіць пазіцыю з FEN'; + + @override + String get studyLoadAGameFromPgn => 'Загрузіць гульні з PGN'; + + @override + String get studyAutomatic => 'Аўтаматычна'; + + @override + String get studyUrlOfTheGame => 'URL гульняў, адзін на радок'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Загрузіць партыі з $param1 або $param2'; + } + + @override + String get studyCreateChapter => 'Стварыць раздзел'; + + @override + String get studyCreateStudy => 'Стварыць навучанне'; + + @override + String get studyEditStudy => 'Рэдактаваць навучанне'; + + @override + String get studyVisibility => 'Бачнасць'; + + @override + String get studyPublic => 'Публічны'; + + @override + String get studyUnlisted => 'Нябачны'; + + @override + String get studyInviteOnly => 'Толькі па запрашэннях'; + + @override + String get studyAllowCloning => 'Дазволіць кланаванне'; + + @override + String get studyNobody => 'Ніхто'; + + @override + String get studyOnlyMe => 'Толькі я'; + + @override + String get studyContributors => 'Рэдактары'; + + @override + String get studyMembers => 'Удзельнікі'; + + @override + String get studyEveryone => 'Кожны'; + + @override + String get studyEnableSync => 'Уключыць сінхранізацыю'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Так: трымаць усіх на аднолькавай пазіцыі'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: хай людзі вольна праглядаюць пазіцыі'; + + @override + String get studyPinnedStudyComment => 'Замацаваць каментар да занятку'; + @override String get studyStart => 'Пачаць'; + + @override + String get studySave => 'Захаваць'; + + @override + String get studyClearChat => 'Ачысціць чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Выдаліць гісторыю чата навучання цалкам? Гэта нельга будзе адмяніць!'; + + @override + String get studyDeleteStudy => 'Выдаліць навучанне'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Выдаліць навучанне поўнасцю? Гэта нельга будзе адмяніць! Увядзіце назву навучання каб падцвердзіць: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Дзе вы жадаеце навучацца?'; + + @override + String get studyGoodMove => 'Добры ход'; + + @override + String get studyMistake => 'Памылка'; + + @override + String get studyBrilliantMove => 'Бліскучы ход'; + + @override + String get studyBlunder => 'Позех'; + + @override + String get studyInterestingMove => 'Цікавы ход'; + + @override + String get studyDubiousMove => 'Сумнеўны ход'; + + @override + String get studyOnlyMove => 'Адзіны ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Раўная пазіцыя'; + + @override + String get studyUnclearPosition => 'Незразумелая пазіцыя'; + + @override + String get studyWhiteIsSlightlyBetter => 'У белых трошкі лепш'; + + @override + String get studyBlackIsSlightlyBetter => 'У чорных трошкі лепш'; + + @override + String get studyWhiteIsBetter => 'У белых лепш'; + + @override + String get studyBlackIsBetter => 'У чорных лепш'; + + @override + String get studyWhiteIsWinning => 'Белыя перамагаюць'; + + @override + String get studyBlackIsWinning => 'Чорныя перамагаюць'; + + @override + String get studyNovelty => 'Новаўвядзенне'; + + @override + String get studyDevelopment => 'Развіццё'; + + @override + String get studyInitiative => 'Ініцыятыва'; + + @override + String get studyAttack => 'Напад'; + + @override + String get studyCounterplay => 'Контргульня'; + + @override + String get studyTimeTrouble => 'Цэйтнот'; + + @override + String get studyWithCompensation => 'З кампенсацыяй'; + + @override + String get studyWithTheIdea => 'З ідэяй'; + + @override + String get studyNextChapter => 'Наступны раздзел'; + + @override + String get studyPrevChapter => 'Папярэдні раздзел'; + + @override + String get studyStudyActions => 'Дзеянні ў навучанні'; + + @override + String get studyTopics => 'Тэмы'; + + @override + String get studyMyTopics => 'Мае тэмы'; + + @override + String get studyPopularTopics => 'Папулярныя тэмы'; + + @override + String get studyManageTopics => 'Кіраваць тэмамі'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Гуляць зноў'; + + @override + String get studyWhatWouldYouPlay => 'Як бы вы пахадзілі ў гэтай пазіцыі?'; + + @override + String get studyYouCompletedThisLesson => 'Віншуем! Вы прайшлі гэты ўрок.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count раздзелаў', + many: '$count раздзелаў', + few: '$count раздзелы', + one: '$count раздзел', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count партый', + many: '$count партый', + few: '$count партыі', + one: '$count партыя', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count удзельнікаў', + many: '$count удзельнікаў', + few: '$count удзельніка', + one: '$count удзельнік', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульняў', + many: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульняў', + few: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульні', + one: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульню', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'зараз'; + + @override + String get timeagoRightNow => 'прама зараз'; + + @override + String get timeagoCompleted => 'завершана'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count секунд', + many: 'праз $count секунд', + few: 'праз $count секунды', + one: 'праз $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count хвілін', + many: 'праз $count хвілін', + few: 'праз $count хвіліны', + one: 'праз $count хвіліну', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count гадзін', + many: 'праз $count гадзін', + few: 'праз $count гадзіны', + one: 'праз $count гадзіну', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count дзён', + many: 'праз $count дзён', + few: 'праз $count дні', + one: 'праз $count дзень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count тыдняў', + many: 'праз $count тыдняў', + few: 'праз $count тыдні', + one: 'праз $count тыдзень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count месяцаў', + many: 'праз $count месяцаў', + few: 'праз $count месяцы', + one: 'праз $count месяц', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count гадоў', + many: 'праз $count гадоў', + few: 'праз $count гады', + one: 'праз $count год', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count хвілін таму', + many: '$count хвілін таму', + few: '$count хвіліны таму', + one: '$count хвіліну таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count гадзін таму', + many: '$count гадзін таму', + few: '$count гадзіны таму', + one: '$count гадзіну таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дзён таму', + many: '$count дзён таму', + few: '$count дні таму', + one: '$count дзень таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count тыдняў таму', + many: '$count тыдняў таму', + few: '$count тыдні таму', + one: '$count тыдзень таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count месяцаў таму', + many: '$count месяцаў таму', + few: '$count месяцы таму', + one: '$count месяц таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count гадоў таму', + many: '$count гадоў таму', + few: '$count гады таму', + one: '$count год таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Засталося $count хвіліны', + many: 'Засталося $count хвілін', + few: 'Засталося $count хвіліны', + one: 'Засталася $count хвіліна', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Засталося $count гадзіны', + many: 'Засталося $count гадзін', + few: 'Засталося $count гадзіны', + one: 'Засталася $count гадзіна', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 4d6c9a7e35..7a35020880 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsBg extends AppLocalizations { AppLocalizationsBg([String locale = 'bg']) : super(locale); @override - String get mobileHomeTab => 'Начало'; + String get mobileAllGames => 'Всички игри'; @override - String get mobilePuzzlesTab => 'Задачи'; + String get mobileAreYouSure => 'Сигурни ли сте?'; @override - String get mobileToolsTab => 'Анализ'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Гледай'; + String get mobileClearButton => 'Изчисти'; @override - String get mobileSettingsTab => 'Настройки'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'За да видите тази страница, трябва да влезете в профила си.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'Системни цветове'; + String get mobileFeedbackButton => 'Отзиви'; @override - String get mobileFeedbackButton => 'Отзиви'; + String mobileGreeting(String param) { + return 'Здравейте, $param'; + } @override - String get mobileOkButton => 'ОК'; + String get mobileGreetingWithoutName => 'Здравейте'; @override - String get mobileSettingsHapticFeedback => 'Вибрация при докосване'; + String get mobileHideVariation => 'Скрий вариацията'; @override - String get mobileSettingsImmersiveMode => 'Режим \"Цял екран\"'; + String get mobileHomeTab => 'Начало'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'За да видите тази страница, трябва да влезете в профила си.'; @override - String get mobileAllGames => 'Всички игри'; + String get mobileNoSearchResults => 'Няма резултати'; @override - String get mobileRecentSearches => 'Последни търсения'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Изчисти'; + String get mobileOkButton => 'ОК'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Няма резултати'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Сигурни ли сте?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Сподели тази задача'; + String get mobilePuzzleStormSubtitle => 'Решете колкото можете повече задачи за 3 минути.'; @override - String get mobileShareGameURL => 'Сподели URL на играта'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Сподели PGN'; + String get mobilePuzzleThemesSubtitle => 'Решавайте задачи от любимите Ви дебюти или изберете друга тема.'; @override - String get mobileSharePositionAsFEN => 'Сподели позицията във формат FEN'; + String get mobilePuzzlesTab => 'Задачи'; @override - String get mobileShowVariations => 'Покажи вариациите'; + String get mobileRecentSearches => 'Последни търсения'; @override - String get mobileHideVariation => 'Скрий вариацията'; + String get mobileSettingsHapticFeedback => 'Вибрация при докосване'; @override - String get mobileShowComments => 'Покажи коментарите'; + String get mobileSettingsImmersiveMode => 'Режим \"Цял екран\"'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Настройки'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Сподели PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Сподели URL на играта'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Сподели позицията във формат FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Сподели тази задача'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Покажи коментарите'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Покажи резултат'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Покажи вариациите'; @override String get mobileSomethingWentWrong => 'Възникна грешка.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Системни цветове'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Анализ'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Гледай'; @override String get activityActivity => 'Дейност'; @@ -246,6 +243,17 @@ class AppLocalizationsBg extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Изиграни $count $param2 кореспондентски игри', + one: 'Изиграна $count $param2 кореспондентска игра', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsBg extends AppLocalizations { @override String get broadcastBroadcasts => 'Излъчване'; + @override + String get broadcastMyBroadcasts => 'Моите излъчвания'; + @override String get broadcastLiveBroadcasts => 'Излъчвания на турнир на живо'; + @override + String get broadcastBroadcastCalendar => 'Календар на излъчванията'; + + @override + String get broadcastNewBroadcast => 'Нови предавания на живо'; + + @override + String get broadcastSubscribedBroadcasts => 'Излчвания които следя'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Добави рунд'; + + @override + String get broadcastOngoing => 'Текущи'; + + @override + String get broadcastUpcoming => 'Предстоящи'; + + @override + String get broadcastCompleted => 'Завършени'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Име на рунда'; + + @override + String get broadcastRoundNumber => 'Номер на рунда'; + + @override + String get broadcastTournamentName => 'Име на турнира'; + + @override + String get broadcastTournamentDescription => 'Кратко описание на турнира'; + + @override + String get broadcastFullDescription => 'Пълно описание на събитието'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Незадължително дълго описание на излъчването. $param1 е налично. Дължината трябва да по-малка от $param2 знака.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Уебадресът, който Lichess ще проверява, за да получи осъвременявания на PGN. Той трябва да е публично достъпен от интернет.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'По избор, ако знаете, кога започва събитието'; + + @override + String get broadcastCurrentGameUrl => 'URL на настоящата партия'; + + @override + String get broadcastDownloadAllRounds => 'Изтегли всички рундове'; + + @override + String get broadcastResetRound => 'Нулирай този рунд'; + + @override + String get broadcastDeleteRound => 'Изтрий този рунд'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Окончателно изтрийте този рунд и всичките му игри.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Изтрийте този рунд и всичките му игри. Източникът трябва да е активен за да можете да ги възстановите.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Изтрий този турнир'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Окончателно изтрий целия турнир, всичките му рундове и игри.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'По избор: промени имената на играчите, рейтингите и титлите'; + + @override + String get broadcastFideFederations => 'ФИДЕ федерации'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'ФИДЕ профил'; + + @override + String get broadcastFederation => 'Федерация'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Отвори в Lichess'; + + @override + String get broadcastTeams => 'Отбори'; + + @override + String get broadcastBoards => 'Дъски'; + + @override + String get broadcastOverview => 'Общ преглед'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Официален уебсайт'; + + @override + String get broadcastStandings => 'Класиране'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Игри в този турнир'; + + @override + String get broadcastScore => 'Резултат'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count излъчвания', + one: '$count излъчване', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Предизвикателства: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get preferencesInGameOnly => 'Само по време на игра'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматен часовник'; @@ -750,6 +1008,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Мелодия за известия'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Задачи'; @@ -1390,10 +1651,10 @@ class AppLocalizationsBg extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Опонентът има малко възможни ходове и всеки един от тях води до влошаване на положението му.'; @override - String get puzzleThemeHealthyMix => 'От всичко по малко'; + String get puzzleThemeMix => 'От всичко по малко'; @override - String get puzzleThemeHealthyMixDescription => 'По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.'; + String get puzzleThemeMixDescription => 'По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.'; @override String get puzzleThemePlayerGames => 'Партии на играча'; @@ -1767,9 +2028,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get byCPL => 'По CPL'; - @override - String get openStudy => 'Проучване'; - @override String get enable => 'Включване'; @@ -1797,9 +2055,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get removesTheDepthLimit => 'Анализът ще е безкраен, а компютърът ви - топъл'; - @override - String get engineManager => 'Мениджър на двигателя'; - @override String get blunder => 'Груба грешка'; @@ -2063,6 +2318,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get gamesPlayed => 'Изиграни игри'; + @override + String get ok => 'ОК'; + @override String get cancel => 'Отказ'; @@ -2437,9 +2695,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get unblock => 'Отблокирай'; - @override - String get followsYou => 'Следва ви'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 започна да следва $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsBg extends AppLocalizations { String get other => 'Друго'; @override - String get reportDescriptionHelp => 'Поставете линк към играта и обяснете какъв е проблемът с поведението на този потребител. Не казвайте единствено, че мами, но ни кажете как сте стигнали до този извод. Вашият доклад ще бъде обработен по-бързо, ако е написан на английски.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Моля дай поне един линк до измамна игра.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Статистика'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsBg extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess стриймъри'; + @override + String get studyPrivate => 'Лични'; + + @override + String get studyMyStudies => 'Моите казуси'; + + @override + String get studyStudiesIContributeTo => 'Казуси, към които допринасям'; + + @override + String get studyMyPublicStudies => 'Моите публични казуси'; + + @override + String get studyMyPrivateStudies => 'Моите лични казуси'; + + @override + String get studyMyFavoriteStudies => 'Моите любими казуси'; + + @override + String get studyWhatAreStudies => 'Какво представляват казусите?'; + + @override + String get studyAllStudies => 'Всички казуси'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Казуси от $param'; + } + + @override + String get studyNoneYet => 'Все още няма.'; + + @override + String get studyHot => 'Популярни'; + + @override + String get studyDateAddedNewest => 'Дата на добавяне (най-нови)'; + + @override + String get studyDateAddedOldest => 'Дата на добавяне (най-стари)'; + + @override + String get studyRecentlyUpdated => 'Скоро обновени'; + + @override + String get studyMostPopular => 'Най-популярни'; + + @override + String get studyAlphabetical => 'Азбучно'; + + @override + String get studyAddNewChapter => 'Добавяне на нов раздел'; + + @override + String get studyAddMembers => 'Добави членове'; + + @override + String get studyInviteToTheStudy => 'Покани към казуса'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Моля канете само хора, които познавате и които биха искали да се присъединят.'; + + @override + String get studySearchByUsername => 'Търсене по потребителско име'; + + @override + String get studySpectator => 'Зрител'; + + @override + String get studyContributor => 'Сътрудник'; + + @override + String get studyKick => 'Изритване'; + + @override + String get studyLeaveTheStudy => 'Напусни казуса'; + + @override + String get studyYouAreNowAContributor => 'Вие сте сътрудник'; + + @override + String get studyYouAreNowASpectator => 'Вие сте зрител'; + + @override + String get studyPgnTags => 'PGN тагове'; + + @override + String get studyLike => 'Харесай'; + + @override + String get studyUnlike => 'Не харесвам'; + + @override + String get studyNewTag => 'Нов таг'; + + @override + String get studyCommentThisPosition => 'Коментирай позицията'; + + @override + String get studyCommentThisMove => 'Коментирай хода'; + + @override + String get studyAnnotateWithGlyphs => 'Анотация със специални символи'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Тази глава е твърде къса и не може да бъде анализирана.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Само сътрудници към казуса могат да пускат компютърен анализ.'; + + @override + String get studyGetAFullComputerAnalysis => 'Вземи пълен сървърен анализ на основна линия.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Уверете се, че главата е завършена. Можете да пуснете анализ само веднъж.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Всички синхронизирани членове остават на същата позиция'; + + @override + String get studyShareChanges => 'Споделете промените със зрителите и ги запазете на сървъра'; + + @override + String get studyPlaying => 'Играе се'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Първа'; + + @override + String get studyPrevious => 'Предишна'; + + @override + String get studyNext => 'Следваща'; + + @override + String get studyLast => 'Последна'; + @override String get studyShareAndExport => 'Сподели'; + @override + String get studyCloneStudy => 'Клонирай'; + + @override + String get studyStudyPgn => 'PGN на казуса'; + + @override + String get studyDownloadAllGames => 'Изтегли всички партии'; + + @override + String get studyChapterPgn => 'PGN на главата'; + + @override + String get studyCopyChapterPgn => 'Копирай PGN'; + + @override + String get studyDownloadGame => 'Изтегли партия'; + + @override + String get studyStudyUrl => 'URL на казуса'; + + @override + String get studyCurrentChapterUrl => 'URL на настоящата глава'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Можете да поставите това във форум и ще бъде вградено'; + + @override + String get studyStartAtInitialPosition => 'Започни от начална позиция'; + + @override + String studyStartAtX(String param) { + return 'Започни от $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вгради в твоя сайт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Прочети повече за вграждането'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Само публични казуси могат да бъдат вграждани!'; + + @override + String get studyOpen => 'Отвори'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, предоставени от $param2'; + } + + @override + String get studyStudyNotFound => 'Казусът не бе открит'; + + @override + String get studyEditChapter => 'Промени глава'; + + @override + String get studyNewChapter => 'Нова глава'; + + @override + String studyImportFromChapterX(String param) { + return 'Импортиране от $param'; + } + + @override + String get studyOrientation => 'Ориентация'; + + @override + String get studyAnalysisMode => 'Режим на анализ'; + + @override + String get studyPinnedChapterComment => 'Коментар на главата'; + + @override + String get studySaveChapter => 'Запази глава'; + + @override + String get studyClearAnnotations => 'Изтрий анотациите'; + + @override + String get studyClearVariations => 'Изчисти вариациите'; + + @override + String get studyDeleteChapter => 'Изтрий глава'; + + @override + String get studyDeleteThisChapter => 'Изтриване на главата? Това е необратимо!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Изтрий всички коментари, специални символи и нарисувани форми в главата?'; + + @override + String get studyRightUnderTheBoard => 'Точно под дъската'; + + @override + String get studyNoPinnedComment => 'Никакви'; + + @override + String get studyNormalAnalysis => 'Нормален анализ'; + + @override + String get studyHideNextMoves => 'Скриване на следващите ходове'; + + @override + String get studyInteractiveLesson => 'Интерактивен урок'; + + @override + String studyChapterX(String param) { + return 'Глава: $param'; + } + + @override + String get studyEmpty => 'Празна'; + + @override + String get studyStartFromInitialPosition => 'Започни от начална позиция'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Започни от избрана позиция'; + + @override + String get studyLoadAGameByUrl => 'Зареди партии от URL'; + + @override + String get studyLoadAPositionFromFen => 'Зареди позиция от FEN'; + + @override + String get studyLoadAGameFromPgn => 'Зареди партии от PGN'; + + @override + String get studyAutomatic => 'Автоматичен'; + + @override + String get studyUrlOfTheGame => 'URL на партиите, по една на линия'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Зареди партии от $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Създай'; + + @override + String get studyCreateStudy => 'Създай казус'; + + @override + String get studyEditStudy => 'Редактирай казус'; + + @override + String get studyVisibility => 'Видимост'; + + @override + String get studyPublic => 'Публични'; + + @override + String get studyUnlisted => 'Несподелени'; + + @override + String get studyInviteOnly => 'Само с покани'; + + @override + String get studyAllowCloning => 'Позволи клониране'; + + @override + String get studyNobody => 'Никой'; + + @override + String get studyOnlyMe => 'Само за мен'; + + @override + String get studyContributors => 'Сътрудници'; + + @override + String get studyMembers => 'Членове'; + + @override + String get studyEveryone => 'Всички'; + + @override + String get studyEnableSync => 'Разреши синхронизиране'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: дръж всички на същата позиция'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: позволи свободно разглеждане'; + + @override + String get studyPinnedStudyComment => 'Коментар на казуса'; + @override String get studyStart => 'Начало'; + + @override + String get studySave => 'Запази'; + + @override + String get studyClearChat => 'Изтрий чат съобщенията'; + + @override + String get studyDeleteTheStudyChatHistory => 'Изтриване на чат историята? Това е необратимо!'; + + @override + String get studyDeleteStudy => 'Изтрий казуса'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Изтриване на целия казус? Това е необратимо! Въведете името на казуса за да потвърдите: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Къде да бъде проучено това?'; + + @override + String get studyGoodMove => 'Добър ход'; + + @override + String get studyMistake => 'Грешка'; + + @override + String get studyBrilliantMove => 'Отличен ход'; + + @override + String get studyBlunder => 'Груба грешка'; + + @override + String get studyInterestingMove => 'Интересен ход'; + + @override + String get studyDubiousMove => 'Съмнителен ход'; + + @override + String get studyOnlyMove => 'Единствен ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Равна позиция'; + + @override + String get studyUnclearPosition => 'Неясна позиция'; + + @override + String get studyWhiteIsSlightlyBetter => 'Белите са малко по-добре'; + + @override + String get studyBlackIsSlightlyBetter => 'Черните са малко по-добре'; + + @override + String get studyWhiteIsBetter => 'Белите са по-добре'; + + @override + String get studyBlackIsBetter => 'Черните са по-добре'; + + @override + String get studyWhiteIsWinning => 'Белите печелят'; + + @override + String get studyBlackIsWinning => 'Черните печелят'; + + @override + String get studyNovelty => 'Нововъведeние'; + + @override + String get studyDevelopment => 'Развитие'; + + @override + String get studyInitiative => 'Инициатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контра атака'; + + @override + String get studyTimeTrouble => 'Проблем с времето'; + + @override + String get studyWithCompensation => 'С компенсация'; + + @override + String get studyWithTheIdea => 'С идеята'; + + @override + String get studyNextChapter => 'Следваща глава'; + + @override + String get studyPrevChapter => 'Предишна глава'; + + @override + String get studyStudyActions => 'Опции за учене'; + + @override + String get studyTopics => 'Теми'; + + @override + String get studyMyTopics => 'Моите теми'; + + @override + String get studyPopularTopics => 'Популярни теми'; + + @override + String get studyManageTopics => 'Управление на темите'; + + @override + String get studyBack => 'Обратно'; + + @override + String get studyPlayAgain => 'Играйте отново'; + + @override + String get studyWhatWouldYouPlay => 'Какво бихте играли в тази позиция?'; + + @override + String get studyYouCompletedThisLesson => 'Поздравления! Вие завършихте този урок.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Глави', + one: '$count Глава', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Игри', + one: '$count Игра', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Членове', + one: '$count Член', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Постави твоя PGN текст тук, до $count партии', + one: 'Постави твоя PGN текст тук, до $count партия', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'току що'; + + @override + String get timeagoRightNow => 'точно сега'; + + @override + String get timeagoCompleted => 'завършено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count секунди', + one: 'след $count секунда', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count минути', + one: 'след $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count часа', + one: 'след $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count дни', + one: 'след $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count седмици', + one: 'след $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count месеца', + one: 'след $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count години', + one: 'след $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count минути', + one: 'преди $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Преди $count часа', + one: 'преди $count час', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Преди $count дни', + one: 'преди $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count седмици', + one: 'преди $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count месеца', + one: 'преди $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count години', + one: 'преди $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'остават $count минути', + one: 'остава $count минутa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'остават $count часа', + one: 'остава $count час', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 36ed6689cb..70def05f6b 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsBn extends AppLocalizations { AppLocalizationsBn([String locale = 'bn']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsBn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'কার্যকলাপ'; @@ -246,6 +243,17 @@ class AppLocalizationsBn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsBn extends AppLocalizations { @override String get broadcastBroadcasts => 'সম্প্রচার'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'সরাসরি টুর্নামেন্ট সম্প্রচার'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'নতুন সরাসরি সম্প্রচার'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'চলমান'; + + @override + String get broadcastUpcoming => 'আসন্ন'; + + @override + String get broadcastCompleted => 'সমাপ্ত'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'গোল নম্বর'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'ইভেন্টের সম্পূর্ণ বিবরণ'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'ইউআরএল যা লাইসেন্সেস পিজিএন আপডেট পেতে চেক করবে। এটি অবশ্যই ইন্টারনেট থেকে সর্বজনীনভাবে অ্যাক্সেসযোগ্য।.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'প্রতিদ্বন্দ্বীরা:$param1'; @@ -609,6 +864,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'দাবার ঘড়ি'; @@ -750,6 +1008,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'পাজল'; @@ -1390,10 +1651,10 @@ class AppLocalizationsBn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'প্রতিপক্ষের সীমিত চাল আছে, এবং সব চাল তাদের অবস্থান আরো খারাপ করবে।'; @override - String get puzzleThemeHealthyMix => 'পরিমিত মিশ্রণ'; + String get puzzleThemeMix => 'পরিমিত মিশ্রণ'; @override - String get puzzleThemeHealthyMixDescription => 'সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।'; + String get puzzleThemeMixDescription => 'সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।'; @override String get puzzleThemePlayerGames => 'খেলোয়ারদের খেলা হতে'; @@ -1636,7 +1897,7 @@ class AppLocalizationsBn extends AppLocalizations { String get deleteFromHere => 'এখান থেকে মুছুন'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'ভেরিয়েশন সঙ্কুচিত করুন'; @override String get expandVariations => 'Expand variations'; @@ -1767,9 +2028,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get byCPL => 'CPL দ্বারা'; - @override - String get openStudy => 'মুক্ত অধ্যয়ন'; - @override String get enable => 'সচল'; @@ -1797,9 +2055,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get removesTheDepthLimit => 'গভীরতার সীমা অপসারণ করুন এবং আপনার কম্পিউটারকে গরম রাখুন'; - @override - String get engineManager => 'ইঞ্জিন ম্যানেজার'; - @override String get blunder => 'গুরুতর ভুল'; @@ -1878,7 +2133,7 @@ class AppLocalizationsBn extends AppLocalizations { String get friends => 'বন্ধুরা'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'অন্যান্য খেলোয়াড়'; @override String get discussions => 'বার্তাগুলি'; @@ -2063,6 +2318,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get gamesPlayed => 'খেলা খেলেছেন'; + @override + String get ok => 'OK'; + @override String get cancel => 'বাতিল করুন'; @@ -2437,9 +2695,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get unblock => 'বাধা উঠিয়ে নিন'; - @override - String get followsYou => 'আপনাকে অনুসরণ করছে'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 অনুসরণ করা শুরু করেছেন $param2'; @@ -2715,7 +2970,7 @@ class AppLocalizationsBn extends AppLocalizations { String get website => 'Website'; @override - String get mobile => 'Mobile'; + String get mobile => 'মোবাইল'; @override String get help => 'সাহায্য'; @@ -2772,7 +3027,13 @@ class AppLocalizationsBn extends AppLocalizations { String get other => 'অন্য কোনো কারণ'; @override - String get reportDescriptionHelp => 'এখানে সেই খেলাটির link দেন এবং বলুন ওই ব্যক্তি ব্যবহারে কি অসুবিধা ছিল ?'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'অনুগ্রহ করে একটা চিটেড গেমের লিংক দিন।'; @@ -4077,6 +4338,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get nothingToSeeHere => 'এই মুহূর্তে এখানে দেখার কিছু নেই.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsBn extends AppLocalizations { @override String get streamerLichessStreamers => 'লিছেসস স্ত্রেয়ামের'; + @override + String get studyPrivate => 'ব্যাক্তিগত'; + + @override + String get studyMyStudies => 'আমার অধ্যায়ন'; + + @override + String get studyStudiesIContributeTo => 'যেসকল অধ্যায়নে আমার অবদান রয়েছে'; + + @override + String get studyMyPublicStudies => 'জনসাধারনকৃত আমার অধ্যায়নগুলো'; + + @override + String get studyMyPrivateStudies => 'আমার ব্যাক্তিগত অধ্যায়ন'; + + @override + String get studyMyFavoriteStudies => 'আমার পছন্দের অধ্যায়ন'; + + @override + String get studyWhatAreStudies => 'অধ্যায়ন কি?'; + + @override + String get studyAllStudies => 'সকল অধ্যায়নগুলি'; + + @override + String studyStudiesCreatedByX(String param) { + return 'অধ্যায়ন তৈরি করেছেন $param'; + } + + @override + String get studyNoneYet => 'আপাতত নেই।'; + + @override + String get studyHot => 'গরমাগরম'; + + @override + String get studyDateAddedNewest => 'তৈরির তারিখ (সবচেয়ে নতুন)'; + + @override + String get studyDateAddedOldest => 'তৈরির তারিখ (সবচেয়ে পুরনো)'; + + @override + String get studyRecentlyUpdated => 'সাম্প্রতিক হালনাগাদকৃত'; + + @override + String get studyMostPopular => 'সবচেয়ে জনপ্রিয়'; + + @override + String get studyAlphabetical => 'বর্ণানুক্রমিক'; + + @override + String get studyAddNewChapter => 'নতুন অধ্যায় যোগ করুন'; + + @override + String get studyAddMembers => 'সদস্য যোগ করুন'; + + @override + String get studyInviteToTheStudy => 'স্টাডিতে আমন্ত্রণ জানান'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'দয়া করে যাদের আপনি জানেন তাদের এবং যারা সক্রিয়ভাবে যোগদান করতে চায়, কেবল তাদেরকেই আমন্ত্রন জানান।'; + + @override + String get studySearchByUsername => 'ইউজারনেম দ্বারা খুঁজুন'; + + @override + String get studySpectator => 'দর্শক'; + + @override + String get studyContributor => 'অবদানকারী'; + + @override + String get studyKick => 'লাথি দিয়ে বের করুন'; + + @override + String get studyLeaveTheStudy => 'Leave the study'; + + @override + String get studyYouAreNowAContributor => 'You are now a contributor'; + + @override + String get studyYouAreNowASpectator => 'আপনি এখন দর্শক'; + + @override + String get studyPgnTags => 'PGN ট্যাগ'; + + @override + String get studyLike => 'পছন্দ করা'; + + @override + String get studyUnlike => 'পছন্দ নয়'; + + @override + String get studyNewTag => 'নতুন ট্যাগ'; + + @override + String get studyCommentThisPosition => 'Comment on this position'; + + @override + String get studyCommentThisMove => 'Comment on this move'; + + @override + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'এনালাইসিস করার জন্য চ্যাপ্টারটা খুব ছোট'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'শুধুমাত্র স্টাডি\'টার কন্ট্রিবিউটররাই কম্পিউটার এনালাইসিস এর রিকোয়েস্ট করতে পারবে।'; + + @override + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'খেলছে'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'সর্ব প্রথম'; + + @override + String get studyPrevious => 'আগের ধাপ'; + + @override + String get studyNext => 'পরের ধাপ'; + + @override + String get studyLast => 'সর্বশেষ'; + @override String get studyShareAndExport => 'Share & export'; + @override + String get studyCloneStudy => 'Clone'; + + @override + String get studyStudyPgn => 'অধ্যায়ন PGN আকারে'; + + @override + String get studyDownloadAllGames => 'ডাউনলোড করুন সকল গেম'; + + @override + String get studyChapterPgn => 'Chapter PGN'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Download game'; + + @override + String get studyStudyUrl => 'Study URL'; + + @override + String get studyCurrentChapterUrl => 'Current chapter URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; + + @override + String get studyStartAtInitialPosition => 'Start at initial position'; + + @override + String studyStartAtX(String param) { + return 'Start at $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Embed in your website'; + + @override + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; + + @override + String get studyOpen => 'ওপেন'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; + } + + @override + String get studyStudyNotFound => 'Study not found'; + + @override + String get studyEditChapter => 'Edit chapter'; + + @override + String get studyNewChapter => 'New chapter'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Analysis mode'; + + @override + String get studyPinnedChapterComment => 'Pinned chapter comment'; + + @override + String get studySaveChapter => 'Save chapter'; + + @override + String get studyClearAnnotations => 'Clear annotations'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Delete chapter'; + + @override + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; + + @override + String get studyRightUnderTheBoard => 'Right under the board'; + + @override + String get studyNoPinnedComment => 'None'; + + @override + String get studyNormalAnalysis => 'Normal analysis'; + + @override + String get studyHideNextMoves => 'Hide next moves'; + + @override + String get studyInteractiveLesson => 'Interactive lesson'; + + @override + String studyChapterX(String param) { + return 'Chapter $param'; + } + + @override + String get studyEmpty => 'Empty'; + + @override + String get studyStartFromInitialPosition => 'Start from initial position'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'নির্দিষ্ট অবস্থান থেকে শুরু করুন'; + + @override + String get studyLoadAGameByUrl => 'URL থেকে খেলা লোড করুন'; + + @override + String get studyLoadAPositionFromFen => 'FEN থেকে একটি অবস্থান লোড করুন'; + + @override + String get studyLoadAGameFromPgn => 'PGN থেকে খেলা লোড করুন'; + + @override + String get studyAutomatic => 'স্বয়ংক্রিয়'; + + @override + String get studyUrlOfTheGame => 'খেলাগুলোর URL, লাইনপ্রতি একটি'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 অথবা $param2 থেকে খেলাসমূহ লোড করুন'; + } + + @override + String get studyCreateChapter => 'অধ্যায় তৈরি করুন'; + + @override + String get studyCreateStudy => 'স্টাডি তৈরি করুন'; + + @override + String get studyEditStudy => 'স্টাডি সম্পাদনা করুন'; + + @override + String get studyVisibility => 'দৃশ্যমানতা'; + + @override + String get studyPublic => 'পাবলিক'; + + @override + String get studyUnlisted => 'প্রাইভেট'; + + @override + String get studyInviteOnly => 'কেবল আমন্ত্রনভিত্তিক'; + + @override + String get studyAllowCloning => 'ক্লোন করার অনুমতি দিন'; + + @override + String get studyNobody => 'কেউ না'; + + @override + String get studyOnlyMe => 'শুধু আমি'; + + @override + String get studyContributors => 'অবদানকারীরা'; + + @override + String get studyMembers => 'সদস্যবৃন্দ'; + + @override + String get studyEveryone => 'সবাই'; + + @override + String get studyEnableSync => 'সাইনক চালু করুন'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'সবাইকে একই অবস্থানে রাখুন'; + + @override + String get studyNoLetPeopleBrowseFreely => 'না: মানুষকে মুক্তভাবে ব্রাউজ করতে দিন'; + + @override + String get studyPinnedStudyComment => 'পিন করা স্টাডি মন্তব্য'; + @override String get studyStart => 'শুরু করুন'; + + @override + String get studySave => 'সংরক্ষন করুন'; + + @override + String get studyClearChat => 'চ্যাট পরিষ্কার করুন'; + + @override + String get studyDeleteTheStudyChatHistory => 'স্টাডি চ্যাটের ইতিহাস মুছে ফেলবেন? এটা কিন্তু ফিরে আসবে না!'; + + @override + String get studyDeleteStudy => 'স্টাডি মুছে ফেলুন'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'আপনি কোথায় এটা চর্চা করবেন?'; + + @override + String get studyGoodMove => 'ভালো চাল'; + + @override + String get studyMistake => 'ভূল চাল'; + + @override + String get studyBrilliantMove => 'অসাধারণ চাল'; + + @override + String get studyBlunder => 'ব্লান্ডার'; + + @override + String get studyInterestingMove => 'আগ্রহোদ্দীপক চাল'; + + @override + String get studyDubiousMove => 'অনিশ্চিত চাল'; + + @override + String get studyOnlyMove => 'একমাত্র সম্ভাব্য চাল'; + + @override + String get studyZugzwang => 'যুগযোয়াং'; + + @override + String get studyEqualPosition => 'সমান অবস্থান'; + + @override + String get studyUnclearPosition => 'অনিশ্চিত অবস্থান'; + + @override + String get studyWhiteIsSlightlyBetter => 'সাদা একটু বেশি ভালো'; + + @override + String get studyBlackIsSlightlyBetter => 'কালো একটু বেশি ভালো'; + + @override + String get studyWhiteIsBetter => 'সাদা ভালো'; + + @override + String get studyBlackIsBetter => 'কালো ভালো'; + + @override + String get studyWhiteIsWinning => 'সাদা জিতছে'; + + @override + String get studyBlackIsWinning => 'কালো জিতছে'; + + @override + String get studyNovelty => 'নোভেল্টি'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countটি অধ্যায়', + one: '$countটি অধ্যায়', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countটি খেলা', + one: '$countটি খেলা', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count জন সদস্য', + one: '$count জন সদস্য', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN টেক্সট এখানে পেস্ট করুন, $count টি খেলা পর্যন্ত', + one: 'PGN টেক্সট এখানে পেস্ট করুন, $count টি খেলা পর্যন্ত', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'এখনই'; + + @override + String get timeagoRightNow => 'এই মুহূর্তে'; + + @override + String get timeagoCompleted => 'সম্পন্ন হয়েছে'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সেকেন্ডের মধ্যে', + one: '$count সেকেন্ডের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিটের মধ্যে', + one: '$count মিনিটের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টার মধ্যে', + one: '$count ঘন্টার মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count দিনের মধ্যে', + one: '$count দিনের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সপ্তাহের মধ্যে', + one: '$count সপ্তাহের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মাসের মধ্যে', + one: '$count মাসের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count বছরের মধ্যে', + one: '$count বছরের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিট আগে', + one: '$count মিনিট আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টা আগে', + one: '$count ঘণ্টা আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count দিন আগে', + one: '$count দিন আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সপ্তাহ আগে', + one: '$count সপ্তাহ আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মাস আগে', + one: '$count মাস আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count বছর আগে', + one: '$count বছর আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিট বাকি', + one: '$count মিনিট বাকি', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টা বাকি', + one: '$count ঘন্টা বাকি', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 859cba0240..ae5d633667 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsBr extends AppLocalizations { AppLocalizationsBr([String locale = 'br']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsBr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Obererezhioù diwezhañ'; @@ -270,6 +267,17 @@ class AppLocalizationsBr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -399,9 +407,256 @@ class AppLocalizationsBr extends AppLocalizations { @override String get broadcastBroadcasts => 'War-eeun'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Tournamantoù skignet war-eeun'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Skignañ war-eeun nevez'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'O ren'; + + @override + String get broadcastUpcoming => 'A-benn nebeut'; + + @override + String get broadcastCompleted => 'Tremenet'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Niverenn ar batalm'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Deskrivadur an abadenn a-bezh'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Deskrivadur hir ar skignañ war-eeun ma fell deoc\'h.$param1 zo dijabl. Ne vo ket hiroc\'h evit $param2 sin.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'An URL a ray Lichess ganti evit kaout hizivadurioù ar PGN. Ret eo dezhi bezañ digor d\'an holl war Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Diret eo, ma ouzit pegoulz e krogo'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Dilemel an tournamant-mañ'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Dilemel an tournamant da viken, an holl grogadoù ha pep tra penn-da-benn.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Daeoù: $param1'; @@ -660,6 +915,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Horolaj echedoù'; @@ -801,6 +1059,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klevet ar c\'hloc\'h'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Poelladennoù'; @@ -1442,10 +1703,10 @@ class AppLocalizationsBr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'A bep seurt'; + String get puzzleThemeMix => 'A bep seurt'; @override - String get puzzleThemeHealthyMixDescription => 'A bep seurt. N\'ouzit ket petra gortoz hag e mod-se e voc\'h prest evit pep tra! Heñvel ouzh ar c\'hrogadoù gwir.'; + String get puzzleThemeMixDescription => 'A bep seurt. N\'ouzit ket petra gortoz hag e mod-se e voc\'h prest evit pep tra! Heñvel ouzh ar c\'hrogadoù gwir.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1819,9 +2080,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get byCPL => 'Dre CPL'; - @override - String get openStudy => 'Digeriñ ar studi'; - @override String get enable => 'Enaouiñ'; @@ -1849,9 +2107,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; - @override - String get engineManager => 'Merañ an urzhiataer'; - @override String get blunder => 'Bourd'; @@ -2115,6 +2370,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get gamesPlayed => 'Krogadoù c\'hoariet'; + @override + String get ok => 'OK'; + @override String get cancel => 'Nullañ'; @@ -2489,9 +2747,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get unblock => 'Distankañ'; - @override - String get followsYou => 'Ho heuilh'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 zo krog da heuliañ $param2'; @@ -2824,7 +3079,13 @@ class AppLocalizationsBr extends AppLocalizations { String get other => 'All'; @override - String get reportDescriptionHelp => 'Pegit liamm ar c\'hrogad(où) ha displegit ar pezh a ya a-dreuz gant emzalc\'h oc\'h enebour. Lâret \"o truchañ emañ\" ne vo ket trawalc\'h, ret eo displegañ mat. Buanoc\'h e pledimp ganti ma skrivit e saozneg.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Roit d\'an nebeutañ ul liamm hag a gas d\'ur c\'hrogad trucherezh ennañ.'; @@ -4129,6 +4390,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4901,9 +5165,744 @@ class AppLocalizationsBr extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamerien Lichess'; + @override + String get studyPrivate => 'Prevez'; + + @override + String get studyMyStudies => 'Ma studiadennoù'; + + @override + String get studyStudiesIContributeTo => 'Studiadennoù am eus kemeret perzh enne'; + + @override + String get studyMyPublicStudies => 'Ma studiadennoù foran'; + + @override + String get studyMyPrivateStudies => 'Ma studiadennoù prevez'; + + @override + String get studyMyFavoriteStudies => 'Ma studiadennoù muiañ-karet'; + + @override + String get studyWhatAreStudies => 'Petra eo ar studiadennoù?'; + + @override + String get studyAllStudies => 'An holl studiadennoù'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studiadennoù krouet gant $param'; + } + + @override + String get studyNoneYet => 'Hini ebet evit poent.'; + + @override + String get studyHot => 'Deus ar c\'hiz'; + + @override + String get studyDateAddedNewest => 'Deiziad ouzhpennet (nevesañ)'; + + @override + String get studyDateAddedOldest => 'Deiziad ouzhpennet (koshañ)'; + + @override + String get studyRecentlyUpdated => 'Hizivaet a-nevez'; + + @override + String get studyMostPopular => 'Muiañ karet'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Ouzhpennañ ur pennad'; + + @override + String get studyAddMembers => 'Ouzhpennañ izili'; + + @override + String get studyInviteToTheStudy => 'Pediñ d\'ar studiadenn'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Na bedit nemet tud a anavezit hag o deus c\'hoant da gemer perzh da vat en ho studiadenn.'; + + @override + String get studySearchByUsername => 'Klask dre anv implijer'; + + @override + String get studySpectator => 'Arvester'; + + @override + String get studyContributor => 'Perzhiad'; + + @override + String get studyKick => 'Forbannañ'; + + @override + String get studyLeaveTheStudy => 'Kuitaat ar studiadenn'; + + @override + String get studyYouAreNowAContributor => 'Perzhiad oc\'h bremañ'; + + @override + String get studyYouAreNowASpectator => 'Un arvester oc\'h bremañ'; + + @override + String get studyPgnTags => 'Tikedennoù PGN'; + + @override + String get studyLike => 'Plijet'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Tikedenn nevez'; + + @override + String get studyCommentThisPosition => 'Lâret ur ger diwar-benn al lakadur-mañ'; + + @override + String get studyCommentThisMove => 'Ober un evezhiadenn diwar-benn ar fiñvadenn-mañ'; + + @override + String get studyAnnotateWithGlyphs => 'Notennaouiñ gant arouezioù'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Re verr eo ar pennad evit bezañ dielfennet.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'N\'eus nemet perzhidi ar studiadenn a c\'hall goulenn un dielfennañ urzhiataer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Kaout un dielfennañ klok eus ar bennlinenn graet gant un urzhiataer.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bezit sur eo klok ar pennad. Ne c\'hallit goulenn un dielfennañ nemet ur wech.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Er memes lec\'hiadur e chom holl izili ar SYNC'; + + @override + String get studyShareChanges => 'Rannañ cheñchamantoù gant an arvesterien ha saveteiñ anezhe war ar servor'; + + @override + String get studyPlaying => 'O c\'hoari'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Kentañ'; + + @override + String get studyPrevious => 'War-gil'; + + @override + String get studyNext => 'War-lec\'h'; + + @override + String get studyLast => 'Diwezhañ'; + @override String get studyShareAndExport => 'Skignañ & ezporzhiañ'; + @override + String get studyCloneStudy => 'Eilañ'; + + @override + String get studyStudyPgn => 'PGN ar studi'; + + @override + String get studyDownloadAllGames => 'Pellgargañ an holl grogadoù'; + + @override + String get studyChapterPgn => 'PGN ar pennad'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Pellgargañ ur c\'hrogad'; + + @override + String get studyStudyUrl => 'Studiañ URL'; + + @override + String get studyCurrentChapterUrl => 'URL ar pennad evit poent'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Gallout a rit pegañ se er forom evit ensoc\'hañ'; + + @override + String get studyStartAtInitialPosition => 'Kregiñ el lec\'hiadur kentañ'; + + @override + String studyStartAtX(String param) { + return 'Kregiñ e $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Enframmañ en ho lec\'hienn pe blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Goût hiroc\'h diwar-benn an ensoc\'hañ'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ar studiadennoù foran a c\'hall bezañ ensoc\'het!'; + + @override + String get studyOpen => 'Digeriñ'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, zo kaset deoc\'h gant $param2'; + } + + @override + String get studyStudyNotFound => 'N\'eo ket bet kavet ar studiadenn'; + + @override + String get studyEditChapter => 'Aozañ ar pennad'; + + @override + String get studyNewChapter => 'Pennad nevez'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Tuadur'; + + @override + String get studyAnalysisMode => 'Doare dielfennañ'; + + @override + String get studyPinnedChapterComment => 'Ali war ar pennad spilhet'; + + @override + String get studySaveChapter => 'Saveteiñ pennad'; + + @override + String get studyClearAnnotations => 'Diverkañ an notennoù'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Dilemel pennad'; + + @override + String get studyDeleteThisChapter => 'Dilemel ar pennad-mañ? Hep distro e vo!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Diverkañ an holl evezhiadennoù ha notennoù er pennad?'; + + @override + String get studyRightUnderTheBoard => 'Dindan an dablez'; + + @override + String get studyNoPinnedComment => 'Hini ebet'; + + @override + String get studyNormalAnalysis => 'Dielfennañ normal'; + + @override + String get studyHideNextMoves => 'Kuzhat ar fiñvadennoù da heul'; + + @override + String get studyInteractiveLesson => 'Kentel etreoberiat'; + + @override + String studyChapterX(String param) { + return 'Pennad $param'; + } + + @override + String get studyEmpty => 'Goullo'; + + @override + String get studyStartFromInitialPosition => 'Kregiñ el lec\'hiadur kentañ'; + + @override + String get studyEditor => 'Aozer'; + + @override + String get studyStartFromCustomPosition => 'Kregiñ adalek ul lakadur aozet'; + + @override + String get studyLoadAGameByUrl => 'Kargañ ur c\'hrogad dre URL'; + + @override + String get studyLoadAPositionFromFen => 'Kargañ ul lakadur dre FEN'; + + @override + String get studyLoadAGameFromPgn => 'Kargañ ul lakadur dre PGN'; + + @override + String get studyAutomatic => 'Emgefre'; + + @override + String get studyUrlOfTheGame => 'URL ar c\'hrogad'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Kargañ ur c\'hrogad eus $param1 pe $param2'; + } + + @override + String get studyCreateChapter => 'Krouiñ pennad'; + + @override + String get studyCreateStudy => 'Krouiñ ur studiadenn'; + + @override + String get studyEditStudy => 'Aozañ studiadenn'; + + @override + String get studyVisibility => 'Gwelusted'; + + @override + String get studyPublic => 'Foran'; + + @override + String get studyUnlisted => 'N\'eo ket bet listennet'; + + @override + String get studyInviteOnly => 'Kouvidi hepken'; + + @override + String get studyAllowCloning => 'Aotreañ ar c\'hlonañ'; + + @override + String get studyNobody => 'Den ebet'; + + @override + String get studyOnlyMe => 'Me hepken'; + + @override + String get studyContributors => 'Perzhidi'; + + @override + String get studyMembers => 'Izili'; + + @override + String get studyEveryone => 'An holl dud'; + + @override + String get studyEnableSync => 'Gweredekaat sync'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ya: laoskit an traoù evel m\'emaint'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nann: laoskit an dud merdeiñ trankilik'; + + @override + String get studyPinnedStudyComment => 'Ali war ar studiadenn spilhet'; + @override String get studyStart => 'Kregiñ'; + + @override + String get studySave => 'Saveteiñ'; + + @override + String get studyClearChat => 'Diverkañ ar flapañ'; + + @override + String get studyDeleteTheStudyChatHistory => 'Dilemel an istor-flapañ? Hep distro e vo!'; + + @override + String get studyDeleteStudy => 'Dilemel ar studiadenn'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Pelec\'h ho peus c\'hoant da studiañ se?'; + + @override + String get studyGoodMove => 'Fiñvadenn vat'; + + @override + String get studyMistake => 'Fazi'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Bourd'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pennad', + many: '$count pennad', + few: '$count pennad', + two: '$count pennad', + one: '$count pennad', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count C\'hoariadenn', + many: '$count C\'hoariadenn', + few: '$count C\'hoariadenn', + two: '$count C\'hoariadenn', + one: '$count C\'hoariadenn', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ezel', + many: '$count Ezel', + few: '$count Ezel', + two: '$count Ezel', + one: '$count Ezel', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + many: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + few: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + two: 'Pegit testenn ho PGN amañ, betek $count grogad', + one: 'Pegit testenn ho PGN amañ, betek $count krogad', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'bremañ'; + + @override + String get timeagoRightNow => 'bremañ'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count eilenn', + many: 'a-benn $count eilenn', + few: 'a-benn $count eilenn', + two: 'a-benn $count eilenn', + one: 'a-benn $count eilenn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count munutenn', + many: 'a-benn $count munutenn', + few: 'a-benn $count munutenn', + two: 'a-benn $count vunutenn', + one: 'a-benn $count vunutenn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count eur', + many: 'a-benn $count eur', + few: 'a-benn $count eur', + two: 'a-benn $count eur', + one: 'a-benn $count eur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count deiz', + many: 'a-benn $count deiz', + few: 'a-benn $count deiz', + two: 'a-benn $count zeiz', + one: 'a-benn $count deiz', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count sizhun', + many: 'a-benn $count sizhun', + few: 'a-benn $count sizhun', + two: 'a-benn $count sizhun', + one: 'a-benn $count sizhun', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count miz', + many: 'a-benn $count miz', + few: 'a-benn $count miz', + two: 'a-benn $count viz', + one: 'a-benn $count miz', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count bloaz', + many: 'a-benn $count bloaz', + few: 'a-benn $count bloaz', + two: 'a-benn $count vloaz', + one: 'a-benn $count bloaz', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count munutenn zo', + many: '$count munutenn zo', + few: '$count munutenn zo', + two: '$count vunutenn zo', + one: '$count vunutenn zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count eur zo', + many: '$count eur zo', + few: '$count eur zo', + two: '$count eur zo', + one: '$count eur zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count deiz zo', + many: '$count deiz zo', + few: '$count deiz zo', + two: '$count zeiz zo', + one: '$count deiz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sizhun zo', + many: '$count sizhun zo', + few: '$count sizhun zo', + two: '$count sizhun zo', + one: '$count sizhun zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count miz zo', + many: '$count miz zo', + few: '$count miz zo', + two: '$count viz zo', + one: '$count miz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bloaz zo', + many: '$count bloaz zo', + few: '$count bloaz zo', + two: '$count vloaz zo', + one: '$count bloaz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 59c21e3501..e6fe373180 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsBs extends AppLocalizations { AppLocalizationsBs([String locale = 'bs']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsBs extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivnost'; @@ -254,6 +251,17 @@ class AppLocalizationsBs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +373,257 @@ class AppLocalizationsBs extends AppLocalizations { @override String get broadcastBroadcasts => 'Emitovanja'; + @override + String get broadcastMyBroadcasts => 'Moja emitiranja'; + @override String get broadcastLiveBroadcasts => 'Prenos turnira uživo'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Novo emitovanje uživo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Dodajte kolo'; + + @override + String get broadcastOngoing => 'U toku'; + + @override + String get broadcastUpcoming => 'Nadolazeći'; + + @override + String get broadcastCompleted => 'Završeno'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ime kola'; + + @override + String get broadcastRoundNumber => 'Zaokružen broj'; + + @override + String get broadcastTournamentName => 'Naziv turnira'; + + @override + String get broadcastTournamentDescription => 'Kratak opis turnira'; + + @override + String get broadcastFullDescription => 'Potpuni opis događaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobavezni dugi opis događaja koji se emituje. $param1 je dostupan. Dužina mora biti manja od $param2 slova.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Link koji će Lichess koristiti kako bi redovno ažurirao PGN. Mora biti javno dostupan na internetu.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobavezno, ukoliko znate kada počinje događaj'; + + @override + String get broadcastCurrentGameUrl => 'Link za trenutnu partiju'; + + @override + String get broadcastDownloadAllRounds => 'Skinite sve runde'; + + @override + String get broadcastResetRound => 'Ponovo postavite ovo kolo'; + + @override + String get broadcastDeleteRound => 'Izbrišite ovo kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivno izbrišite ovo kolo i partije u njemu.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbrišite sve partije iz ovog kola. Izvor mora biti aktivan da biste ih mogli ponovo kreirati.'; + + @override + String get broadcastEditRoundStudy => 'Podesite studiju kola'; + + @override + String get broadcastDeleteTournament => 'Izbrišite ovaj turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitivno izbrišite cijeli turnir, sva kola i sve partije.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count emitovanja', + few: '$count emitovanja', + one: '$count emitovanje', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izazovi: $param1'; @@ -626,6 +882,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get preferencesInGameOnly => 'Samo unutar igre'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sat'; @@ -767,6 +1026,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvuk obavještenja'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Problemi'; @@ -1412,10 +1674,10 @@ class AppLocalizationsBs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Protivnik ima ograničen broj poteza, a svaki od njih pogoršava mu poziciju.'; @override - String get puzzleThemeHealthyMix => 'Zdrava mješavina'; + String get puzzleThemeMix => 'Zdrava mješavina'; @override - String get puzzleThemeHealthyMixDescription => 'Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.'; + String get puzzleThemeMixDescription => 'Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.'; @override String get puzzleThemePlayerGames => 'Igračke igre'; @@ -1789,9 +2051,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get byCPL => 'Po SDP'; - @override - String get openStudy => 'Otvori studiju'; - @override String get enable => 'Omogući'; @@ -1819,9 +2078,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get removesTheDepthLimit => 'Uklanja granicu do koje računar može analizirati, i održava toplinu računara'; - @override - String get engineManager => 'Upravitelj šahovskog programa'; - @override String get blunder => 'Grubi previd'; @@ -2085,6 +2341,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get gamesPlayed => 'Odigrane partije'; + @override + String get ok => 'OK'; + @override String get cancel => 'Odustani'; @@ -2459,9 +2718,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Prati vas'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je počeo pratiti $param2'; @@ -2794,7 +3050,13 @@ class AppLocalizationsBs extends AppLocalizations { String get other => 'Ostalo'; @override - String get reportDescriptionHelp => 'Zalijepite link na partiju ili partije u pitanju i objasnite što nije bilo u redu sa ponašanjem korisnika. Nemojte samo reći \"varao je\", nego objasnite kako ste došli do tog zaključka. Vaša prijava će biti brže obrađena ukoliko je napišete na engleskom jeziku.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Molimo navedite barem jedan link na partiju u kojoj je igrač varao.'; @@ -4099,6 +4361,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4789,9 +5054,710 @@ class AppLocalizationsBs extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess emiteri'; + @override + String get studyPrivate => 'Privatna'; + + @override + String get studyMyStudies => 'Moje studije'; + + @override + String get studyStudiesIContributeTo => 'Studije kojima doprinosim'; + + @override + String get studyMyPublicStudies => 'Moje javne studije'; + + @override + String get studyMyPrivateStudies => 'Moje privatne studije'; + + @override + String get studyMyFavoriteStudies => 'Moje omiljene studije'; + + @override + String get studyWhatAreStudies => 'Šta su studije?'; + + @override + String get studyAllStudies => 'Sve studije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studije koje je kreirao/la $param'; + } + + @override + String get studyNoneYet => 'Još nijedna.'; + + @override + String get studyHot => 'U trendu'; + + @override + String get studyDateAddedNewest => 'Datum dodavanja (najnovije)'; + + @override + String get studyDateAddedOldest => 'Datum dodavanja (najstarije)'; + + @override + String get studyRecentlyUpdated => 'Nedavno ažurirane'; + + @override + String get studyMostPopular => 'Najpopularnije'; + + @override + String get studyAlphabetical => 'Abecedno'; + + @override + String get studyAddNewChapter => 'Dodajte novo poglavlje'; + + @override + String get studyAddMembers => 'Dodajte članove'; + + @override + String get studyInviteToTheStudy => 'Pozovite na studiju'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Molimo Vas da pozovete samo ljude koje znate i koji su zainteresovani da aktivno učustvuju u ovoj studiji.'; + + @override + String get studySearchByUsername => 'Pretraga prema korisničkom imenu'; + + @override + String get studySpectator => 'Posmatrač'; + + @override + String get studyContributor => 'Saradnik'; + + @override + String get studyKick => 'Izbaci'; + + @override + String get studyLeaveTheStudy => 'Napustite studiju'; + + @override + String get studyYouAreNowAContributor => 'Sada ste saradnik'; + + @override + String get studyYouAreNowASpectator => 'Sada ste posmatrač'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Sviđa mi se'; + + @override + String get studyUnlike => 'Ne sviđa mi se'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentirajte ovu poziciju'; + + @override + String get studyCommentThisMove => 'Komentirajte ovaj potez'; + + @override + String get studyAnnotateWithGlyphs => 'Obilježite poteze simbolima'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Poglavlje je prekratko za analizu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo saradnici u studiji mogu zahtijevati računarsku analizu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dobijte potpunu serversku analizu glavne varijacije.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Budite sigurni da je poglavlje gotovo. Računarsku analizu možete zahtjevati samo jednom.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Svi sinhronizovani članovi ostaju na istoj poziciji'; + + @override + String get studyShareChanges => 'Podijelite promjene sa posmatračima i sačuvajte ih na server'; + + @override + String get studyPlaying => 'U toku'; + + @override + String get studyShowEvalBar => 'Evaluacijske trake'; + + @override + String get studyFirst => 'Prva strana'; + + @override + String get studyPrevious => 'Prethodna strana'; + + @override + String get studyNext => 'Sljedeća strana'; + + @override + String get studyLast => 'Posljednja strana'; + @override String get studyShareAndExport => 'Podijelite i izvezite'; + @override + String get studyCloneStudy => 'Klonirajte'; + + @override + String get studyStudyPgn => 'Studirajte PGN'; + + @override + String get studyDownloadAllGames => 'Skinite sve partije'; + + @override + String get studyChapterPgn => 'PGN poglavlja'; + + @override + String get studyCopyChapterPgn => 'Kopirajte PGN'; + + @override + String get studyDownloadGame => 'Skini partiju'; + + @override + String get studyStudyUrl => 'Link studije'; + + @override + String get studyCurrentChapterUrl => 'Link trenutnog poglavlja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Možete ovo zalijepiti na forumu ili Vašem blogu na Lichessu kako biste ugradili poglavlje'; + + @override + String get studyStartAtInitialPosition => 'Krenite sa inicijalnom pozicijom'; + + @override + String studyStartAtX(String param) { + return 'Krenite sa $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ugradite na Vaš sajt'; + + @override + String get studyReadMoreAboutEmbedding => 'Pročitajte više o ugrađivanju'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Samo javne studije mogu biti ugrađene!'; + + @override + String get studyOpen => 'Otvorite'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam je donio $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nije pronađena'; + + @override + String get studyEditChapter => 'Uredite poglavlje'; + + @override + String get studyNewChapter => 'Novo poglavlje'; + + @override + String studyImportFromChapterX(String param) { + return 'Uvezite iz $param'; + } + + @override + String get studyOrientation => 'Orijentacija'; + + @override + String get studyAnalysisMode => 'Tip analize'; + + @override + String get studyPinnedChapterComment => 'Stalni komentar poglavlja'; + + @override + String get studySaveChapter => 'Sačuvajte poglavlje'; + + @override + String get studyClearAnnotations => 'Izbrišite bilješke'; + + @override + String get studyClearVariations => 'Ukloni varijante'; + + @override + String get studyDeleteChapter => 'Izbrišite poglavlje'; + + @override + String get studyDeleteThisChapter => 'Da li želite izbrisati ovo poglavlje? Nakon ove akcije, poglavlje se ne može vratiti!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Da li želite izbrisati sve komentare, simbole i nacrtane oblike u ovom poglavlju?'; + + @override + String get studyRightUnderTheBoard => 'Odmah ispod ploče'; + + @override + String get studyNoPinnedComment => 'Nijedan'; + + @override + String get studyNormalAnalysis => 'Normalna analiza'; + + @override + String get studyHideNextMoves => 'Sakrijte sljedeće poteze'; + + @override + String get studyInteractiveLesson => 'Interaktivna lekcija'; + + @override + String studyChapterX(String param) { + return 'Poglavlje $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Krenite sa inicijalnom pozicijom'; + + @override + String get studyEditor => 'Uređivač'; + + @override + String get studyStartFromCustomPosition => 'Krenite sa željenom pozicijom'; + + @override + String get studyLoadAGameByUrl => 'Učitajte partiju pomoću linka'; + + @override + String get studyLoadAPositionFromFen => 'Učitajte partiju pomoću FEN koda'; + + @override + String get studyLoadAGameFromPgn => 'Učitajte partiju pomoću PGN formata'; + + @override + String get studyAutomatic => 'Automatska'; + + @override + String get studyUrlOfTheGame => 'Link partije'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Učitajte partiju sa $param1 ili $param2'; + } + + @override + String get studyCreateChapter => 'Kreirajte poglavlje'; + + @override + String get studyCreateStudy => 'Kreirajte studiju'; + + @override + String get studyEditStudy => 'Uredite studiju'; + + @override + String get studyVisibility => 'Vidljivost'; + + @override + String get studyPublic => 'Javna'; + + @override + String get studyUnlisted => 'Neizlistane'; + + @override + String get studyInviteOnly => 'Samo po pozivu'; + + @override + String get studyAllowCloning => 'Dozvolite kloniranje'; + + @override + String get studyNobody => 'Niko'; + + @override + String get studyOnlyMe => 'Samo ja'; + + @override + String get studyContributors => 'Saradnici'; + + @override + String get studyMembers => 'Članovi'; + + @override + String get studyEveryone => 'Svi'; + + @override + String get studyEnableSync => 'Omogućite sinhronizaciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: zadržite sve na istoj poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: Dozvolite ljudima da slobodno pregledaju'; + + @override + String get studyPinnedStudyComment => 'Stalni komentar studije'; + @override String get studyStart => 'Pokreni'; + + @override + String get studySave => 'Sačuvaj'; + + @override + String get studyClearChat => 'Izbrišite dopisivanje'; + + @override + String get studyDeleteTheStudyChatHistory => 'Da li želite izbrisati svo dopisivanje vezano za ovu studiju? Nakon ove akcije, obrisani tekst se ne može vratiti!'; + + @override + String get studyDeleteStudy => 'Izbrišite studiju'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdje želite da ovu poziciju prostudirate?'; + + @override + String get studyGoodMove => 'Dobar potez'; + + @override + String get studyMistake => 'Greška'; + + @override + String get studyBrilliantMove => 'Briljantan potez'; + + @override + String get studyBlunder => 'Grubi previd'; + + @override + String get studyInterestingMove => 'Zanimljiv potez'; + + @override + String get studyDubiousMove => 'Sumnjiv potez'; + + @override + String get studyOnlyMove => 'Jedini potez'; + + @override + String get studyZugzwang => 'Iznudica'; + + @override + String get studyEqualPosition => 'Jednaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bijeli je u blagoj prednosti'; + + @override + String get studyBlackIsSlightlyBetter => 'Crni je u blagoj prednosti'; + + @override + String get studyWhiteIsBetter => 'Bijeli je bolji'; + + @override + String get studyBlackIsBetter => 'Crni je bolji'; + + @override + String get studyWhiteIsWinning => 'Bijeli dobija'; + + @override + String get studyBlackIsWinning => 'Crni dobija'; + + @override + String get studyNovelty => 'Nov potez'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Inicijativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protivnapad'; + + @override + String get studyTimeTrouble => 'Cajtnot'; + + @override + String get studyWithCompensation => 'S kompenzacijom'; + + @override + String get studyWithTheIdea => 'S idejom'; + + @override + String get studyNextChapter => 'Sljedeće poglavlje'; + + @override + String get studyPrevChapter => 'Prethodno poglavlje'; + + @override + String get studyStudyActions => 'Opcije za studiju'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Popularne teme'; + + @override + String get studyManageTopics => 'Upravljajte temama'; + + @override + String get studyBack => 'Nazad'; + + @override + String get studyPlayAgain => 'Igrajte ponovo'; + + @override + String get studyWhatWouldYouPlay => 'Šta biste odigrali u ovoj poziciji?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitamo! Kompletirali ste ovu lekciju.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Poglavlja', + few: '$count Poglavlja', + one: '$count Poglavlje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partija', + few: '$count Partije', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članova', + few: '$count Člana', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ovdje zalijepite svoj PGN tekst, do $count partija', + few: 'Ovdje zalijepite svoj PGN tekst, do $count partije', + one: 'Ovdje zalijepite svoj PGN tekst, do $count partije', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'upravo sada'; + + @override + String get timeagoRightNow => 'upravo sada'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekundi', + few: 'za $count sekunde', + one: 'za $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minuta', + few: 'za $count minute', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sati', + few: 'za $count sata', + one: 'za $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dana', + few: 'za $count dana', + one: 'za $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sedmica', + few: 'za $count sedmice', + one: 'za $count sedmicu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count mjeseci', + few: 'za $count mjeseca', + one: 'za $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godina', + few: 'za $count godine', + one: 'za $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count minuta', + few: 'prije $count minute', + one: 'prije $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sati', + few: 'prije $count sata', + one: 'prije $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count dana', + few: 'prije $count dana', + one: 'prije $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sedmica', + few: 'prije $count sedmice', + one: 'prije $count sedmicu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count mjeseci', + few: 'prije $count mjeseca', + one: 'prije $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count godina', + few: 'prije $count godine', + one: 'prije $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index da53e6374b..8f6589e01b 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsCa extends AppLocalizations { AppLocalizationsCa([String locale = 'ca']) : super(locale); @override - String get mobileHomeTab => 'Inici'; + String get mobileAllGames => 'Totes les partides'; @override - String get mobilePuzzlesTab => 'Problemes'; + String get mobileAreYouSure => 'Estàs segur?'; @override - String get mobileToolsTab => 'Eines'; + String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; @override - String get mobileWatchTab => 'Visualitza'; + String get mobileClearButton => 'Neteja'; @override - String get mobileSettingsTab => 'Configuració'; + String get mobileCorrespondenceClearSavedMove => 'Elimina la jugada guardada'; @override - String get mobileMustBeLoggedIn => 'Has d\'estar connectat per veure aquesta pàgina.'; + String get mobileCustomGameJoinAGame => 'Unir-se a una partida'; @override - String get mobileSystemColors => 'Colors del sistema'; + String get mobileFeedbackButton => 'Suggeriments'; @override - String get mobileFeedbackButton => 'Suggeriments'; + String mobileGreeting(String param) { + return 'Hola, $param'; + } @override - String get mobileOkButton => 'D\'acord'; + String get mobileGreetingWithoutName => 'Hola'; @override - String get mobileSettingsHapticFeedback => 'Resposta hàptica'; + String get mobileHideVariation => 'Amaga les variacions'; @override - String get mobileSettingsImmersiveMode => 'Mode immersiu'; + String get mobileHomeTab => 'Inici'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Retransmissors en directe'; @override - String get mobileNotFollowingAnyUser => 'No estàs seguint a cap usuari.'; + String get mobileMustBeLoggedIn => 'Has d\'estar connectat per veure aquesta pàgina.'; @override - String get mobileAllGames => 'Totes les partides'; + String get mobileNoSearchResults => 'Sense resultats'; @override - String get mobileRecentSearches => 'Cerques recents'; + String get mobileNotFollowingAnyUser => 'No estàs seguint a cap usuari.'; @override - String get mobileClearButton => 'Neteja'; + String get mobileOkButton => 'D\'acord'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsCa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sense resultats'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Estàs segur?'; + String get mobilePuzzleStormConfirmEndRun => 'Voleu acabar aquesta ronda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perdreu la vostra ratxa i la vostra puntuació es guardarà.'; + String get mobilePuzzleStormFilterNothingToShow => 'Res a mostrar, si us plau canvieu els filtres'; @override String get mobilePuzzleStormNothingToShow => 'Res a mostrar. Fes algunes rondes al Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Comparteix aquest problema'; + String get mobilePuzzleStormSubtitle => 'Resoleu el màxim nombre de problemes en 3 minuts.'; @override - String get mobileShareGameURL => 'Comparteix l\'enllaç a la partida'; + String get mobilePuzzleStreakAbortWarning => 'Perdreu la vostra ratxa i la vostra puntuació es guardarà.'; @override - String get mobileShareGamePGN => 'Comparteix PGN'; + String get mobilePuzzleThemesSubtitle => 'Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.'; @override - String get mobileSharePositionAsFEN => 'Comparteix la posició com a FEN'; + String get mobilePuzzlesTab => 'Problemes'; @override - String get mobileShowVariations => 'Mostra les variacions'; + String get mobileRecentSearches => 'Cerques recents'; @override - String get mobileHideVariation => 'Amaga les variacions'; + String get mobileSettingsHapticFeedback => 'Resposta hàptica'; @override - String get mobileShowComments => 'Mostra comentaris'; + String get mobileSettingsImmersiveMode => 'Mode immersiu'; @override - String get mobilePuzzleStormConfirmEndRun => 'Voleu acabar aquesta ronda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Res a mostrar, si us plau canvieu els filtres'; + String get mobileSettingsTab => 'Configuració'; @override - String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; + String get mobileShareGamePGN => 'Comparteix PGN'; @override - String get mobileCancelDrawOffer => 'Anul·la la petició de taules'; + String get mobileShareGameURL => 'Comparteix l\'enllaç a la partida'; @override - String get mobileWaitingForOpponentToJoin => 'Esperant que s\'uneixi l\'adversari...'; + String get mobileSharePositionAsFEN => 'Comparteix la posició com a FEN'; @override - String get mobileBlindfoldMode => 'A la cega'; + String get mobileSharePuzzle => 'Comparteix aquest problema'; @override - String get mobileLiveStreamers => 'Retransmissors en directe'; + String get mobileShowComments => 'Mostra comentaris'; @override - String get mobileCustomGameJoinAGame => 'Unir-se a una partida'; + String get mobileShowResult => 'Mostra el resultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Elimina la jugada guardada'; + String get mobileShowVariations => 'Mostra les variacions'; @override String get mobileSomethingWentWrong => 'Alguna cosa ha anat malament.'; @override - String get mobileShowResult => 'Mostra el resultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.'; + String get mobileSystemColors => 'Colors del sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resoleu el màxim nombre de problemes en 3 minuts.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hola, $param'; - } + String get mobileToolsTab => 'Eines'; @override - String get mobileGreetingWithoutName => 'Hola'; + String get mobileWaitingForOpponentToJoin => 'Esperant que s\'uneixi l\'adversari...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Visualitza'; @override String get activityActivity => 'Activitat'; @@ -246,6 +243,17 @@ class AppLocalizationsCa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsCa extends AppLocalizations { @override String get broadcastBroadcasts => 'Retransmissions'; + @override + String get broadcastMyBroadcasts => 'Les meves retransmissions'; + @override String get broadcastLiveBroadcasts => 'Retransmissions de tornejos en directe'; + @override + String get broadcastBroadcastCalendar => 'Calendari de retransmissions'; + + @override + String get broadcastNewBroadcast => 'Nova retransmissió en directe'; + + @override + String get broadcastSubscribedBroadcasts => 'Emissions que segueixo'; + + @override + String get broadcastAboutBroadcasts => 'Sobre les retransmissions'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Com utilitzar les retransmissions de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nova ronda tindrà els mateixos membres i contribuïdors que l\'anterior.'; + + @override + String get broadcastAddRound => 'Afegir una ronda'; + + @override + String get broadcastOngoing => 'En curs'; + + @override + String get broadcastUpcoming => 'Properes'; + + @override + String get broadcastCompleted => 'Acabada'; + + @override + String get broadcastCompletedHelp => 'Lichess detecta el final de la ronda en funció de les partides de l\'origen. Utilitzeu aquesta opció si no hi ha origen.'; + + @override + String get broadcastRoundName => 'Nom de ronda'; + + @override + String get broadcastRoundNumber => 'Ronda número'; + + @override + String get broadcastTournamentName => 'Nom del torneig'; + + @override + String get broadcastTournamentDescription => 'Breu descripció del torneig'; + + @override + String get broadcastFullDescription => 'Descripció total de l\'esdeveniment'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opció de llarga descripció de l\'esdeveniment. $param1 és disponible. Ha de tenir menys de $param2 lletres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL origen del PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess comprovarà per a obtenir actualitzacions PGN. Ha de ser públicament accessible des d\'Internet.'; + + @override + String get broadcastSourceGameIds => 'Fins a 64 identificadors de partides de Lichess, separades per espais.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Dia d\'inici a la zona horari del torneig: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, si saps quan comença l\'esdeveniment'; + + @override + String get broadcastCurrentGameUrl => 'URL actual de joc'; + + @override + String get broadcastDownloadAllRounds => 'Baixa totes les rondes'; + + @override + String get broadcastResetRound => 'Restablir aquesta ronda'; + + @override + String get broadcastDeleteRound => 'Eliminar aquesta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivament la ronda i les seves partides.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar totes les partides d\'aquesta ronda. L\'origen ha d\'estar actiu per a recrear-les.'; + + @override + String get broadcastEditRoundStudy => 'Edita l\'estudi de la ronda'; + + @override + String get broadcastDeleteTournament => 'Elimina aquest torneig'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina el torneig de forma definitiva, amb totes les seves rondes i les seves partides.'; + + @override + String get broadcastShowScores => 'Mostra les puntuacions dels jugadors en funció dels resultats de les partides'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: Reemplaça noms dels jugadors, puntuacions i títols'; + + @override + String get broadcastFideFederations => 'Federacions FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 Ràting'; + + @override + String get broadcastFidePlayers => 'Jugadors FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'No s\'ha trobat el jugador FIDE'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federació'; + + @override + String get broadcastAgeThisYear => 'Edat aquest any'; + + @override + String get broadcastUnrated => 'Sense avaluació'; + + @override + String get broadcastRecentTournaments => 'Tornejos recents'; + + @override + String get broadcastOpenLichess => 'Obre a Lichess'; + + @override + String get broadcastTeams => 'Equips'; + + @override + String get broadcastBoards => 'Taulers'; + + @override + String get broadcastOverview => 'Visió general'; + + @override + String get broadcastSubscribeTitle => 'Subscriviu-vos per ser notificats quan comença cada ronda. Podeu activar/desactivara la campana o modificar les notificacions push a les preferències del vostre compte.'; + + @override + String get broadcastUploadImage => 'Puja una imatge del torneig'; + + @override + String get broadcastNoBoardsYet => 'Encara no hi ha taulers. Apareixeran en el moment que es carreguin les partides.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Els taulers es poden carregar per codi o a través de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Començar a les $param'; + } + + @override + String get broadcastStartVerySoon => 'La retransmissió començarà aviat.'; + + @override + String get broadcastNotYetStarted => 'La retransmissió encara no ha començat.'; + + @override + String get broadcastOfficialWebsite => 'Lloc web oficial'; + + @override + String get broadcastStandings => 'Classificació'; + + @override + String get broadcastOfficialStandings => 'Classificació oficial'; + + @override + String broadcastIframeHelp(String param) { + return 'Més opcions a la $param'; + } + + @override + String get broadcastWebmastersPage => 'pàgina d\'administració'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Un origen públic en PGN públic en temps real d\'aquesta ronda. També oferim un $param per una sincronització més ràpida i eficient.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incrusta aquesta retransmissió al vostre lloc web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incrusta $param al vostre lloc web'; + } + + @override + String get broadcastRatingDiff => 'Diferència puntuació'; + + @override + String get broadcastGamesThisTournament => 'Partides en aquest torneig'; + + @override + String get broadcastScore => 'Puntuació'; + + @override + String get broadcastAllTeams => 'Tots els equips'; + + @override + String get broadcastTournamentFormat => 'Format del torneig'; + + @override + String get broadcastTournamentLocation => 'Ubicació del torneig'; + + @override + String get broadcastTopPlayers => 'Millors jugadors'; + + @override + String get broadcastTimezone => 'Zona horària'; + + @override + String get broadcastFideRatingCategory => 'Categoria puntuació FIDE'; + + @override + String get broadcastOptionalDetails => 'Detalls opcionals'; + + @override + String get broadcastPastBroadcasts => 'Retransmissions finalitzades'; + + @override + String get broadcastAllBroadcastsByMonth => 'Veure totes les retransmissions per més'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count retransmissions', + one: '$count retransmissió', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafiaments: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get preferencesInGameOnly => 'Només durant la partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Rellotge d\'escacs'; @@ -750,6 +1008,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'So de notificació'; + @override + String get preferencesBlindfold => 'A la cega'; + @override String get puzzlePuzzles => 'Problemes'; @@ -1390,10 +1651,10 @@ class AppLocalizationsCa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'El rival té els moviments limitats i cada jugada empitjora la seva posició.'; @override - String get puzzleThemeHealthyMix => 'Una mica de cada'; + String get puzzleThemeMix => 'Una mica de cada'; @override - String get puzzleThemeHealthyMixDescription => 'Una mica de tot. No sabràs el que t\'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.'; + String get puzzleThemeMixDescription => 'Una mica de tot. No sabràs el que t\'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.'; @override String get puzzleThemePlayerGames => 'Partides de jugadors'; @@ -1767,9 +2028,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get byCPL => 'Per CPL'; - @override - String get openStudy => 'Obrir estudi'; - @override String get enable => 'Habilitar'; @@ -1797,9 +2055,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get removesTheDepthLimit => 'Treu el límit de profunditat i escalfa el teu ordinador'; - @override - String get engineManager => 'Gestió del mòdul'; - @override String get blunder => 'Errada greu'; @@ -2063,6 +2318,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get gamesPlayed => 'Partides jugades'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancel·lar'; @@ -2437,9 +2695,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get unblock => 'Desbloqueja'; - @override - String get followsYou => 'T\'està seguint'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ha començat a seguir $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsCa extends AppLocalizations { String get other => 'Altres'; @override - String get reportDescriptionHelp => 'Enganxa l\'enllaç de la partida (o partides) i explica el comportament negatiu d\'aquest usuari. No et limitis a dir que \"fa trampes\", i explica com has arribat a aquesta conclusió. El teu informe serà processat més ràpidament si l\'escrius en anglès.'; + String get reportCheatBoostHelp => 'Enganxa l\'enllaç de la partida (o partides) i explica quin és el problema amb el comportament d\'aquest usuari. No et limitis a dir que \"fa trampes\", explica\'ns com has arribat a aquesta conclusió.'; + + @override + String get reportUsernameHelp => 'Explica quin és el comportament ofensiu d\'aquest usuari. No et limitis a dir simplement \"és ofensiu/inapropiat\", explica\'ns com has arribat a aquesta conclusió, especialment si l\'insult està ofuscat, en un idioma diferent de l\'anglès, és un barbarisme o és una referència històrica o cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'El teu informe serà tractat més ràpidament si està escrit en anglès.'; @override String get error_provideOneCheatedGameLink => 'Si us plau, proporcioneu com a mínim un enllaç a un joc on s\'han fet trampes.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get nothingToSeeHere => 'Res a veure per aquí de moment.'; + @override + String get stats => 'Estadístiques'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsCa extends AppLocalizations { @override String get streamerLichessStreamers => 'Retransmissors de Lichess'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Els meus estudis'; + + @override + String get studyStudiesIContributeTo => 'Estudis on jo hi contribueixo'; + + @override + String get studyMyPublicStudies => 'Els meus estudis públics'; + + @override + String get studyMyPrivateStudies => 'Els meus estudis privats'; + + @override + String get studyMyFavoriteStudies => 'Els meus estudis favorits'; + + @override + String get studyWhatAreStudies => 'Què són els estudis?'; + + @override + String get studyAllStudies => 'Tots els estudis'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudis creats per $param'; + } + + @override + String get studyNoneYet => 'Res encara.'; + + @override + String get studyHot => 'Candent'; + + @override + String get studyDateAddedNewest => 'Data d’inclusió (més nous)'; + + @override + String get studyDateAddedOldest => 'Data d’inclusió (més antics)'; + + @override + String get studyRecentlyUpdated => 'Actualitzat darrerament'; + + @override + String get studyMostPopular => 'Més popular'; + + @override + String get studyAlphabetical => 'Alfabètic'; + + @override + String get studyAddNewChapter => 'Afegir un nou capítol'; + + @override + String get studyAddMembers => 'Afegeix membres'; + + @override + String get studyInviteToTheStudy => 'Convida a l’estudi'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Si us plau, convida gent que coneixes, i que vólen unir-se activament a l’estudi.'; + + @override + String get studySearchByUsername => 'Cerca per nom d\'usuari'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Contribuïdor'; + + @override + String get studyKick => 'Expulsa'; + + @override + String get studyLeaveTheStudy => 'Deixar l’estudi'; + + @override + String get studyYouAreNowAContributor => 'Ara ets un contribuïdor'; + + @override + String get studyYouAreNowASpectator => 'Actualment ets un espectador'; + + @override + String get studyPgnTags => 'Etiquetes PGN'; + + @override + String get studyLike => 'M’agrada'; + + @override + String get studyUnlike => 'Ja no m\'agrada'; + + @override + String get studyNewTag => 'Nova etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar en aquesta posició'; + + @override + String get studyCommentThisMove => 'Comentar en aquest moviment'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar amb signes'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'El capítol és massa curt per ser analitzat.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Només els contribuïdors de l’estudi poden demanar un anàlisis computeritzat.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obté un anàlisi complert desde el servidor de la línia principal.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Segura’t que el capítol és complert. Només pots requerir l’anàlisi una sola vegada.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tots els membres sincronitzats es mantenen a la mateixa posició'; + + @override + String get studyShareChanges => 'Comparteix els canvis amb els espectadors i guarda’ls al servidor'; + + @override + String get studyPlaying => 'Jugant'; + + @override + String get studyShowEvalBar => 'Barres d\'avaluació'; + + @override + String get studyFirst => 'Primer'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Següent'; + + @override + String get studyLast => 'Últim'; + @override String get studyShareAndExport => 'Comparteix i exporta'; + @override + String get studyCloneStudy => 'Clona'; + + @override + String get studyStudyPgn => 'PGN de l’estudi'; + + @override + String get studyDownloadAllGames => 'Descarrega tots els jocs'; + + @override + String get studyChapterPgn => 'PGN del capítol'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descarrega partida'; + + @override + String get studyStudyUrl => 'URL de l’estudi'; + + @override + String get studyCurrentChapterUrl => 'URL del capítol actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Pots enganxar això en el forum per insertar'; + + @override + String get studyStartAtInitialPosition => 'Comnçar a la posició inicial'; + + @override + String studyStartAtX(String param) { + return 'Començar a $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Inserta en la teva web o blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Llegeix més sobre insertar'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Només els estudis públics poden ser inserits!'; + + @override + String get studyOpen => 'Obrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, presentat per $param2'; + } + + @override + String get studyStudyNotFound => 'Estudi no trobat'; + + @override + String get studyEditChapter => 'Editar capítol'; + + @override + String get studyNewChapter => 'Nou capítol'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientaciò'; + + @override + String get studyAnalysisMode => 'Mode d\'anàlisi'; + + @override + String get studyPinnedChapterComment => 'Comentari del capítol fixat'; + + @override + String get studySaveChapter => 'Guarda el capítol'; + + @override + String get studyClearAnnotations => 'Netejar anotacions'; + + @override + String get studyClearVariations => 'Netejar variacions'; + + @override + String get studyDeleteChapter => 'Eliminar capítol'; + + @override + String get studyDeleteThisChapter => 'Eliminar aquest capítol? No hi ha volta enrera!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Esborrar tots els comentaris, signes i marques en aquest capítol?'; + + @override + String get studyRightUnderTheBoard => 'Just a sota el tauler'; + + @override + String get studyNoPinnedComment => 'Cap'; + + @override + String get studyNormalAnalysis => 'Análisis normal'; + + @override + String get studyHideNextMoves => 'Oculta els següents moviments'; + + @override + String get studyInteractiveLesson => 'Lliçó interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítol $param'; + } + + @override + String get studyEmpty => 'Buit'; + + @override + String get studyStartFromInitialPosition => 'Començar a la posició inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Començar a una posició personalitzada'; + + @override + String get studyLoadAGameByUrl => 'Carregar una partida desde una URL'; + + @override + String get studyLoadAPositionFromFen => 'Carregar una posició via codi FEN'; + + @override + String get studyLoadAGameFromPgn => 'Carregar una partida PGN'; + + @override + String get studyAutomatic => 'Automàtic'; + + @override + String get studyUrlOfTheGame => 'URL del joc'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar una partida desde $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítol'; + + @override + String get studyCreateStudy => 'Crear estudi'; + + @override + String get studyEditStudy => 'Editar estudi'; + + @override + String get studyVisibility => 'Visibilitat'; + + @override + String get studyPublic => 'Públic'; + + @override + String get studyUnlisted => 'No llistats'; + + @override + String get studyInviteOnly => 'Només per invitació'; + + @override + String get studyAllowCloning => 'Permitir clonat'; + + @override + String get studyNobody => 'Ningú'; + + @override + String get studyOnlyMe => 'Només jo'; + + @override + String get studyContributors => 'Col·laboradors'; + + @override + String get studyMembers => 'Membres'; + + @override + String get studyEveryone => 'Tothom'; + + @override + String get studyEnableSync => 'Habilita la sincronització'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sí: tothom veu la mateixa posició'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: permetre que la gent navegui lliurement'; + + @override + String get studyPinnedStudyComment => 'Comentar estudi fixat'; + @override String get studyStart => 'Inici'; + + @override + String get studySave => 'Desa'; + + @override + String get studyClearChat => 'Neteja el Chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Eliminar el xat de l’estudi? No hi ha volta enrera!'; + + @override + String get studyDeleteStudy => 'Eliminar estudi'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Esteu segurs que voleu eliminar el estudi? Tingues en compte que no es pot desfer. Per a confirmar-ho escriu el nom del estudi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'A on vols estudiar-ho?'; + + @override + String get studyGoodMove => 'Bona jugada'; + + @override + String get studyMistake => 'Errada'; + + @override + String get studyBrilliantMove => 'Jugada brillant'; + + @override + String get studyBlunder => 'Error greu'; + + @override + String get studyInterestingMove => 'Jugada interessant'; + + @override + String get studyDubiousMove => 'Jugada dubtosa'; + + @override + String get studyOnlyMove => 'Única jugada'; + + @override + String get studyZugzwang => 'Zugzwang (atzucac)'; + + @override + String get studyEqualPosition => 'Posició igualada'; + + @override + String get studyUnclearPosition => 'Posició poc clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'El blanc està lleugerament millor'; + + @override + String get studyBlackIsSlightlyBetter => 'El negre està lleugerament millor'; + + @override + String get studyWhiteIsBetter => 'El blanc està millor'; + + @override + String get studyBlackIsBetter => 'El negre està millor'; + + @override + String get studyWhiteIsWinning => 'El blanc està guanyant'; + + @override + String get studyBlackIsWinning => 'El negre està guanyant'; + + @override + String get studyNovelty => 'Novetat'; + + @override + String get studyDevelopment => 'Desenvolupament'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Atac'; + + @override + String get studyCounterplay => 'Contra atac'; + + @override + String get studyTimeTrouble => 'Problema de temps'; + + @override + String get studyWithCompensation => 'Amb compensació'; + + @override + String get studyWithTheIdea => 'Amb la idea'; + + @override + String get studyNextChapter => 'Capítol següent'; + + @override + String get studyPrevChapter => 'Capítol Anterior'; + + @override + String get studyStudyActions => 'Acions de l\'estudi'; + + @override + String get studyTopics => 'Temes'; + + @override + String get studyMyTopics => 'Els meus temes'; + + @override + String get studyPopularTopics => 'Temes populars'; + + @override + String get studyManageTopics => 'Gestiona els temes'; + + @override + String get studyBack => 'Enrere'; + + @override + String get studyPlayAgain => 'Torna a jugar'; + + @override + String get studyWhatWouldYouPlay => 'Que jugaríeu en aquesta posició?'; + + @override + String get studyYouCompletedThisLesson => 'Enhorabona, heu completat aquesta lliçó.'; + + @override + String studyPerPage(String param) { + return '$param per pàgina'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítols', + one: '$count Capítol', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Jocs', + one: '$count Joc', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membres', + one: '$count Membre', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Enganxa el teu PGN aquí, fins a $count partides', + one: 'Enganxa el teu PGN aquí, fins a $count partida', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'ara mateix'; + + @override + String get timeagoRightNow => 'ara mateix'; + + @override + String get timeagoCompleted => 'completat'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segons', + one: 'en $count segon', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minuts', + one: 'en $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count hores', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count dies', + one: 'en $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count setmanes', + one: 'en $count setmana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count mesos', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count anys', + one: 'en $count any', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count minuts', + one: 'fa $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count hores', + one: 'fa $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count dies', + one: 'fa $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count setmanes', + one: 'fa $count setmana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count mesos', + one: 'fa $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count anys', + one: 'fa $count any', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Queden $count minuts', + one: 'Queda $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Queden $count hores', + one: 'Queda $count hora', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index eaef3e5109..2ea7f09df6 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsCs extends AppLocalizations { AppLocalizationsCs([String locale = 'cs']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Jste si jistý?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Zrušit nabídnutí vrácení tahu'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Vymazat'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Vymazat uložené tahy'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Připojit se ke hře'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Ahoj, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Ahoj'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Schovej variace'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Živé vysílání'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'Žádné výsledky'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Hráči s \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; - - @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormConfirmEndRun => 'Chceš ukončit tento běh?'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nic k zobrazení, prosím změn filtry'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormNothingToShow => 'Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Vyřeš co nejvíce úloh co dokážeš za 3 minuty.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'Ztratíte aktuální sérii a vaše skóre bude uloženo.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Hraj úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGamePGN => 'Sdílet PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Sdílet URL hry'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Sdílet pozici jako FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Sdílej tuto úlohu'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Zobraz komentáře'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Zobrazit výsledky'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Zobraz variace'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Něco se pokazilo.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Čeká se na připojení protihráče...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivita'; @@ -262,6 +259,19 @@ class AppLocalizationsCs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dokončeno $count $param2 korespondenčních partii', + many: 'Dokončeno $count $param2 korespondenčních partii', + few: 'Dokončeny $count $param2 korespondenční partie', + one: 'Dokončena $count $param2 korespondenční partie', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsCs extends AppLocalizations { @override String get broadcastBroadcasts => 'Přenosy'; + @override + String get broadcastMyBroadcasts => 'Moje vysílání'; + @override String get broadcastLiveBroadcasts => 'Živé přenosy turnajů'; + @override + String get broadcastBroadcastCalendar => 'Kalendář přenosů'; + + @override + String get broadcastNewBroadcast => 'Nový živý přenos'; + + @override + String get broadcastSubscribedBroadcasts => 'Odebírané přenosy'; + + @override + String get broadcastAboutBroadcasts => 'O vysílání'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Jak používat Lichess vysílání.'; + + @override + String get broadcastTheNewRoundHelp => 'Nové kolo bude mít stejné členy a přispěvatele jako to předchozí.'; + + @override + String get broadcastAddRound => 'Přidat kolo'; + + @override + String get broadcastOngoing => 'Probíhající'; + + @override + String get broadcastUpcoming => 'Chystané'; + + @override + String get broadcastCompleted => 'Dokončené'; + + @override + String get broadcastCompletedHelp => 'Lichess detekuje dokončení kola na základě zdrojových her. Tento přepínač použijte, pokud není k dispozici žádný zdroj.'; + + @override + String get broadcastRoundName => 'Číslo kola'; + + @override + String get broadcastRoundNumber => 'Číslo kola'; + + @override + String get broadcastTournamentName => 'Název turnaje'; + + @override + String get broadcastTournamentDescription => 'Stručný popis turnaje'; + + @override + String get broadcastFullDescription => 'Úplný popis události'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Volitelný dlouhý popis přenosu. $param1 je k dispozici. Délka musí být menší než $param2 znaků.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Zdrojová URL adresa'; + + @override + String get broadcastSourceUrlHelp => 'URL adresa, kterou bude Lichess kontrolovat pro získání PGN aktualizací. Musí být veřejně přístupná z internetu.'; + + @override + String get broadcastSourceGameIds => 'Až 64 ID Lichess her, oddělených mezerama.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Datum zahájení v lokálním čase turnaje: $param'; + } + + @override + String get broadcastStartDateHelp => 'Nepovinné, pokud víte, kdy událost začíná'; + + @override + String get broadcastCurrentGameUrl => 'URL adresa právě probíhající partie'; + + @override + String get broadcastDownloadAllRounds => 'Stáhnout hry ze všech kol'; + + @override + String get broadcastResetRound => 'Resetovat toto kolo'; + + @override + String get broadcastDeleteRound => 'Smazat toto kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivně smazat kolo a jeho hry.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Smazat všechny hry v tomto kole. Zdroj musí být aktivní aby bylo možno je znovu vytvořit.'; + + @override + String get broadcastEditRoundStudy => 'Upravit studie kola'; + + @override + String get broadcastDeleteTournament => 'Smazat tento turnaj'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Opravdu smazat celý turnaj, všechna kola a hry.'; + + @override + String get broadcastShowScores => 'Zobraz skóre hráču dle herních výsledků'; + + @override + String get broadcastReplacePlayerTags => 'Volitelné: nahraď jména hráčů, rating a tituly'; + + @override + String get broadcastFideFederations => 'FIDE federace'; + + @override + String get broadcastTop10Rating => 'Rating top 10'; + + @override + String get broadcastFidePlayers => 'FIDE hráči'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE hráč nenalezen'; + + @override + String get broadcastFideProfile => 'FIDE profil'; + + @override + String get broadcastFederation => 'Federace'; + + @override + String get broadcastAgeThisYear => 'Věk tento rok'; + + @override + String get broadcastUnrated => 'Nehodnocen'; + + @override + String get broadcastRecentTournaments => 'Nedávné tournamenty'; + + @override + String get broadcastOpenLichess => 'Otevřít v Lichess'; + + @override + String get broadcastTeams => 'Týmy'; + + @override + String get broadcastBoards => 'Šachovnice'; + + @override + String get broadcastOverview => 'Přehled'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Nahrát obrázek turnaje'; + + @override + String get broadcastNoBoardsYet => 'Zatím žádné šachovnice. Ty se zobrazí se po nahrání partií.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Začíná po $param'; + } + + @override + String get broadcastStartVerySoon => 'Vysílání začne velmi brzy.'; + + @override + String get broadcastNotYetStarted => 'Vysílání ještě nezačalo.'; + + @override + String get broadcastOfficialWebsite => 'Oficiální stránka'; + + @override + String get broadcastStandings => 'Pořadí'; + + @override + String get broadcastOfficialStandings => 'Oficiální pořadí'; + + @override + String broadcastIframeHelp(String param) { + return 'Více možností na $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Skóre'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vysílání', + many: '$count vysílání', + few: '$count vysílání', + one: '$count vysílání', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Výzvy: $param1'; @@ -643,6 +902,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get preferencesInGameOnly => 'Pouze u partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šachové hodiny'; @@ -784,6 +1046,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Typ zvukového upozornění'; + @override + String get preferencesBlindfold => 'Páska přes oči'; + @override String get puzzlePuzzles => 'Úlohy'; @@ -1434,10 +1699,10 @@ class AppLocalizationsCs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Soupeř musí zahrát jakýkoliv tah, přičemž všechny zhoršují jeho pozici a zlepšují naší pozici.'; @override - String get puzzleThemeHealthyMix => 'Mix úloh'; + String get puzzleThemeMix => 'Mix úloh'; @override - String get puzzleThemeHealthyMixDescription => 'Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.'; + String get puzzleThemeMixDescription => 'Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.'; @override String get puzzleThemePlayerGames => 'Z vašich her'; @@ -1680,10 +1945,10 @@ class AppLocalizationsCs extends AppLocalizations { String get deleteFromHere => 'Smazat odsud'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Schovat variace'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Zobrazit variace'; @override String get forceVariation => 'Zobrazit jako variantu'; @@ -1811,9 +2076,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get byCPL => 'Dle CPL'; - @override - String get openStudy => 'Otevřít studii'; - @override String get enable => 'Povolit analýzu'; @@ -1841,9 +2103,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get removesTheDepthLimit => 'Zapne nekonečnou analýzu a odstraní omezení hloubky propočtu'; - @override - String get engineManager => 'Správce enginu'; - @override String get blunder => 'Hrubá chyba'; @@ -1922,7 +2181,7 @@ class AppLocalizationsCs extends AppLocalizations { String get friends => 'Přátelé'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'ostatní hráči'; @override String get discussions => 'Konverzace'; @@ -2107,6 +2366,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get gamesPlayed => 'Odehraných partií'; + @override + String get ok => 'OK'; + @override String get cancel => 'Zrušit'; @@ -2481,9 +2743,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get unblock => 'Odblokovat'; - @override - String get followsYou => 'Vás sleduje'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 začal sledovat $param2'; @@ -2675,16 +2934,16 @@ class AppLocalizationsCs extends AppLocalizations { String get editProfile => 'Upravit profil'; @override - String get realName => 'Real name'; + String get realName => 'Skutečné jméno'; @override String get setFlair => 'Nastav si svou ikonu za jménem'; @override - String get flair => 'Upravitelná ikona'; + String get flair => 'Ikona'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => 'Existuje nastavení které schová všechny uživatelské ikony za jménem po celém webu.'; @override String get biography => 'O mně'; @@ -2735,7 +2994,7 @@ class AppLocalizationsCs extends AppLocalizations { String get puzzles => 'Puzzle'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Online roboti'; @override String get name => 'Jméno'; @@ -2756,10 +3015,10 @@ class AppLocalizationsCs extends AppLocalizations { String get yes => 'Ano'; @override - String get website => 'Website'; + String get website => 'Web'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Nápověda:'; @@ -2816,7 +3075,13 @@ class AppLocalizationsCs extends AppLocalizations { String get other => 'Jiné'; @override - String get reportDescriptionHelp => 'Vložte link na hru(y) a popište, co je špatně na chování tohoto hráče. (Pokud možno anglicky.)'; + String get reportCheatBoostHelp => 'Zde vlož odkaz na hru(hry) a napiš co dělal tento uživatel. Nepiš pouze \"on podváděl\", ale napiš proč si myslíš že podváděl.'; + + @override + String get reportUsernameHelp => 'Vysvětli co je urážlivého na jeho u6ivatelském jménu. Nepiš pouze \"Je urážlivé/nevhodné\", ale řekni i důvod proč to tak je, zejména pokud je urážka zatemněná, nebo je v jiném jazyce než v angličtině, nebo je ve slangu či jde o historickou nebokulturní referenci.'; + + @override + String get reportProcessedFasterInEnglish => 'Nahlášení bude rychlejší pokud bude v angličtině.'; @override String get error_provideOneCheatedGameLink => 'Prosím, uveďte alespoň jeden link na partii, ve které se podvádělo.'; @@ -2919,7 +3184,7 @@ class AppLocalizationsCs extends AppLocalizations { String get outsideTheBoard => 'Mimo šachovnici'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Všechny pole na šachovnici'; @override String get onSlowGames => 'Při pomalých hrách'; @@ -3133,7 +3398,7 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get yourPendingSimuls => 'Your pending simuls'; + String get yourPendingSimuls => 'Tvoje simulace ve frontě'; @override String get createdSimuls => 'Nově vytvořené simultánky'; @@ -3142,7 +3407,7 @@ class AppLocalizationsCs extends AppLocalizations { String get hostANewSimul => 'Vytvoř novou simultánku'; @override - String get signUpToHostOrJoinASimul => 'Sign up to host or join a simul'; + String get signUpToHostOrJoinASimul => 'Zaregistruj se abys mohl založit nebo se připojit k simulaci'; @override String get noSimulFound => 'Simultánka nenalezena'; @@ -3217,7 +3482,7 @@ class AppLocalizationsCs extends AppLocalizations { String get keyGoToStartOrEnd => 'běžte na začátek/konec'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get keyCycleSelectedVariation => 'Projdi zkrze vybranou variaci'; @override String get keyShowOrHideComments => 'zobrazte/skryjte komentáře'; @@ -3241,22 +3506,22 @@ class AppLocalizationsCs extends AppLocalizations { String get keyNextInaccuracy => 'Další nepřesnost'; @override - String get keyPreviousBranch => 'Previous branch'; + String get keyPreviousBranch => 'Předchozí větev'; @override - String get keyNextBranch => 'Next branch'; + String get keyNextBranch => 'Další větev'; @override String get toggleVariationArrows => 'Přepnout šipky variant'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => 'Projdi předchozí/následující variantu'; @override String get toggleGlyphAnnotations => 'Přepnout poznámky glyfů'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => 'Přepni zvýraznění pozice'; @override String get variationArrowsInfo => 'Šipky variant umožňují navigaci bez použití seznamu tahů.'; @@ -3515,22 +3780,22 @@ class AppLocalizationsCs extends AppLocalizations { String get backgroundImageUrl => 'URL zdroj obrázku na pozadí:'; @override - String get board => 'Board'; + String get board => 'Šachovnice'; @override - String get size => 'Size'; + String get size => 'Velikost'; @override - String get opacity => 'Opacity'; + String get opacity => 'Průhlednost'; @override - String get brightness => 'Brightness'; + String get brightness => 'Jas'; @override String get hue => 'Hue'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Vrátit barvy na původní nastavení'; @override String get pieceSet => 'Vzhled figur'; @@ -4119,7 +4384,10 @@ class AppLocalizationsCs extends AppLocalizations { String get lichessPatronInfo => 'Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem.\nVeškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Momentálně zde není nic k vidění.'; + + @override + String get stats => 'Statistiky'; @override String opponentLeftCounter(int count) { @@ -4856,8 +5124,730 @@ class AppLocalizationsCs extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streameři'; @override - String get studyShareAndExport => 'Sdílení a export'; + String get studyPrivate => 'Soukromé'; @override - String get studyStart => 'Začít'; + String get studyMyStudies => 'Moje studie'; + + @override + String get studyStudiesIContributeTo => 'Studie, ke kterým přispívám'; + + @override + String get studyMyPublicStudies => 'Moje veřejné studie'; + + @override + String get studyMyPrivateStudies => 'Moje soukromé studie'; + + @override + String get studyMyFavoriteStudies => 'Moje oblíbené studie'; + + @override + String get studyWhatAreStudies => 'Co jsou studie?'; + + @override + String get studyAllStudies => 'Všechny studie'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studie vytvořené hráčem $param'; + } + + @override + String get studyNoneYet => 'Zatím nic.'; + + @override + String get studyHot => 'Oblíbené'; + + @override + String get studyDateAddedNewest => 'Datum přidání (nejnovější)'; + + @override + String get studyDateAddedOldest => 'Datum přidání (nejstarší)'; + + @override + String get studyRecentlyUpdated => 'Nedávno aktualizované'; + + @override + String get studyMostPopular => 'Nejoblíbenější'; + + @override + String get studyAlphabetical => 'Abecedně'; + + @override + String get studyAddNewChapter => 'Přidat novou kapitolu'; + + @override + String get studyAddMembers => 'Přidat uživatele'; + + @override + String get studyInviteToTheStudy => 'Pozvat do studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosím zvěte pouze lidi, které znáte a kteří se chtějí aktivně připojit k této studii.'; + + @override + String get studySearchByUsername => 'Hledat podle uživatelského jména'; + + @override + String get studySpectator => 'Divák'; + + @override + String get studyContributor => 'Přispívající'; + + @override + String get studyKick => 'Vyhodit'; + + @override + String get studyLeaveTheStudy => 'Opustit studii'; + + @override + String get studyYouAreNowAContributor => 'Nyní jste přispívající'; + + @override + String get studyYouAreNowASpectator => 'Nyní jste divák'; + + @override + String get studyPgnTags => 'PGN tagy'; + + @override + String get studyLike => 'To se mi líbí'; + + @override + String get studyUnlike => 'Už se mi nelíbí'; + + @override + String get studyNewTag => 'Nový štítek'; + + @override + String get studyCommentThisPosition => 'Komentář k tomuto příspěvku'; + + @override + String get studyCommentThisMove => 'Komentář k tomuto tahu'; + + @override + String get studyAnnotateWithGlyphs => 'Popsat glyfy'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitola je moc krátká na to, aby mohla být zanalyzována.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Pouze přispěvatelé mohou požádat o počítačovou analýzu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Získejte plnou počítačovou analýzu hlavní varianty.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ujistěte se, že je kapitola úplná. O analýzu můžete požádat pouze jednou.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Všichni SYNCHRONIZOVANÍ členové zůstávají na stejné pozici'; + + @override + String get studyShareChanges => 'Sdílet změny s diváky a uložit je na server'; + + @override + String get studyPlaying => 'Probíhající'; + + @override + String get studyShowEvalBar => 'Lišta hodnotící pozici'; + + @override + String get studyFirst => 'První'; + + @override + String get studyPrevious => 'Předchozí'; + + @override + String get studyNext => 'Další'; + + @override + String get studyLast => 'Poslední'; + + @override + String get studyShareAndExport => 'Sdílení a export'; + + @override + String get studyCloneStudy => 'Klonovat'; + + @override + String get studyStudyPgn => 'PGN studie'; + + @override + String get studyDownloadAllGames => 'Stáhnout všechny hry'; + + @override + String get studyChapterPgn => 'PGN kapitoly'; + + @override + String get studyCopyChapterPgn => 'Kopírovat PGN'; + + @override + String get studyDownloadGame => 'Stáhnout hru'; + + @override + String get studyStudyUrl => 'URL studie'; + + @override + String get studyCurrentChapterUrl => 'URL aktuální kapitoly'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Tento odkaz můžete vložit např. do diskusního fóra'; + + @override + String get studyStartAtInitialPosition => 'Začít ve výchozí pozici'; + + @override + String studyStartAtX(String param) { + return 'Začít u tahu $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vložte vaší stránku nebo blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Přečtěte si více o vkládání'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Lze vložit pouze veřejné studie!'; + + @override + String get studyOpen => 'Otevřít'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vám přináší $param2'; + } + + @override + String get studyStudyNotFound => 'Studie nenalezena'; + + @override + String get studyEditChapter => 'Upravit kapitolu'; + + @override + String get studyNewChapter => 'Nová kapitola'; + + @override + String studyImportFromChapterX(String param) { + return 'Importovat z $param'; + } + + @override + String get studyOrientation => 'Orientace'; + + @override + String get studyAnalysisMode => 'Režim rozboru'; + + @override + String get studyPinnedChapterComment => 'Připnutý komentář u kapitoly'; + + @override + String get studySaveChapter => 'Uložit kapitolu'; + + @override + String get studyClearAnnotations => 'Vymazat anotace'; + + @override + String get studyClearVariations => 'Vymazat varianty'; + + @override + String get studyDeleteChapter => 'Odstranit kapitolu'; + + @override + String get studyDeleteThisChapter => 'Opravdu chcete odstranit tuto kapitolu? Kapitola bude navždy ztracena!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vymazat všechny komentáře a výtvory v této kapitole?'; + + @override + String get studyRightUnderTheBoard => 'Přímo pod šachovnicí'; + + @override + String get studyNoPinnedComment => 'Žádný'; + + @override + String get studyNormalAnalysis => 'Normální rozbor'; + + @override + String get studyHideNextMoves => 'Skrýt následující tahy'; + + @override + String get studyInteractiveLesson => 'Interaktivní lekce'; + + @override + String studyChapterX(String param) { + return 'Kapitola: $param'; + } + + @override + String get studyEmpty => 'Prázdné'; + + @override + String get studyStartFromInitialPosition => 'Začít z původní pozice'; + + @override + String get studyEditor => 'Tvůrce'; + + @override + String get studyStartFromCustomPosition => 'Začít od vlastní pozice'; + + @override + String get studyLoadAGameByUrl => 'Načíst hru podle URL'; + + @override + String get studyLoadAPositionFromFen => 'Načíst polohu z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Načíst hru z PGN'; + + @override + String get studyAutomatic => 'Automatický'; + + @override + String get studyUrlOfTheGame => 'URL hry'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Načíst hru z $param1 nebo $param2'; + } + + @override + String get studyCreateChapter => 'Vytvořit kapitolu'; + + @override + String get studyCreateStudy => 'Vytvořit studii'; + + @override + String get studyEditStudy => 'Upravit studii'; + + @override + String get studyVisibility => 'Viditelnost'; + + @override + String get studyPublic => 'Veřejná'; + + @override + String get studyUnlisted => 'Neveřejná'; + + @override + String get studyInviteOnly => 'Pouze na pozvání'; + + @override + String get studyAllowCloning => 'Povolit klonování'; + + @override + String get studyNobody => 'Nikdo'; + + @override + String get studyOnlyMe => 'Pouze já'; + + @override + String get studyContributors => 'Přispěvatelé'; + + @override + String get studyMembers => 'Členové'; + + @override + String get studyEveryone => 'Kdokoli'; + + @override + String get studyEnableSync => 'Povolit synchronizaci'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ano, všichni zůstávají na stejné pozici'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne, umožnit volné procházení'; + + @override + String get studyPinnedStudyComment => 'Připnutý komentář studie'; + + @override + String get studyStart => 'Začít'; + + @override + String get studySave => 'Uložit'; + + @override + String get studyClearChat => 'Vyčistit chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Opravdu chcete vymazat historii chatu? Operaci nelze vrátit!'; + + @override + String get studyDeleteStudy => 'Smazat studii'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Opravdu chcete smazat celou studii? Akci nelze vrátit zpět. Zadejte název studie pro potvrzení: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kde chcete tuto pozici studovat?'; + + @override + String get studyGoodMove => 'Dobrý tah'; + + @override + String get studyMistake => 'Chyba'; + + @override + String get studyBrilliantMove => 'Výborný tah'; + + @override + String get studyBlunder => 'Hrubá chyba'; + + @override + String get studyInterestingMove => 'Zajímavý tah'; + + @override + String get studyDubiousMove => 'Pochybný tah'; + + @override + String get studyOnlyMove => 'Jediný tah'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Rovná pozice'; + + @override + String get studyUnclearPosition => 'Nejasná pozice'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bílý stojí o něco lépe'; + + @override + String get studyBlackIsSlightlyBetter => 'Černý stojí o něco lépe'; + + @override + String get studyWhiteIsBetter => 'Bílý stojí lépe'; + + @override + String get studyBlackIsBetter => 'Černý stojí lépe'; + + @override + String get studyWhiteIsWinning => 'Bílý má rozhodující výhodu'; + + @override + String get studyBlackIsWinning => 'Černý má rozhodující výhodu'; + + @override + String get studyNovelty => 'Novinka'; + + @override + String get studyDevelopment => 'Vývin'; + + @override + String get studyInitiative => 'S iniciativou'; + + @override + String get studyAttack => 'S útokem'; + + @override + String get studyCounterplay => 'S protihrou'; + + @override + String get studyTimeTrouble => 'Časová tíseň'; + + @override + String get studyWithCompensation => 'S kompenzací'; + + @override + String get studyWithTheIdea => 'S ideou'; + + @override + String get studyNextChapter => 'Další kapitola'; + + @override + String get studyPrevChapter => 'Předchozí kapitola'; + + @override + String get studyStudyActions => 'Akce pro studii'; + + @override + String get studyTopics => 'Témata'; + + @override + String get studyMyTopics => 'Moje témata'; + + @override + String get studyPopularTopics => 'Oblíbená témata'; + + @override + String get studyManageTopics => 'Správa témat'; + + @override + String get studyBack => 'Zpět'; + + @override + String get studyPlayAgain => 'Hrát znovu'; + + @override + String get studyWhatWouldYouPlay => 'Co byste v této pozici hráli?'; + + @override + String get studyYouCompletedThisLesson => 'Blahopřejeme! Dokončili jste tuto lekci.'; + + @override + String studyPerPage(String param) { + return '$param na stránku'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitol', + many: '$count kapitol', + few: '$count kapitoly', + one: '$count kapitola', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count her', + many: '$count her', + few: '$count hry', + one: '$count hra', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count členů', + many: '$count členů', + few: '$count členi', + one: '$count člen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Vložte obsah vašeho PGN souboru (až $count her)', + many: 'Vložte obsah vašeho PGN souboru (až $count her)', + few: 'Vložte obsah vašeho PGN souboru (až $count hry)', + one: 'Vložte obsah vašeho PGN souboru (až $count hra)', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'právě teď'; + + @override + String get timeagoRightNow => 'právě teď'; + + @override + String get timeagoCompleted => 'dokončeno'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekund', + many: 'za $count sekund', + few: 'za $count sekundy', + one: 'za $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minut', + many: 'za $count minut', + few: 'za $count minuty', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count hodin', + many: 'za $count hodin', + few: 'za $count hodiny', + one: 'za $count hodinu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dnů', + many: 'za $count dnů', + few: 'za $count dny', + one: 'za $count den', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count týdnů', + many: 'za $count týdnů', + few: 'za $count týdny', + one: 'za $count týden', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count měsíců', + many: 'za $count měsíců', + few: 'za $count měsíce', + one: 'za $count měsíc', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count let', + many: 'za $count let', + few: 'za $count roky', + one: 'za $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count minutami', + many: 'před $count minutami', + few: 'před $count minutami', + one: 'před $count minutou', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count hodinami', + many: 'před $count hodinami', + few: 'před $count hodinami', + one: 'před $count hodinou', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count dny', + many: 'před $count dny', + few: 'před $count dny', + one: 'před $count dnem', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count týdny', + many: 'před $count týdny', + few: 'před $count týdny', + one: 'před $count týdnem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count měsíci', + many: 'před $count měsíci', + few: 'před $count měsíci', + one: 'před $count měsícem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count lety', + many: 'před $count lety', + few: 'před $count lety', + one: 'před $count rokem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zbývá $count minut', + many: 'Zbývá $count minut', + few: 'Zbývají $count minuty', + one: 'Zbývá $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zbývá $count hodin', + many: 'Zbývá $count hodin', + few: 'Zbývají $count hodiny', + one: 'Zbývá $count hodina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index e23dcef0b7..4c1466b1f9 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsDa extends AppLocalizations { AppLocalizationsDa([String locale = 'da']) : super(locale); @override - String get mobileHomeTab => 'Hjem'; + String get mobileAllGames => 'Alle partier'; @override - String get mobilePuzzlesTab => 'Opgaver'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Værktøjer'; + String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; @override - String get mobileWatchTab => 'Se'; + String get mobileClearButton => 'Ryd'; @override - String get mobileSettingsTab => 'Indstillinger'; + String get mobileCorrespondenceClearSavedMove => 'Ryd gemt træk'; @override - String get mobileMustBeLoggedIn => 'Du skal være logget ind for at se denne side.'; + String get mobileCustomGameJoinAGame => 'Deltag i et parti'; @override - String get mobileSystemColors => 'Systemfarver'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hej, $param'; + } @override - String get mobileOkButton => 'Ok'; + String get mobileGreetingWithoutName => 'Hej'; @override - String get mobileSettingsHapticFeedback => 'Haptisk feedback'; + String get mobileHideVariation => 'Skjul variation'; @override - String get mobileSettingsImmersiveMode => 'Fordybelsestilstand'; + String get mobileHomeTab => 'Hjem'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.'; + String get mobileLiveStreamers => 'Live-streamere'; @override - String get mobileNotFollowingAnyUser => 'Du følger ikke nogen brugere.'; + String get mobileMustBeLoggedIn => 'Du skal være logget ind for at se denne side.'; @override - String get mobileAllGames => 'Alle partier'; + String get mobileNoSearchResults => 'Ingen resultater'; @override - String get mobileRecentSearches => 'Seneste søgninger'; + String get mobileNotFollowingAnyUser => 'Du følger ikke nogen brugere.'; @override - String get mobileClearButton => 'Ryd'; + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsDa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen resultater'; + String get mobilePrefMagnifyDraggedPiece => 'Forstør brik, som trækkes'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du afslutte dette løb?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du vil miste din nuværende stime og din score vil blive gemt.'; + String get mobilePuzzleStormFilterNothingToShow => 'Intet at vise, ændr venligst filtre'; @override String get mobilePuzzleStormNothingToShow => 'Intet at vise. Spil nogle runder af Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne opgave'; + String get mobilePuzzleStormSubtitle => 'Løs så mange opgaver som muligt på 3 minutter.'; @override - String get mobileShareGameURL => 'Del partiets URL'; + String get mobilePuzzleStreakAbortWarning => 'Du vil miste din nuværende stime og din score vil blive gemt.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.'; @override - String get mobileSharePositionAsFEN => 'Del position som FEN'; + String get mobilePuzzlesTab => 'Opgaver'; @override - String get mobileShowVariations => 'Vis variationer'; + String get mobileRecentSearches => 'Seneste søgninger'; @override - String get mobileHideVariation => 'Skjul variation'; + String get mobileSettingsHapticFeedback => 'Haptisk feedback'; @override - String get mobileShowComments => 'Vis kommentarer'; + String get mobileSettingsImmersiveMode => 'Fordybelsestilstand'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du afslutte dette løb?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Intet at vise, ændr venligst filtre'; + String get mobileSettingsTab => 'Indstillinger'; @override - String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileCancelDrawOffer => 'Træk tilbud om remis tilbage'; + String get mobileShareGameURL => 'Del partiets URL'; @override - String get mobileWaitingForOpponentToJoin => 'Venter på at modstander slutter sig til...'; + String get mobileSharePositionAsFEN => 'Del position som FEN'; @override - String get mobileBlindfoldMode => 'Bind for øjnene'; + String get mobileSharePuzzle => 'Del denne opgave'; @override - String get mobileLiveStreamers => 'Live-streamere'; + String get mobileShowComments => 'Vis kommentarer'; @override - String get mobileCustomGameJoinAGame => 'Deltag i et parti'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Ryd gemt træk'; + String get mobileShowVariations => 'Vis variationer'; @override String get mobileSomethingWentWrong => 'Noget gik galt.'; @override - String get mobileShowResult => 'Vis resultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.'; + String get mobileSystemColors => 'Systemfarver'; @override - String get mobilePuzzleStormSubtitle => 'Løs så mange opgaver som muligt på 3 minutter.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hej, $param'; - } + String get mobileToolsTab => 'Værktøjer'; @override - String get mobileGreetingWithoutName => 'Hej'; + String get mobileWaitingForOpponentToJoin => 'Venter på at modstander slutter sig til...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Se'; @override String get activityActivity => 'Aktivitet'; @@ -246,6 +243,17 @@ class AppLocalizationsDa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Afsluttede $count $param2 korrespondancepartier', + one: 'Afsluttede $count $param2 korrespondanceparti', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsDa extends AppLocalizations { @override String get broadcastBroadcasts => 'Udsendelser'; + @override + String get broadcastMyBroadcasts => 'Mine udsendelser'; + @override String get broadcastLiveBroadcasts => 'Live turnerings-udsendelser'; + @override + String get broadcastBroadcastCalendar => 'Kaldender for udsendelser'; + + @override + String get broadcastNewBroadcast => 'Ny live-udsendelse'; + + @override + String get broadcastSubscribedBroadcasts => 'Udsendelser, du abonnerer på'; + + @override + String get broadcastAboutBroadcasts => 'Om udsendelse'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Sådan bruges Lichess-udsendelser.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runde vil have de samme medlemmer og bidragydere som den foregående.'; + + @override + String get broadcastAddRound => 'Tilføj en runde'; + + @override + String get broadcastOngoing => 'I gang'; + + @override + String get broadcastUpcoming => 'Kommende'; + + @override + String get broadcastCompleted => 'Afsluttet'; + + @override + String get broadcastCompletedHelp => 'Lichess registrerer rund-færdiggørelse baseret på kildepartierne. Brug denne skifter, hvis der ikke er nogen kilde.'; + + @override + String get broadcastRoundName => 'Rundenavn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnavn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivelse af turnering'; + + @override + String get broadcastFullDescription => 'Fuld beskrivelse af begivenheden'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valgfri lang beskrivelse af transmissionen. $param1 er tilgængelig. Længde skal være mindre end $param2 tegn.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL for PGN-kilde'; + + @override + String get broadcastSourceUrlHelp => 'URL som Lichess vil trække på for at få PGN updates. Den skal være offentlig tilgængelig fra internettet.'; + + @override + String get broadcastSourceGameIds => 'Op til 64 Lichess parti-ID\'er, adskilt af mellemrum.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringens lokale tidszone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valgfri, hvis du ved, hvornår begivenheden starter'; + + @override + String get broadcastCurrentGameUrl => 'Nuværende parti URL'; + + @override + String get broadcastDownloadAllRounds => 'Download alle runder'; + + @override + String get broadcastResetRound => 'Nulstil denne runde'; + + @override + String get broadcastDeleteRound => 'Slet denne runde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slet runden og dens partier endegyldigt.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Slet alle partier i denne runde. Kilden skal være aktiv for at genskabe dem.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slet denne turnering'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slet hele turneringen, alle dens runder og alle dens partier.'; + + @override + String get broadcastShowScores => 'Vis spilleres point baseret på resultater fra partier'; + + @override + String get broadcastReplacePlayerTags => 'Valgfrit: udskift spillernavne, ratings og titler'; + + @override + String get broadcastFideFederations => 'FIDE-føderationer'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spillere'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-spiller ikke fundet'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Føderation'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Uden rating'; + + @override + String get broadcastRecentTournaments => 'Seneste turneringer'; + + @override + String get broadcastOpenLichess => 'Åbn i Lichess'; + + @override + String get broadcastTeams => 'Hold'; + + @override + String get broadcastBoards => 'Brætter'; + + @override + String get broadcastOverview => 'Oversigt'; + + @override + String get broadcastSubscribeTitle => 'Abonner på at blive underrettet, når hver runde starter. Du kan skifte mellem klokke- eller push-meddelelser for udsendelser i dine kontoindstillinger.'; + + @override + String get broadcastUploadImage => 'Upload turneringsbillede'; + + @override + String get broadcastNoBoardsYet => 'Ingen brætter endnu. Disse vises når partier er uploadet.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brætter kan indlæses med en kilde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starter efter $param'; + } + + @override + String get broadcastStartVerySoon => 'Udsendelsen starter meget snart.'; + + @override + String get broadcastNotYetStarted => 'Udsendelsen er endnu ikke startet.'; + + @override + String get broadcastOfficialWebsite => 'Officielt websted'; + + @override + String get broadcastStandings => 'Stillinger'; + + @override + String get broadcastOfficialStandings => 'Officiel stilling'; + + @override + String broadcastIframeHelp(String param) { + return 'Flere muligheder på $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'En offentlig, realtids PGN-kilde til denne runde. Vi tilbyder også en $param for hurtigere og mere effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Indlejr denne udsendelse på dit website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Indlejr $param på dit website'; + } + + @override + String get broadcastRatingDiff => 'Rating-forskel'; + + @override + String get broadcastGamesThisTournament => 'Partier i denne turnering'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'Alle hold'; + + @override + String get broadcastTournamentFormat => 'Turneringsformat'; + + @override + String get broadcastTournamentLocation => 'Turneringssted'; + + @override + String get broadcastTopPlayers => 'Topspillere'; + + @override + String get broadcastTimezone => 'Tidszone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; + + @override + String get broadcastOptionalDetails => 'Valgfri detaljer'; + + @override + String get broadcastPastBroadcasts => 'Tidligere udsendelser'; + + @override + String get broadcastAllBroadcastsByMonth => 'Vis alle udsendelser efter måned'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count udsendelser', + one: '$count udsendelse', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Udfordringer: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get preferencesInGameOnly => 'Kun i spillet'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Skakur'; @@ -750,6 +1008,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Notifikationslyd'; + @override + String get preferencesBlindfold => 'Blindskak'; + @override String get puzzlePuzzles => 'Taktikopgaver'; @@ -1390,10 +1651,10 @@ class AppLocalizationsDa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Modstanderen har begrænsede muligheder for træk, og ethvert træk vil forværre positionen.'; @override - String get puzzleThemeHealthyMix => 'Sund blanding'; + String get puzzleThemeMix => 'Sund blanding'; @override - String get puzzleThemeHealthyMixDescription => 'Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.'; + String get puzzleThemeMixDescription => 'Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.'; @override String get puzzleThemePlayerGames => 'Spiller-partier'; @@ -1586,7 +1847,7 @@ class AppLocalizationsDa extends AppLocalizations { String get computerAnalysis => 'Computeranalyse'; @override - String get computerAnalysisAvailable => 'Computeranalyse klar'; + String get computerAnalysisAvailable => 'Computeranalyse tilgængelig'; @override String get computerAnalysisDisabled => 'Computeranalyse deaktiveret'; @@ -1767,9 +2028,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get byCPL => 'CBT'; - @override - String get openStudy => 'Åben studie'; - @override String get enable => 'Aktivér'; @@ -1797,9 +2055,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get removesTheDepthLimit => 'Fjerner dybdegrænsen, og holder din computer varm'; - @override - String get engineManager => 'Administration af skakprogram'; - @override String get blunder => 'Brøler'; @@ -2063,6 +2318,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get gamesPlayed => 'Antal partier spillet'; + @override + String get ok => 'Ok'; + @override String get cancel => 'Annuller'; @@ -2437,9 +2695,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get unblock => 'Stop blokering'; - @override - String get followsYou => 'Følger dig'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 følger nu $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsDa extends AppLocalizations { String get other => 'Andet'; @override - String get reportDescriptionHelp => 'Indsæt et link til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel.'; + String get reportCheatBoostHelp => 'Indsæt linket til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel. Sig ikke blot \"de snyder\", men fortæl os, hvordan du nåede frem til den konklusion.'; + + @override + String get reportUsernameHelp => 'Forklar, hvad der er stødende ved dette brugernavn. Sig ikke blot \"det er stødende/upassende\", men fortæl os, hvordan du nåede frem til denne konklusion, især hvis fornærmelsen er sløret, ikke er på engelsk, er slang eller er en historisk/kulturel reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Din indberetning vil blive behandlet hurtigere, hvis den er skrevet på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Angiv mindst ét link til et parti med snyd.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get nothingToSeeHere => 'Intet at se her i øjeblikket.'; + @override + String get stats => 'Statistik'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsDa extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-streamere'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studier'; + + @override + String get studyStudiesIContributeTo => 'Studier jeg bidrager til'; + + @override + String get studyMyPublicStudies => 'Mine offentlige studier'; + + @override + String get studyMyPrivateStudies => 'Mine private studier'; + + @override + String get studyMyFavoriteStudies => 'Mine favoritstudier'; + + @override + String get studyWhatAreStudies => 'Hvad er studier?'; + + @override + String get studyAllStudies => 'Alle studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier oprettet af $param'; + } + + @override + String get studyNoneYet => 'Ingen endnu.'; + + @override + String get studyHot => 'Populært'; + + @override + String get studyDateAddedNewest => 'Dato tilføjet (nyeste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføjet (ældste)'; + + @override + String get studyRecentlyUpdated => 'Nyligt opdateret'; + + @override + String get studyMostPopular => 'Mest populære'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Tilføj et nyt kapitel'; + + @override + String get studyAddMembers => 'Tilføj medlemmer'; + + @override + String get studyInviteToTheStudy => 'Inviter til studiet'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter venligst kun personer du kender, og som ønsker at være en del af dette studie.'; + + @override + String get studySearchByUsername => 'Søg på brugernavn'; + + @override + String get studySpectator => 'Tilskuer'; + + @override + String get studyContributor => 'Bidragsyder'; + + @override + String get studyKick => 'Smid ud'; + + @override + String get studyLeaveTheStudy => 'Forlad dette studie'; + + @override + String get studyYouAreNowAContributor => 'Du er nu bidragsyder'; + + @override + String get studyYouAreNowASpectator => 'Du er nu tilskuer'; + + @override + String get studyPgnTags => 'PGN tags'; + + @override + String get studyLike => 'Synes godt om'; + + @override + String get studyUnlike => 'Synes ikke godt om'; + + @override + String get studyNewTag => 'Nyt tag'; + + @override + String get studyCommentThisPosition => 'Kommenter på denne stilling'; + + @override + String get studyCommentThisMove => 'Kommenter på dette træk'; + + @override + String get studyAnnotateWithGlyphs => 'Annoter med glyffer'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Dette kapitel er for kort til at blive analyseret.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Kun studiets bidragsydere kan anmode om en computeranalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få en fuld server-computeranalyse af hovedlinjen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sikr dig at kapitlet er færdigt. Du kan kun anmode om analyse én gang.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC medlemmer forbliver på samme stilling'; + + @override + String get studyShareChanges => 'Del ændringer med tilskuere og gem dem på serveren'; + + @override + String get studyPlaying => 'Spiller'; + + @override + String get studyShowEvalBar => 'Evalueringssøjler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Forrige'; + + @override + String get studyNext => 'Næste'; + + @override + String get studyLast => 'Sidste'; + @override String get studyShareAndExport => 'Del & eksport'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie PGN'; + + @override + String get studyDownloadAllGames => 'Download alle partier'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Download parti'; + + @override + String get studyStudyUrl => 'Studie URL'; + + @override + String get studyCurrentChapterUrl => 'Nuværende kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan indsætte dette i forummet for at indlejre'; + + @override + String get studyStartAtInitialPosition => 'Start ved indledende stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Indlejr på din hjemmeside eller blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Læs mere om indlejring'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Kun offentlige studier kan indlejres!'; + + @override + String get studyOpen => 'Åbn'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 bragt til dig af $param2'; + } + + @override + String get studyStudyNotFound => 'Studie ikke fundet'; + + @override + String get studyEditChapter => 'Rediger kapitel'; + + @override + String get studyNewChapter => 'Nyt kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Import fra $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysetilstand'; + + @override + String get studyPinnedChapterComment => 'Fastgjort kapitelkommentar'; + + @override + String get studySaveChapter => 'Gem kapitel'; + + @override + String get studyClearAnnotations => 'Ryd annoteringer'; + + @override + String get studyClearVariations => 'Ryd varianter'; + + @override + String get studyDeleteChapter => 'Slet kapitel'; + + @override + String get studyDeleteThisChapter => 'Slet dette kapitel? Du kan ikke fortryde!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Ryd alle kommentarer og figurer i dette kapitel?'; + + @override + String get studyRightUnderTheBoard => 'Lige under brættet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul næste træk'; + + @override + String get studyInteractiveLesson => 'Interaktiv lektion'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved indledende stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start fra brugerdefinerede stilling'; + + @override + String get studyLoadAGameByUrl => 'Indlæs et parti fra URL'; + + @override + String get studyLoadAPositionFromFen => 'Indlæs en stilling fra FEN'; + + @override + String get studyLoadAGameFromPgn => 'Indlæs et parti fra PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Indlæs et parti fra $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opret kapitel'; + + @override + String get studyCreateStudy => 'Opret studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlighed'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ikke listet'; + + @override + String get studyInviteOnly => 'Kun inviterede'; + + @override + String get studyAllowCloning => 'Tillad kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Kun mig'; + + @override + String get studyContributors => 'Bidragydere'; + + @override + String get studyMembers => 'Medlemmer'; + + @override + String get studyEveryone => 'Enhver'; + + @override + String get studyEnableSync => 'Aktiver synk'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behold alle på den samme stilling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nej: lad folk gennemse frit'; + + @override + String get studyPinnedStudyComment => 'Fastgjort studie-kommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Gem'; + + @override + String get studyClearChat => 'Ryd chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slet studiets chat-historik? Du kan ikke fortryde!'; + + @override + String get studyDeleteStudy => 'Slet studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slet hele studiet? Det kan ikke fortrydes! Skriv navnet på studiet for at bekræfte: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvor vil du studere det?'; + + @override + String get studyGoodMove => 'Godt træk'; + + @override + String get studyMistake => 'Fejl'; + + @override + String get studyBrilliantMove => 'Fremragende træk'; + + @override + String get studyBlunder => 'Brøler'; + + @override + String get studyInterestingMove => 'Interessant træk'; + + @override + String get studyDubiousMove => 'Tvivlsomt træk'; + + @override + String get studyOnlyMove => 'Eneste mulige træk'; + + @override + String get studyZugzwang => 'Træktvang'; + + @override + String get studyEqualPosition => 'Lige stilling'; + + @override + String get studyUnclearPosition => 'Uafklaret stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Hvid står lidt bedre'; + + @override + String get studyBlackIsSlightlyBetter => 'Sort står lidt bedre'; + + @override + String get studyWhiteIsBetter => 'Hvid står bedre'; + + @override + String get studyBlackIsBetter => 'Sort står bedre'; + + @override + String get studyWhiteIsWinning => 'Hvid vinder'; + + @override + String get studyBlackIsWinning => 'Sort vinder'; + + @override + String get studyNovelty => 'Nyfunden'; + + @override + String get studyDevelopment => 'Udvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Angreb'; + + @override + String get studyCounterplay => 'Modspil'; + + @override + String get studyTimeTrouble => 'Tidsproblemer'; + + @override + String get studyWithCompensation => 'Med kompensation'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Næste kapitel'; + + @override + String get studyPrevChapter => 'Forrige kapitel'; + + @override + String get studyStudyActions => 'Studiehandlinger'; + + @override + String get studyTopics => 'Emner'; + + @override + String get studyMyTopics => 'Mine emner'; + + @override + String get studyPopularTopics => 'Populære emner'; + + @override + String get studyManageTopics => 'Administrér emner'; + + @override + String get studyBack => 'Tilbage'; + + @override + String get studyPlayAgain => 'Spil igen'; + + @override + String get studyWhatWouldYouPlay => 'Hvad ville du spille i denne position?'; + + @override + String get studyYouCompletedThisLesson => 'Tillykke! Du har fuldført denne lektion.'; + + @override + String studyPerPage(String param) { + return '$param pr. side'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitler', + one: '$count kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Medlemmer', + one: '$count Medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Indsæt din PGN-tekst her, op til $count partier', + one: 'Indsæt din PGN-tekst her, op til $count parti', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'for lidt siden'; + + @override + String get timeagoRightNow => 'netop nu'; + + @override + String get timeagoCompleted => 'afsluttet'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutter', + one: 'om $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timer', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dage', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count uger', + one: 'om $count uge', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count måneder', + one: 'om $count måned', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter siden', + one: '$count minut siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer siden', + one: '$count time siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dage siden', + one: '$count dag siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uger siden', + one: '$count uge siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count måneder siden', + one: '$count måned siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år siden', + one: '$count år siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter tilbage', + one: '$count minut tilbage', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer tilbage', + one: '$count time tilbage', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index da502af1fd..660308d945 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); @override - String get mobileHomeTab => 'Start'; + String get mobileAllGames => 'Alle Partien'; @override - String get mobilePuzzlesTab => 'Aufgaben'; + String get mobileAreYouSure => 'Bist du sicher?'; @override - String get mobileToolsTab => 'Werkzeuge'; + String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; @override - String get mobileWatchTab => 'Zuschauen'; + String get mobileClearButton => 'Löschen'; @override - String get mobileSettingsTab => 'Einstellungen'; + String get mobileCorrespondenceClearSavedMove => 'Gespeicherten Zug löschen'; @override - String get mobileMustBeLoggedIn => 'Du musst eingeloggt sein, um diese Seite anzuzeigen.'; + String get mobileCustomGameJoinAGame => 'Einer Partie beitreten'; @override - String get mobileSystemColors => 'Systemfarben'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsHapticFeedback => 'Haptisches Feedback'; + String get mobileHideVariation => 'Variante ausblenden'; @override - String get mobileSettingsImmersiveMode => 'Immersiver Modus'; + String get mobileHomeTab => 'Start'; @override - String get mobileSettingsImmersiveModeSubtitle => 'System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.'; + String get mobileLiveStreamers => 'Livestreamer'; @override - String get mobileNotFollowingAnyUser => 'Du folgst keinem Nutzer.'; + String get mobileMustBeLoggedIn => 'Du musst eingeloggt sein, um diese Seite anzuzeigen.'; @override - String get mobileAllGames => 'Alle Partien'; + String get mobileNoSearchResults => 'Keine Ergebnisse'; @override - String get mobileRecentSearches => 'Letzte Suchen'; + String get mobileNotFollowingAnyUser => 'Du folgst keinem Nutzer.'; @override - String get mobileClearButton => 'Löschen'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Keine Ergebnisse'; + String get mobilePrefMagnifyDraggedPiece => 'Vergrößern der gezogenen Figur'; @override - String get mobileAreYouSure => 'Bist du sicher?'; + String get mobilePuzzleStormConfirmEndRun => 'Möchtest du diesen Durchlauf beenden?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nichts anzuzeigen, bitte passe deine Filter an'; @override String get mobilePuzzleStormNothingToShow => 'Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Teile diese Aufgabe'; + String get mobilePuzzleStormSubtitle => 'Löse so viele Aufgaben wie möglich in 3 Minuten.'; @override - String get mobileShareGameURL => 'Link der Partie teilen'; + String get mobilePuzzleStreakAbortWarning => 'Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.'; @override - String get mobileShareGamePGN => 'PGN teilen'; + String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.'; @override - String get mobileSharePositionAsFEN => 'Stellung als FEN teilen'; + String get mobilePuzzlesTab => 'Aufgaben'; @override - String get mobileShowVariations => 'Varianten anzeigen'; + String get mobileRecentSearches => 'Letzte Suchen'; @override - String get mobileHideVariation => 'Variante ausblenden'; + String get mobileSettingsHapticFeedback => 'Haptisches Feedback'; @override - String get mobileShowComments => 'Kommentare anzeigen'; + String get mobileSettingsImmersiveMode => 'Immersiver Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Möchtest du diesen Durchlauf beenden?'; + String get mobileSettingsImmersiveModeSubtitle => 'System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nichts anzuzeigen, bitte passe deine Filter an'; + String get mobileSettingsTab => 'Optionen'; @override - String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; + String get mobileShareGamePGN => 'PGN teilen'; @override - String get mobileCancelDrawOffer => 'Remisangebot zurücknehmen'; + String get mobileShareGameURL => 'Link der Partie teilen'; @override - String get mobileWaitingForOpponentToJoin => 'Warte auf Beitritt eines Gegners...'; + String get mobileSharePositionAsFEN => 'Stellung als FEN teilen'; @override - String get mobileBlindfoldMode => 'Blind spielen'; + String get mobileSharePuzzle => 'Teile diese Aufgabe'; @override - String get mobileLiveStreamers => 'Livestreamer'; + String get mobileShowComments => 'Kommentare anzeigen'; @override - String get mobileCustomGameJoinAGame => 'Einer Partie beitreten'; + String get mobileShowResult => 'Ergebnis anzeigen'; @override - String get mobileCorrespondenceClearSavedMove => 'Gespeicherten Zug löschen'; + String get mobileShowVariations => 'Varianten anzeigen'; @override String get mobileSomethingWentWrong => 'Etwas ist schiefgelaufen.'; @override - String get mobileShowResult => 'Ergebnis anzeigen'; - - @override - String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Thema.'; + String get mobileSystemColors => 'Systemfarben'; @override - String get mobilePuzzleStormSubtitle => 'Löse in 3 Minuten so viele Aufgaben wie möglich.'; + String get mobileTheme => 'Erscheinungsbild'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Werkzeuge'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Warte auf Beitritt eines Gegners...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Zuschauen'; @override String get activityActivity => 'Verlauf'; @@ -246,6 +243,17 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hat $count $param2-Fernschachpartien gespielt', + one: 'Hat $count $param2-Fernschachpartie gespielt', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsDe extends AppLocalizations { @override String get broadcastBroadcasts => 'Übertragungen'; + @override + String get broadcastMyBroadcasts => 'Meine Übertragungen'; + @override String get broadcastLiveBroadcasts => 'Live-Turnierübertragungen'; + @override + String get broadcastBroadcastCalendar => 'Sendekalender'; + + @override + String get broadcastNewBroadcast => 'Neue Liveübertragung'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonnierte Übertragungen'; + + @override + String get broadcastAboutBroadcasts => 'Über Übertragungen'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Wie man Lichess-Übertragungen benutzt.'; + + @override + String get broadcastTheNewRoundHelp => 'Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende.'; + + @override + String get broadcastAddRound => 'Eine Runde hinzufügen'; + + @override + String get broadcastOngoing => 'Laufend'; + + @override + String get broadcastUpcoming => 'Demnächst'; + + @override + String get broadcastCompleted => 'Beendet'; + + @override + String get broadcastCompletedHelp => 'Lichess erkennt Rundenabschlüsse basierend auf den Quellspielen. Verwende diesen Schalter, wenn keine Quelle vorhanden ist.'; + + @override + String get broadcastRoundName => 'Rundenname'; + + @override + String get broadcastRoundNumber => 'Rundennummer'; + + @override + String get broadcastTournamentName => 'Turniername'; + + @override + String get broadcastTournamentDescription => 'Kurze Turnierbeschreibung'; + + @override + String get broadcastFullDescription => 'Vollständige Ereignisbeschreibung'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionale, ausführliche Beschreibung der Übertragung. $param1 ist verfügbar. Die Beschreibung muss kürzer als $param2 Zeichen sein.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Quell-URL'; + + @override + String get broadcastSourceUrlHelp => 'URL die Lichess abfragt um PGN Aktualisierungen zu erhalten. Sie muss öffentlich aus dem Internet zugänglich sein.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess Partie-IDs, getrennt durch Leerzeichen.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum in der Zeitzone des Tunierstandortes: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, falls du weißt wann das Ereignis beginnt'; + + @override + String get broadcastCurrentGameUrl => 'URL der aktuellen Partie'; + + @override + String get broadcastDownloadAllRounds => 'Alle Runden herunterladen'; + + @override + String get broadcastResetRound => 'Diese Runde zurücksetzen'; + + @override + String get broadcastDeleteRound => 'Diese Runde löschen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Lösche die Runde und ihre Partien endgültig.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Lösche alle Partien dieser Runde. Die Quelle muss aktiv sein, um sie neu zu erstellen.'; + + @override + String get broadcastEditRoundStudy => 'Rundenstudie bearbeiten'; + + @override + String get broadcastDeleteTournament => 'Dieses Turnier löschen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Lösche definitiv das gesamte Turnier, alle seine Runden und Partien.'; + + @override + String get broadcastShowScores => 'Punktestand der Spieler basierend auf Spielergebnissen anzeigen'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Spielernamen, Wertungen und Titel ersetzen'; + + @override + String get broadcastFideFederations => 'FIDE-Verbände'; + + @override + String get broadcastTop10Rating => 'Top-10-Wertung'; + + @override + String get broadcastFidePlayers => 'FIDE-Spieler'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-Spieler nicht gefunden'; + + @override + String get broadcastFideProfile => 'FIDE-Profil'; + + @override + String get broadcastFederation => 'Verband'; + + @override + String get broadcastAgeThisYear => 'Alter in diesem Jahr'; + + @override + String get broadcastUnrated => 'Ungewertet'; + + @override + String get broadcastRecentTournaments => 'Letzte Turniere'; + + @override + String get broadcastOpenLichess => 'In Lichess öffnen'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Bretter'; + + @override + String get broadcastOverview => 'Überblick'; + + @override + String get broadcastSubscribeTitle => 'Abonnieren, um bei Rundenbeginn benachrichtigt zu werden. Du kannst in deinen Benutzereinstellungen für Übertragungen zwischen einer Benachrichtigung per Glocke oder per Push-Benachrichtigung wählen.'; + + @override + String get broadcastUploadImage => 'Turnierbild hochladen'; + + @override + String get broadcastNoBoardsYet => 'Noch keine Bretter vorhanden. Diese werden angezeigt, sobald die Partien hochgeladen werden.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Die Bretter können per Quelle oder via $param geladen werden'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Beginnt nach $param'; + } + + @override + String get broadcastStartVerySoon => 'Diese Übertragung wird in Kürze beginnen.'; + + @override + String get broadcastNotYetStarted => 'Die Übertragung hat noch nicht begonnen.'; + + @override + String get broadcastOfficialWebsite => 'Offizielle Webseite'; + + @override + String get broadcastStandings => 'Rangliste'; + + @override + String get broadcastOfficialStandings => 'Offizielle Rangliste'; + + @override + String broadcastIframeHelp(String param) { + return 'Weitere Optionen auf der $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster-Seite'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Eine öffentliche Echtzeit-PGN-Quelle für diese Runde. Wir bieten auch eine $param für eine schnellere und effizientere Synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bette diese Übertragung in deine Webseite ein'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bette $param in deine Webseite ein'; + } + + @override + String get broadcastRatingDiff => 'Wertungsdifferenz'; + + @override + String get broadcastGamesThisTournament => 'Partien in diesem Turnier'; + + @override + String get broadcastScore => 'Punktestand'; + + @override + String get broadcastAllTeams => 'Alle Teams'; + + @override + String get broadcastTournamentFormat => 'Turnierformat'; + + @override + String get broadcastTournamentLocation => 'Turnierort'; + + @override + String get broadcastTopPlayers => 'Spitzenspieler'; + + @override + String get broadcastTimezone => 'Zeitzone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-Wertungskategorie'; + + @override + String get broadcastOptionalDetails => 'Optionale Details'; + + @override + String get broadcastPastBroadcasts => 'Vergangene Übertragungen'; + + @override + String get broadcastAllBroadcastsByMonth => 'Alle Übertragungen nach Monat anzeigen'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Übertragungen', + one: '$count Übertragung', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Herausforderungen: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur während einer Partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachuhr'; @@ -750,6 +1008,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Glocken-Benachrichtigungston'; + @override + String get preferencesBlindfold => 'Blindschach'; + @override String get puzzlePuzzles => 'Taktikaufgaben'; @@ -1390,10 +1651,10 @@ class AppLocalizationsDe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Der Gegner ist in der Anzahl seiner Züge limitiert und jeder seiner Züge verschlechtert seine Stellung.'; @override - String get puzzleThemeHealthyMix => 'Gesunder Mix'; + String get puzzleThemeMix => 'Gesunder Mix'; @override - String get puzzleThemeHealthyMixDescription => 'Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.'; + String get puzzleThemeMixDescription => 'Ein bisschen von allem. Du weißt nicht, was dich erwartet, deshalb bleibst du bereit für alles! Genau wie in echten Partien.'; @override String get puzzleThemePlayerGames => 'Partien von Spielern'; @@ -1767,9 +2028,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get byCPL => 'Nach CPL'; - @override - String get openStudy => 'Studie öffnen'; - @override String get enable => 'Einschalten'; @@ -1797,9 +2055,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt die Tiefenbegrenzung und hält deinen Computer warm'; - @override - String get engineManager => 'Engineverwaltung'; - @override String get blunder => 'Grober Patzer'; @@ -2063,6 +2318,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get gamesPlayed => 'Gespielte Partien'; + @override + String get ok => 'OK'; + @override String get cancel => 'Abbrechen'; @@ -2437,9 +2695,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get unblock => 'Nicht mehr blockieren'; - @override - String get followsYou => 'Folgt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 folgt jetzt $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsDe extends AppLocalizations { String get other => 'Sonstiges'; @override - String get reportDescriptionHelp => 'Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie Du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.'; + String get reportCheatBoostHelp => 'Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Verhaltens des Spielers. Bitte schreibe nicht einfach nur „Die schummeln (bzw. betrügen)“, sondern begründe auch, wie Du zu diesem Schluss kommst.'; + + @override + String get reportUsernameHelp => 'Erkläre, was an diesem Benutzernamen beleidigend oder unangemessen ist. Sage nicht einfach \"Der Name ist beleidigend/unangemessen\", sondern erkläre, wie du zu dieser Schlussfolgerung gekommen bist. Insbesondere wenn die Beleidigung verschleiert wird, nicht auf Englisch, ist in Slang, oder ist ein historischer/kultureller Bezugspunkt.'; + + @override + String get reportProcessedFasterInEnglish => 'Ihr Bericht wird schneller bearbeitet, wenn er auf Englisch verfasst ist.'; @override String get error_provideOneCheatedGameLink => 'Bitte gib mindestens einen Link zu einem Spiel an, in dem betrogen wurde.'; @@ -4057,7 +4318,7 @@ class AppLocalizationsDe extends AppLocalizations { String get lichessDbExplanation => 'Aus gewerteten Partien aller Lichess-Spieler'; @override - String get switchSides => 'andere Farbe'; + String get switchSides => 'Seitenwechsel'; @override String get closingAccountWithdrawAppeal => 'Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehen'; @@ -4077,6 +4338,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get nothingToSeeHere => 'Im Moment gibt es hier nichts zu sehen.'; + @override + String get stats => 'Statistiken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsDe extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Meine Studien'; + + @override + String get studyStudiesIContributeTo => 'Studien, an denen ich mitwirke'; + + @override + String get studyMyPublicStudies => 'Meine öffentlichen Studien'; + + @override + String get studyMyPrivateStudies => 'Meine privaten Studien'; + + @override + String get studyMyFavoriteStudies => 'Meine Lieblingsstudien'; + + @override + String get studyWhatAreStudies => 'Was sind Studien?'; + + @override + String get studyAllStudies => 'Alle Studien'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Von $param erstellte Studien'; + } + + @override + String get studyNoneYet => 'Bisher keine.'; + + @override + String get studyHot => 'Angesagt'; + + @override + String get studyDateAddedNewest => 'Veröffentlichungsdatum (neueste)'; + + @override + String get studyDateAddedOldest => 'Veröffentlichungsdatum (älteste)'; + + @override + String get studyRecentlyUpdated => 'Kürzlich aktualisiert'; + + @override + String get studyMostPopular => 'Beliebteste'; + + @override + String get studyAlphabetical => 'Alphabetisch'; + + @override + String get studyAddNewChapter => 'Neues Kapitel hinzufügen'; + + @override + String get studyAddMembers => 'Mitglieder hinzufügen'; + + @override + String get studyInviteToTheStudy => 'Zur Studie einladen'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bitte lade nur Leute ein, die dich kennen und die aktiv an dieser Studie teilnehmen möchten.'; + + @override + String get studySearchByUsername => 'Suche nach Benutzernamen'; + + @override + String get studySpectator => 'Zuschauer'; + + @override + String get studyContributor => 'Mitwirkender'; + + @override + String get studyKick => 'Rauswerfen'; + + @override + String get studyLeaveTheStudy => 'Studie verlassen'; + + @override + String get studyYouAreNowAContributor => 'Du bist jetzt ein Mitwirkender'; + + @override + String get studyYouAreNowASpectator => 'Du bist jetzt Zuschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gefällt mir'; + + @override + String get studyUnlike => 'Gefällt mir nicht mehr'; + + @override + String get studyNewTag => 'Neuer Tag'; + + @override + String get studyCommentThisPosition => 'Kommentiere diese Stellung'; + + @override + String get studyCommentThisMove => 'Kommentiere diesen Zug'; + + @override + String get studyAnnotateWithGlyphs => 'Mit Symbolen kommentieren'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Das Kapitel ist zu kurz zum Analysieren.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur Mitwirkende an der Studie können eine Computeranalyse anfordern.'; + + @override + String get studyGetAFullComputerAnalysis => 'Erhalte eine vollständige serverseitige Computeranalyse der Hauptvariante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Stelle sicher, dass das Kapitel vollständig ist. Die Analyse kann nur einmal angefordert werden.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle synchronisierten Mitglieder sehen die gleiche Stellung'; + + @override + String get studyShareChanges => 'Teile Änderungen mit den Zuschauern und speichere sie auf dem Server'; + + @override + String get studyPlaying => 'Laufende Partien'; + + @override + String get studyShowEvalBar => 'Stellungsbewertungs-Balken'; + + @override + String get studyFirst => 'Erste Seite'; + + @override + String get studyPrevious => 'Zurück'; + + @override + String get studyNext => 'Weiter'; + + @override + String get studyLast => 'Letzte Seite'; + @override String get studyShareAndExport => 'Teilen und exportieren'; + @override + String get studyCloneStudy => 'Klonen'; + + @override + String get studyStudyPgn => 'Studien PGN'; + + @override + String get studyDownloadAllGames => 'Lade alle Partien herunter'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopieren'; + + @override + String get studyDownloadGame => 'Lade die Partie herunter'; + + @override + String get studyStudyUrl => 'Studien URL'; + + @override + String get studyCurrentChapterUrl => 'URL des aktuellen Kapitels'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Zum Einbinden füge dies im Forum ein'; + + @override + String get studyStartAtInitialPosition => 'Beginne mit der Anfangsstellung'; + + @override + String studyStartAtX(String param) { + return 'Beginne mit $param'; + } + + @override + String get studyEmbedInYourWebsite => 'In deine Webseite oder deinen Blog einbetten'; + + @override + String get studyReadMoreAboutEmbedding => 'Lies mehr über das Einbinden'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Nur öffentliche Studien können eingebunden werden!'; + + @override + String get studyOpen => 'Öffnen'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 präsentiert von $param2'; + } + + @override + String get studyStudyNotFound => 'Studie nicht gefunden'; + + @override + String get studyEditChapter => 'Kapitel bearbeiten'; + + @override + String get studyNewChapter => 'Neues Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importiere aus $param'; + } + + @override + String get studyOrientation => 'Ausrichtung'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Angepinnte Kapitelkommentare'; + + @override + String get studySaveChapter => 'Kapitel speichern'; + + @override + String get studyClearAnnotations => 'Anmerkungen löschen'; + + @override + String get studyClearVariations => 'Varianten löschen'; + + @override + String get studyDeleteChapter => 'Kapitel löschen'; + + @override + String get studyDeleteThisChapter => 'Kapitel löschen. Dies kann nicht rückgängig gemacht werden!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Alle Kommentare, Symbole und gezeichnete Formen in diesem Kapitel löschen'; + + @override + String get studyRightUnderTheBoard => 'Direkt unterhalb des Bretts'; + + @override + String get studyNoPinnedComment => 'Keine'; + + @override + String get studyNormalAnalysis => 'Normale Analyse'; + + @override + String get studyHideNextMoves => 'Nächste Züge ausblenden'; + + @override + String get studyInteractiveLesson => 'Interaktive Übung'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Leer'; + + @override + String get studyStartFromInitialPosition => 'Von Ausgangsstellung starten'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Von benutzerdefinierter Stellung starten'; + + @override + String get studyLoadAGameByUrl => 'Lade eine Partie mittels URL'; + + @override + String get studyLoadAPositionFromFen => 'Lade eine Partie mittels FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lade eine Partie mittels PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL der Partie'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Partie von $param1 oder $param2 laden'; + } + + @override + String get studyCreateChapter => 'Kapitel erstellen'; + + @override + String get studyCreateStudy => 'Studie erstellen'; + + @override + String get studyEditStudy => 'Studie bearbeiten'; + + @override + String get studyVisibility => 'Sichtbarkeit'; + + @override + String get studyPublic => 'Öffentlich'; + + @override + String get studyUnlisted => 'Ungelistet'; + + @override + String get studyInviteOnly => 'Nur mit Einladung'; + + @override + String get studyAllowCloning => 'Klonen erlaubt'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Nur ich'; + + @override + String get studyContributors => 'Mitwirkende'; + + @override + String get studyMembers => 'Mitglieder'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Sync aktivieren'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: Gleiche Stellung für alle'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nein: Unabhängige Navigation für alle'; + + @override + String get studyPinnedStudyComment => 'Angepinnter Studienkommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Speichern'; + + @override + String get studyClearChat => 'Chat löschen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Chatverlauf der Studie löschen? Dies kann nicht rückgängig gemacht werden!'; + + @override + String get studyDeleteStudy => 'Studie löschen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Die gesamte Studie löschen? Es gibt kein Zurück! Gib zur Bestätigung den Namen der Studie ein: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Welche Studie möchtest du nutzen?'; + + @override + String get studyGoodMove => 'Guter Zug'; + + @override + String get studyMistake => 'Fehler'; + + @override + String get studyBrilliantMove => 'Brillanter Zug'; + + @override + String get studyBlunder => 'Grober Patzer'; + + @override + String get studyInterestingMove => 'Interessanter Zug'; + + @override + String get studyDubiousMove => 'Fragwürdiger Zug'; + + @override + String get studyOnlyMove => 'Einziger Zug'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Ausgeglichene Stellung'; + + @override + String get studyUnclearPosition => 'Unklare Stellung'; + + @override + String get studyWhiteIsSlightlyBetter => 'Weiß steht leicht besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwarz steht leicht besser'; + + @override + String get studyWhiteIsBetter => 'Weiß steht besser'; + + @override + String get studyBlackIsBetter => 'Schwarz steht besser'; + + @override + String get studyWhiteIsWinning => 'Weiß steht auf Gewinn'; + + @override + String get studyBlackIsWinning => 'Schwarz steht auf Gewinn'; + + @override + String get studyNovelty => 'Neuerung'; + + @override + String get studyDevelopment => 'Entwicklung'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Angriff'; + + @override + String get studyCounterplay => 'Gegenspiel'; + + @override + String get studyTimeTrouble => 'Zeitnot'; + + @override + String get studyWithCompensation => 'Mit Kompensation'; + + @override + String get studyWithTheIdea => 'Mit der Idee'; + + @override + String get studyNextChapter => 'Nächstes Kapitel'; + + @override + String get studyPrevChapter => 'Vorheriges Kapitel'; + + @override + String get studyStudyActions => 'Studien-Aktionen'; + + @override + String get studyTopics => 'Themen'; + + @override + String get studyMyTopics => 'Meine Themen'; + + @override + String get studyPopularTopics => 'Beliebte Themen'; + + @override + String get studyManageTopics => 'Themen verwalten'; + + @override + String get studyBack => 'Zurück'; + + @override + String get studyPlayAgain => 'Erneut spielen'; + + @override + String get studyWhatWouldYouPlay => 'Was würdest du in dieser Stellung spielen?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulation! Du hast diese Lektion abgeschlossen.'; + + @override + String studyPerPage(String param) { + return '$param pro Seite'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partien', + one: '$count Partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Mitglieder', + one: '$count Mitglied', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Füge dein PGN Text hier ein, bis zu $count Partien', + one: 'Füge deinen PGN Text hier ein, bis zu $count Partie', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'gerade eben'; + + @override + String get timeagoRightNow => 'gerade jetzt'; + + @override + String get timeagoCompleted => 'abgeschlossen'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Sekunden', + one: 'in $count Sekunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Minuten', + one: 'in $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Stunden', + one: 'in $count Stunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Tagen', + one: 'in $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Wochen', + one: 'in $count Woche', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Monaten', + one: 'in $count Monat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Jahren', + one: 'in $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Minuten', + one: 'vor $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Stunden', + one: 'vor $count Stunde', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Tagen', + one: 'vor $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Wochen', + one: 'vor $count Woche', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Monaten', + one: 'vor $count Monat', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Jahren', + one: 'vor $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minuten verbleibend', + one: '$count Minute verbleibend', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Stunden verbleiben', + one: '$count Stunde verbleiben', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index d932457dea..88e3cf9bde 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsEl extends AppLocalizations { AppLocalizationsEl([String locale = 'el']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Όλα τα παιχνίδια'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Είστε σίγουροι;'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Ακυρώστε την προσφορά αναίρεσης της κίνησης'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Εκκαθάριση'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Εκκαθάριση αποθηκευμένης κίνησης'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Συμμετοχή σε παιχνίδι'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Πείτε μας τη γνώμη σας'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Καλωσορίσατε, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Καλωσορίσατε'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Απόκρυψη παραλλαγής'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Αρχική'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Streamers ζωντανά αυτή τη στιγμή'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'Δεν βρέθηκαν αποτελέσματα'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'Δεν ακολουθείτε κανέναν χρήστη.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'ΟΚ'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Παίκτες με \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; - - @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePrefMagnifyDraggedPiece => 'Μεγέθυνση του επιλεγμένου κομματιού'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormConfirmEndRun => 'Θέλετε να τερματίσετε αυτόν τον γύρο;'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormFilterNothingToShow => 'Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormNothingToShow => 'Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Γρίφοι'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Πρόσφατες αναζητήσεις'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsHapticFeedback => 'Απόκριση δόνησης'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Λειτουργία εστίασης'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Ρυθμίσεις'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGamePGN => 'Κοινοποίηση PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Κοινοποίηση URL παιχνιδιού'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Κοινοποίηση θέσης ως FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Κοινοποίηση γρίφου'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Εμφάνιση σχολίων'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Εμφάνιση αποτελέσματος'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Εμφάνιση παραλλαγών'; @override - String get mobileShowResult => 'Εμφάνιση αποτελέσματος'; + String get mobileSomethingWentWrong => 'Κάτι πήγε στραβά.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Χρώματα συστήματος'; @override - String get mobilePuzzleStormSubtitle => 'Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.'; + String get mobileTheme => 'Εμφάνιση'; @override - String mobileGreeting(String param) { - return 'Καλωσορίσατε, $param'; - } + String get mobileToolsTab => 'Εργαλεία'; @override - String get mobileGreetingWithoutName => 'Καλωσορίσατε'; + String get mobileWaitingForOpponentToJoin => 'Αναμονή για αντίπαλο...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Δείτε'; @override String get activityActivity => 'Δραστηριότητα'; @@ -246,6 +243,17 @@ class AppLocalizationsEl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ολοκλήρωσε $count παρτίδες αλληλογραφίας $param2', + one: 'Ολοκλήρωσε $count παρτίδα αλληλογραφίας $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEl extends AppLocalizations { @override String get broadcastBroadcasts => 'Αναμεταδόσεις'; + @override + String get broadcastMyBroadcasts => 'Οι αναμεταδόσεις μου'; + @override String get broadcastLiveBroadcasts => 'Αναμεταδόσεις ζωντανών τουρνούα'; + @override + String get broadcastBroadcastCalendar => 'Ημερολόγιο αναμεταδόσεων'; + + @override + String get broadcastNewBroadcast => 'Νέα ζωντανή αναμετάδοση'; + + @override + String get broadcastSubscribedBroadcasts => 'Εγγεγραμμένες μεταδώσεις'; + + @override + String get broadcastAboutBroadcasts => 'Σχετικά με εκπομπές'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Πώς να χρησιμοποιήσετε τις εκπομπές Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Ο νέος γύρος θα έχει τα ίδια μέλη και τους ίδιους συνεισφέροντες όπως ο προηγούμενος.'; + + @override + String get broadcastAddRound => 'Προσθήκη γύρου'; + + @override + String get broadcastOngoing => 'Σε εξέλιξη'; + + @override + String get broadcastUpcoming => 'Προσεχή'; + + @override + String get broadcastCompleted => 'Ολοκληρώθηκε'; + + @override + String get broadcastCompletedHelp => 'Το Lichess ανιχνεύει ολοκλήρωση γύρων, αλλά μπορεί να κάνει λάθος. Χρησιμοποιήστε αυτό για να το ρυθμίσετε χειροκίνητα.'; + + @override + String get broadcastRoundName => 'Όνομα γύρου'; + + @override + String get broadcastRoundNumber => 'Αριθμός γύρου'; + + @override + String get broadcastTournamentName => 'Όνομα τουρνουά'; + + @override + String get broadcastTournamentDescription => 'Σύντομη περιγραφή τουρνουά'; + + @override + String get broadcastFullDescription => 'Πλήρης περιγραφή γεγονότος'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Προαιρετική αναλυτική περιγραφή της αναμετάδοσης. Η μορφή $param1 είναι διαθέσιμη. Το μήκος πρέπει μικρότερο από $param2 χαρακτήρες.'; + } + + @override + String get broadcastSourceSingleUrl => 'Πηγαίο URL για PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Προαιρετικό, εάν γνωρίζετε πότε αρχίζει η εκδήλωση'; + + @override + String get broadcastCurrentGameUrl => 'Διεύθυνση URL αυτού του παιχνιδιού'; + + @override + String get broadcastDownloadAllRounds => 'Λήψη όλων των γύρων'; + + @override + String get broadcastResetRound => 'Επαναφορά αυτού του γύρου'; + + @override + String get broadcastDeleteRound => 'Διαγραφή αυτού του γύρου'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Σίγουρα διαγράψτε τον γύρο και όλα τα παιχνίδια του.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Διαγράψτε όλα τα παιχνίδια αυτού του γύρου. Η πηγή μετάδοσης θα πρέπει να είναι ενεργή για να τα ξαναδημιουργήσετε.'; + + @override + String get broadcastEditRoundStudy => 'Επεξεργασία μελέτης γύρου'; + + @override + String get broadcastDeleteTournament => 'Διαγραφή αυτού του τουρνουά'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Σίγουρα διαγράψτε ολόκληρο τον διαγωνισμό, όλους τους γύρους του και όλα τα παιχνίδια του.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'Ομοσπονδίες FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'Παίκτες FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Δε βρέθηκε παίκτης FIDE'; + + @override + String get broadcastFideProfile => 'Προφίλ FIDE'; + + @override + String get broadcastFederation => 'Ομοσπονδία'; + + @override + String get broadcastAgeThisYear => 'Φετινή ηλικία'; + + @override + String get broadcastUnrated => 'Μη βαθμολογημένο'; + + @override + String get broadcastRecentTournaments => 'Πρόσφατα τουρνουά'; + + @override + String get broadcastOpenLichess => 'Άνοιγμα στο Lichess'; + + @override + String get broadcastTeams => 'Ομάδες'; + + @override + String get broadcastBoards => 'Σκακιέρες'; + + @override + String get broadcastOverview => 'Επισκόπηση'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Ανεβάστε εικόνα τουρνουά'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Οι σκακιέρες μπορούν να φορτωθούν απο μια πηγή ή μέσω του $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Ξεκινάει μετά από $param'; + } + + @override + String get broadcastStartVerySoon => 'Η αναμετάδοση θα ξεκινήσει πολύ σύντομα.'; + + @override + String get broadcastNotYetStarted => 'Η αναμετάδοση δεν έχει ξεκινήσει ακόμα.'; + + @override + String get broadcastOfficialWebsite => 'Επίσημη ιστοσελίδα'; + + @override + String get broadcastStandings => 'Κατάταξη'; + + @override + String get broadcastOfficialStandings => 'Επίσημη Κατάταξη'; + + @override + String broadcastIframeHelp(String param) { + return 'Περισσότερες επιλογές στη $param'; + } + + @override + String get broadcastWebmastersPage => 'σελίδα για webmasters'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Μια δημόσια πηγή PGN πολύ λειτουργεί σε πραγματικό χρόνο για αυτόν τον γύρο. Προσφέρουμε επίσης το $param για γρηγορότερο και αποτελεσματικότερο συγχρονισμό.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Ενσωμάτωση αυτήν την αναμετάδοση στην ιστοσελίδα σας'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Ενσωματώστε τον $param στην ιστοσελίδα σας'; + } + + @override + String get broadcastRatingDiff => 'Διαφορά βαθμολογίας'; + + @override + String get broadcastGamesThisTournament => 'Παρτίδες σε αυτό το τουρνουά'; + + @override + String get broadcastScore => 'Βαθμολογία'; + + @override + String get broadcastAllTeams => 'Όλες οι ομάδες'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Τοποθεσία Τουρνουά'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Ζώνη ώρας'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Προαιρετικές λεπτομέρειες'; + + @override + String get broadcastPastBroadcasts => 'Προηγούμενες αναμετάδοσεις'; + + @override + String get broadcastAllBroadcastsByMonth => 'Προβολή όλων των αναμεταδόσεων ανά μήνα'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count αναμεταδόσεις', + one: '$count αναμετάδοση', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Προκλήσεις: $param1'; @@ -556,7 +811,7 @@ class AppLocalizationsEl extends AppLocalizations { String get preferencesDisplay => 'Εμφάνιση'; @override - String get preferencesPrivacy => 'Απόρρητο'; + String get preferencesPrivacy => 'Ιδιωτικότητα'; @override String get preferencesNotifications => 'Ειδοποιήσεις'; @@ -609,6 +864,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get preferencesInGameOnly => 'Μόνο κατά τη διάρκεια του παιχνιδιού'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Σκακιστικό χρονόμετρο'; @@ -670,7 +928,7 @@ class AppLocalizationsEl extends AppLocalizations { String get preferencesMoveConfirmation => 'Επιβεβαίωση κίνησης'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού του ταμπλό'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού της σκακιέρας'; @override String get preferencesInCorrespondenceGames => 'Στις παρτίδες δι\' αλληλογραφίας'; @@ -750,6 +1008,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ειδοποίηση με ήχο από καμπανάκι'; + @override + String get preferencesBlindfold => 'Τυφλό'; + @override String get puzzlePuzzles => 'Γρίφοι'; @@ -790,10 +1051,10 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzleVoteToLoadNextOne => 'Ψηφίστε για να προχωρήσετε στο επόμενο!'; @override - String get puzzleUpVote => 'Up vote puzzle'; + String get puzzleUpVote => 'Μου άρεσε ο γρίφος'; @override - String get puzzleDownVote => 'Down vote puzzle'; + String get puzzleDownVote => 'Δε μου άρεσε ο γρίφος'; @override String get puzzleYourPuzzleRatingWillNotChange => 'Οι βαθμοί αξιολόγησής σας δε θα αλλάξουν. Αυτοί οι βαθμοί χρησιμεύουν στην επιλογή γρίφων για το επίπεδό σας και όχι στον ανταγωνισμό.'; @@ -837,19 +1098,19 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzlePuzzleComplete => 'Ο γρίφος ολοκληρώθηκε!'; @override - String get puzzleByOpenings => 'By openings'; + String get puzzleByOpenings => 'Ανά άνοιγμα'; @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String get puzzlePuzzlesByOpenings => 'Γρίφοι ανά άνοιγμα'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; + String get puzzleOpeningsYouPlayedTheMost => 'Ανοίγματα που παίξατε πιο συχνά σε βαθμολογημένες παρτίδες σκάκι'; @override - String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favourite opening!'; + String get puzzleUseFindInPage => 'Πατήστε «Εύρεση στη σελίδα» στο μενού του προγράμματος περιήγησης, για να βρείτε το αγαπημένο σας άνοιγμα!'; @override - String get puzzleUseCtrlF => 'Use Ctrl+f to find your favourite opening!'; + String get puzzleUseCtrlF => 'Πατήστε Ctrl+f για να βρείτε το αγαπημένο σας άνοιγμα!'; @override String get puzzleNotTheMove => 'Δεν είναι αυτή η κίνηση!'; @@ -1390,10 +1651,10 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ο αντίπαλος είναι περιορισμένος στις κινήσεις που μπορεί να κάνει και οποιαδήποτε κίνηση επιλέξει επιδεινώνει την θέση του.'; @override - String get puzzleThemeHealthyMix => 'Προτεινόμενο μίγμα'; + String get puzzleThemeMix => 'Προτεινόμενο μίγμα'; @override - String get puzzleThemeHealthyMixDescription => 'Λίγο απ\' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.'; + String get puzzleThemeMixDescription => 'Λίγο απ\' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.'; @override String get puzzleThemePlayerGames => 'Παιχνίδια παίκτη'; @@ -1636,10 +1897,10 @@ class AppLocalizationsEl extends AppLocalizations { String get deleteFromHere => 'Διαγραφή από εδώ'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Σύμπτυξη παραλλαγών'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Εμφάνιση βαριάντων'; @override String get forceVariation => 'Θέσε σε βαριάντα'; @@ -1699,7 +1960,7 @@ class AppLocalizationsEl extends AppLocalizations { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'Δύο εκατομμύρια OTB παρτίδες $param1 + παικτών με αξιολόγηση FIDE από $param2 έως $param3'; + return 'Παρτίδες OTB $param1 + παικτών με αξιολόγηση FIDE, από $param2 έως $param3'; } @override @@ -1767,9 +2028,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get byCPL => 'Με CPL'; - @override - String get openStudy => 'Άνοιγμα μελέτης'; - @override String get enable => 'Ενεργοποίηση'; @@ -1797,9 +2055,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get removesTheDepthLimit => 'Καταργεί το όριο βάθους και κρατά τον υπολογιστή σας ζεστό'; - @override - String get engineManager => 'Διαχειριστής μηχανής'; - @override String get blunder => 'Σοβαρό σφάλμα'; @@ -1878,7 +2133,7 @@ class AppLocalizationsEl extends AppLocalizations { String get friends => 'Φίλοι'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'άλλους παίκτες'; @override String get discussions => 'Συζητήσεις'; @@ -2063,6 +2318,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get gamesPlayed => 'Παιγμένα παιχνίδια'; + @override + String get ok => 'ΟΚ'; + @override String get cancel => 'Ακύρωση'; @@ -2437,9 +2695,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get unblock => 'Κατάργηση απόκλεισης'; - @override - String get followsYou => 'Σας ακολουθεί'; - @override String xStartedFollowingY(String param1, String param2) { return 'Ο $param1 άρχισε να ακολουθεί τον $param2'; @@ -2631,7 +2886,7 @@ class AppLocalizationsEl extends AppLocalizations { String get editProfile => 'Επεξεργασία προφίλ'; @override - String get realName => 'Real name'; + String get realName => 'Πραγματικό όνομα'; @override String get setFlair => 'Ορίστε τη νιφάδα σας'; @@ -2712,10 +2967,10 @@ class AppLocalizationsEl extends AppLocalizations { String get yes => 'Ναι'; @override - String get website => 'Website'; + String get website => 'Ιστοσελίδα'; @override - String get mobile => 'Mobile'; + String get mobile => 'Εφαρμογή Κινητού'; @override String get help => 'Βοήθεια:'; @@ -2772,7 +3027,13 @@ class AppLocalizationsEl extends AppLocalizations { String get other => 'Άλλο'; @override - String get reportDescriptionHelp => 'Κάντε επικόλληση τον σύνδεσμο για το παιχνίδι(α) και εξηγήστε τι είναι παράξενο στη συμπεριφορά του χρήστη. Μην πείτε απλά «επειδή κλέβει», πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα. Η αναφορά σας θα επεξεργαστεί πιο γρήγορα αν είναι γραμμένη στα αγγλικά.'; + String get reportCheatBoostHelp => 'Επικολλήστε τους συνδέσμους με τα παιχνίδια και εξηγήστε μας γιατί θεωρείτε ότι η συμπεριφορά του χρήστη είναι παράξενη σε αυτά. Μη λέτε απλώς ότι «κλέβει» (\"they cheat\"), αλλά πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα.'; + + @override + String get reportUsernameHelp => 'Εξηγήστε μας γιατί είναι προσβλητικό το όνομα αυτού του χρήστη. Μη λέτε απλώς ότι \"είναι προσβλητικό/ακατάλληλο\" (\"it\'s offensive/inappropriate\"), αλλά πείτε μας πώς καταλήξατε σε αυτό το συμπέρασμα, ειδικά αν πρόκειται για προσβολή η οποία δεν είναι ιδιαίτερα εμφανής: για παράδειγμα αν δεν είναι στα αγγλικά, είναι σε κάποια αργκό ή κάνει κάποια προσβλητική ιστορική/πολιτιστική αναφορά.'; + + @override + String get reportProcessedFasterInEnglish => 'Η αναφορά σας θα επεξεργαστεί γρηγορότερα αν είναι γραμμένη στα αγγλικά.'; @override String get error_provideOneCheatedGameLink => 'Καταχωρίστε τουλάχιστον έναν σύνδεσμο σε ένα παιχνίδι εξαπάτησης.'; @@ -3152,7 +3413,7 @@ class AppLocalizationsEl extends AppLocalizations { String get lichessTournaments => 'Τουρνουά στο Lichess'; @override - String get tournamentFAQ => 'Τεκμηρίωση τουρνουά τύπου αρένας'; + String get tournamentFAQ => 'Τεκμηρίωση για τουρνουά τύπου αρένας'; @override String get timeBeforeTournamentStarts => 'Χρόνος προτού ξεκινήσει το τουρνουά'; @@ -3323,7 +3584,7 @@ class AppLocalizationsEl extends AppLocalizations { String get crosstable => 'Αποτελέσματα'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Μπορείτε επίσης να κινηθείτε πάνω στην σκακιέρα για να πάτε στο παιχνίδι.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Μπορείτε επίσης να κινήσετε πάνω στην σκακιέρα για να μετακινηθείτε στο παιχνίδι.'; @override String get scrollOverComputerVariationsToPreviewThem => 'Μετακινήστε το ποντίκι σας πάνω στις βαριάντες του υπολογιστή για την προεπισκόπησή τους.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get nothingToSeeHere => 'Τίποτα για να δείτε εδώ αυτή τη στιγμή.'; + @override + String get stats => 'Στατιστικά'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4724,8 +4988,692 @@ class AppLocalizationsEl extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streamers'; @override - String get studyShareAndExport => 'Διαμοιρασμός & εξαγωγή'; + String get studyPrivate => 'Ιδιωτικό'; @override - String get studyStart => 'Δημιουργία'; + String get studyMyStudies => 'Οι μελέτες μου'; + + @override + String get studyStudiesIContributeTo => 'Μελέτες που συνεισφέρω'; + + @override + String get studyMyPublicStudies => 'Οι δημόσιες μελέτες μου'; + + @override + String get studyMyPrivateStudies => 'Οι ιδιωτικές μελέτες μου'; + + @override + String get studyMyFavoriteStudies => 'Οι αγαπημένες μελέτες μου'; + + @override + String get studyWhatAreStudies => 'Τι είναι οι μελέτες;'; + + @override + String get studyAllStudies => 'Όλες οι μελέτες'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Μελέτες που δημιουργήθηκαν από τον/την $param'; + } + + @override + String get studyNoneYet => 'Τίποτα ακόμη εδώ.'; + + @override + String get studyHot => 'Δημοφιλείς (hot)'; + + @override + String get studyDateAddedNewest => 'Ημερομηνία προσθήκης (νεότερες)'; + + @override + String get studyDateAddedOldest => 'Ημερομηνία προσθήκης (παλαιότερες)'; + + @override + String get studyRecentlyUpdated => 'Πρόσφατα ενημερωμένες'; + + @override + String get studyMostPopular => 'Οι πιο δημοφιλείς'; + + @override + String get studyAlphabetical => 'Αλφαβητικά'; + + @override + String get studyAddNewChapter => 'Προσθήκη νέου κεφαλαίου'; + + @override + String get studyAddMembers => 'Προσθήκη μελών'; + + @override + String get studyInviteToTheStudy => 'Προσκάλεσε στην μελέτη'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Παρακαλώ, προσκαλέστε μόνο άτομα που γνωρίζετε και που θέλουν να συμμετέχουν ενεργά σε αυτήν την μελέτη.'; + + @override + String get studySearchByUsername => 'Αναζήτηση με όνομα χρήστη'; + + @override + String get studySpectator => 'Θεατής'; + + @override + String get studyContributor => 'Συνεισφέρων'; + + @override + String get studyKick => 'Διώξε'; + + @override + String get studyLeaveTheStudy => 'Αποχώρησε από αυτήν την μελέτη'; + + @override + String get studyYouAreNowAContributor => 'Μπορείτε τώρα να συνεισφέρετε στην μελέτη'; + + @override + String get studyYouAreNowASpectator => 'Είστε πλέον θεατής'; + + @override + String get studyPgnTags => 'PGN ετικέτες'; + + @override + String get studyLike => 'Μου αρέσει'; + + @override + String get studyUnlike => 'Δε μου αρέσει'; + + @override + String get studyNewTag => 'Νέα ετικέτα'; + + @override + String get studyCommentThisPosition => 'Σχολίασε την υπάρχουσα θέση'; + + @override + String get studyCommentThisMove => 'Σχολίασε αυτήν την κίνηση'; + + @override + String get studyAnnotateWithGlyphs => 'Σχολιασμός με σύμβολα'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Το κεφάλαιο είναι πολύ μικρό για να αναλυθεί.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Μόνο αυτοί που συνεισφέρουν στην σπουδή μπορούν να ζητήσουν ανάλυση από υπολογιστή.'; + + @override + String get studyGetAFullComputerAnalysis => 'Αίτηση πλήρης ανάλυσης της κύριας γραμμής παρτίδας από μηχανή του σέρβερ.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Σιγουρευτείτε ότι το κεφάλαιο είναι ολοκληρωμένο. Μπορείτε να ζητήσετε ανάλυση μόνο μια φορά.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Όλα τα συγχρονισμένα μέλη παραμένουν στην ίδια θέση'; + + @override + String get studyShareChanges => 'Διαμοιρασμός στους θεατές των αλλαγών και αποθήκευση τους στο σέρβερ'; + + @override + String get studyPlaying => 'Παίζονται'; + + @override + String get studyShowEvalBar => 'Μπάρες αξιολόγησης'; + + @override + String get studyFirst => 'Πρώτη'; + + @override + String get studyPrevious => 'Προηγούμενη'; + + @override + String get studyNext => 'Επόμενη'; + + @override + String get studyLast => 'Τελευταία'; + + @override + String get studyShareAndExport => 'Διαμοιρασμός & εξαγωγή'; + + @override + String get studyCloneStudy => 'Κλωνοποίησε'; + + @override + String get studyStudyPgn => 'PGN της μελέτης'; + + @override + String get studyDownloadAllGames => 'Λήψη όλων των παιχνιδιών'; + + @override + String get studyChapterPgn => 'PGN του κεφαλαίου'; + + @override + String get studyCopyChapterPgn => 'Αντιγραφή PGN'; + + @override + String get studyDownloadGame => 'Λήψη παιχνιδιού'; + + @override + String get studyStudyUrl => 'URL μελέτης'; + + @override + String get studyCurrentChapterUrl => 'Τρέχον κεφάλαιο URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Επικολλήστε το παρόν για ενσωμάτωση στο φόρουμ'; + + @override + String get studyStartAtInitialPosition => 'Ξεκινάει από αρχική θέση'; + + @override + String studyStartAtX(String param) { + return 'Ξεκινάει με $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ενσωματώστε στην ιστοσελίδα σας ή το μπλογκ σας'; + + @override + String get studyReadMoreAboutEmbedding => 'Διαβάστε περισσότερα για την ενσωμάτωση'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Μόνο δημόσιες μελέτες μπορούν να ενσωματωθούν!'; + + @override + String get studyOpen => 'Άνοιξε'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, δημιουργήθηκε από $param2'; + } + + @override + String get studyStudyNotFound => 'Η μελέτη δεν βρέθηκε'; + + @override + String get studyEditChapter => 'Επεξεργάσου το κεφάλαιο'; + + @override + String get studyNewChapter => 'Νέο κεφάλαιο'; + + @override + String studyImportFromChapterX(String param) { + return 'Εισαγωγή από $param'; + } + + @override + String get studyOrientation => 'Προσανατολισμός'; + + @override + String get studyAnalysisMode => 'Τύπος ανάλυσης'; + + @override + String get studyPinnedChapterComment => 'Καρφιτσωμένο σχόλιο κεφαλαίου'; + + @override + String get studySaveChapter => 'Αποθήκευση κεφαλαίου'; + + @override + String get studyClearAnnotations => 'Διαγραφή σχολιασμών'; + + @override + String get studyClearVariations => 'Εκκαθάριση βαριάντων'; + + @override + String get studyDeleteChapter => 'Διαγραφή κεφαλαίου'; + + @override + String get studyDeleteThisChapter => 'Διαγραφή κεφαλαίου; Μη αναιρέσιμη ενέργεια!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Καθαρισμός όλων των σχολίων, συμβόλων και σχεδίων στο τρέχον κεφάλαιο;'; + + @override + String get studyRightUnderTheBoard => 'Κάτω από την σκακιέρα'; + + @override + String get studyNoPinnedComment => 'Καμία'; + + @override + String get studyNormalAnalysis => 'Απλή ανάλυση'; + + @override + String get studyHideNextMoves => 'Απόκρυψη επόμενων κινήσεων'; + + @override + String get studyInteractiveLesson => 'Διαδραστικό μάθημα'; + + @override + String studyChapterX(String param) { + return 'Κεφάλαιο $param'; + } + + @override + String get studyEmpty => 'Κενή'; + + @override + String get studyStartFromInitialPosition => 'Έναρξη από τρέχουσα θέση'; + + @override + String get studyEditor => 'Επεξεργαστής'; + + @override + String get studyStartFromCustomPosition => 'Έναρξη από τρέχουσα θέση'; + + @override + String get studyLoadAGameByUrl => 'Φόρτωση παρτίδας με URL'; + + @override + String get studyLoadAPositionFromFen => 'Φόρτωση θέσης από FEN'; + + @override + String get studyLoadAGameFromPgn => 'Φόρτωσε μια παρτίδα από PGN'; + + @override + String get studyAutomatic => 'Αυτόματο'; + + @override + String get studyUrlOfTheGame => 'URL παρτίδων, ένα ανά γραμμή'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Φόρτωση παρτίδων από $param1 ή $param2'; + } + + @override + String get studyCreateChapter => 'Δημιουργία κεφαλαίου'; + + @override + String get studyCreateStudy => 'Δημιουργία μελέτης'; + + @override + String get studyEditStudy => 'Επεξεργασία μελέτης'; + + @override + String get studyVisibility => 'Ορατότητα'; + + @override + String get studyPublic => 'Δημόσια'; + + @override + String get studyUnlisted => 'Ακαταχώρητη'; + + @override + String get studyInviteOnly => 'Με πρόσκληση'; + + @override + String get studyAllowCloning => 'Επέτρεψε αντιγραφή'; + + @override + String get studyNobody => 'Κανένας'; + + @override + String get studyOnlyMe => 'Μόνο εγώ'; + + @override + String get studyContributors => 'Συνεισφέροντες'; + + @override + String get studyMembers => 'Μέλη'; + + @override + String get studyEveryone => 'Οποιοσδήποτε'; + + @override + String get studyEnableSync => 'Ενεργοποίηση συγχρονισμού'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ναι: όλοι βλέπουν την ίδια θέση'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Όχι: ελεύθερη επιλογή θέσης'; + + @override + String get studyPinnedStudyComment => 'Καρφιτσωμένο σχόλιο μελέτης'; + + @override + String get studyStart => 'Δημιουργία'; + + @override + String get studySave => 'Αποθήκευση'; + + @override + String get studyClearChat => 'Εκκαθάριση συνομιλίας'; + + @override + String get studyDeleteTheStudyChatHistory => 'Διαγραφή συνομιλίας μελέτης; Μη αναιρέσιμη ενέργεια!'; + + @override + String get studyDeleteStudy => 'Διαγραφή μελέτης'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Να διαγραφεί όλη η μελέτη; Η ενέργεια αυτή δεν μπορεί να αναιρεθεί! Πληκτρολογήστε το όνομα της μελέτης για επιβεβαίωση: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Που θέλετε να δημιουργήσετε την μελέτη;'; + + @override + String get studyGoodMove => 'Καλή κίνηση'; + + @override + String get studyMistake => 'Λάθος'; + + @override + String get studyBrilliantMove => 'Εξαιρετική κίνηση'; + + @override + String get studyBlunder => 'Σοβαρό λάθος'; + + @override + String get studyInterestingMove => 'Ενδιαφέρουσα κίνηση'; + + @override + String get studyDubiousMove => 'Κίνηση αμφίβολης αξίας'; + + @override + String get studyOnlyMove => 'Μοναδική κίνηση'; + + @override + String get studyZugzwang => 'Τσούγκσβανγκ'; + + @override + String get studyEqualPosition => 'Ισόπαλη θέση'; + + @override + String get studyUnclearPosition => 'Ασαφής θέση'; + + @override + String get studyWhiteIsSlightlyBetter => 'Το λευκά είναι ελαφρώς καλύτερα'; + + @override + String get studyBlackIsSlightlyBetter => 'Το μαύρα είναι ελαφρώς καλύτερα'; + + @override + String get studyWhiteIsBetter => 'Τα λευκά είναι καλύτερα'; + + @override + String get studyBlackIsBetter => 'Τα μαύρα είναι καλύτερα'; + + @override + String get studyWhiteIsWinning => 'Τα λευκά κερδίζουν'; + + @override + String get studyBlackIsWinning => 'Τα μαύρα κερδίζουν'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Ανάπτυξη'; + + @override + String get studyInitiative => 'Πρωτοβουλία'; + + @override + String get studyAttack => 'Επίθεση'; + + @override + String get studyCounterplay => 'Αντεπίθεση'; + + @override + String get studyTimeTrouble => 'Πίεση χρόνου'; + + @override + String get studyWithCompensation => 'Με αντάλλαγμα'; + + @override + String get studyWithTheIdea => 'Με ιδέα'; + + @override + String get studyNextChapter => 'Επόμενο κεφάλαιο'; + + @override + String get studyPrevChapter => 'Προηγούμενο κεφάλαιο'; + + @override + String get studyStudyActions => 'Ρυθμίσεις μελέτης'; + + @override + String get studyTopics => 'Θέματα'; + + @override + String get studyMyTopics => 'Τα θέματά μου'; + + @override + String get studyPopularTopics => 'Δημοφιλή θέματα'; + + @override + String get studyManageTopics => 'Διαχείριση θεμάτων'; + + @override + String get studyBack => 'Πίσω'; + + @override + String get studyPlayAgain => 'Παίξτε ξανά'; + + @override + String get studyWhatWouldYouPlay => 'Τι θα παίζατε σε αυτή τη θέση;'; + + @override + String get studyYouCompletedThisLesson => 'Συγχαρητήρια! Ολοκληρώσατε αυτό το μάθημα.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Κεφάλαια', + one: '$count Κεφάλαιο', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Παρτίδες', + one: '$count Παρτίδα', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Μέλη', + one: '$count Μέλος', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Επικολλήστε το PGN εδώ, μέχρι $count παρτίδες', + one: 'Επικολλήστε το PGN εδώ, μέχρι $count παρτίδα', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'μόλις τώρα'; + + @override + String get timeagoRightNow => 'αυτή τη στιγμή'; + + @override + String get timeagoCompleted => 'ολοκληρώθηκε'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count δευτερόλεπτα', + one: 'σε $count δευτερόλεπτο', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count λεπτά', + one: 'σε $count λεπτό', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count ώρες', + one: 'σε $count ώρα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count ημέρες', + one: 'σε $count ημέρα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count εβδομάδες', + one: 'σε $count εβδομάδα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count μήνες', + one: 'σε $count μήνα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count έτη', + one: 'σε $count έτος', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count λεπτά πριν', + one: '$count λεπτό πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ώρες πριν', + one: '$count ώρα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ημέρες πριν', + one: '$count μέρα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count εβδομάδες πριν', + one: '$count εβδομάδα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count μήνες πριν', + one: '$count μήνα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count χρόνια πριν', + one: '$count χρόνο πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'απομένουν $count λεπτά', + one: 'απομένει $count λεπτό', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'απομένουν $count ώρες', + one: 'απομένει $count ώρα', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index 949d04a151..8686a89b3c 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Activity'; @@ -246,6 +243,17 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEn extends AppLocalizations { @override String get broadcastBroadcasts => 'Broadcasts'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'New live broadcast'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Ongoing'; + + @override + String get broadcastUpcoming => 'Upcoming'; + + @override + String get broadcastCompleted => 'Completed'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Round number'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Full tournament description'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Chess clock'; @@ -750,6 +1008,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Puzzles'; @@ -1388,10 +1649,10 @@ class AppLocalizationsEn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'Healthy mix'; + String get puzzleThemeMix => 'Healthy mix'; @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1765,9 +2026,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get byCPL => 'By CPL'; - @override - String get openStudy => 'Open study'; - @override String get enable => 'Enable'; @@ -1795,9 +2053,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Blunder'; @@ -2061,6 +2316,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get gamesPlayed => 'Games played'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancel'; @@ -2435,9 +2693,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get unblock => 'Unblock'; - @override - String get followsYou => 'Follows you'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 started following $param2'; @@ -2770,7 +3025,13 @@ class AppLocalizationsEn extends AppLocalizations { String get other => 'Other'; @override - String get reportDescriptionHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; @@ -4075,6 +4336,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4722,4726 +4986,6363 @@ class AppLocalizationsEn extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streamers'; @override - String get studyShareAndExport => 'Share & export'; + String get studyPrivate => 'Private'; @override - String get studyStart => 'Start'; -} + String get studyMyStudies => 'My studies'; -/// The translations for English, as used in the United States (`en_US`). -class AppLocalizationsEnUs extends AppLocalizationsEn { - AppLocalizationsEnUs(): super('en_US'); + @override + String get studyStudiesIContributeTo => 'Studies I contribute to'; @override - String get mobileHomeTab => 'Home'; + String get studyMyPublicStudies => 'My public studies'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get studyMyPrivateStudies => 'My private studies'; @override - String get mobileToolsTab => 'Tools'; + String get studyMyFavoriteStudies => 'My favourite studies'; @override - String get mobileWatchTab => 'Watch'; + String get studyWhatAreStudies => 'What are studies?'; @override - String get mobileSettingsTab => 'Settings'; + String get studyAllStudies => 'All studies'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; + } @override - String get mobileSystemColors => 'System colors'; + String get studyNoneYet => 'None yet.'; @override - String get mobileFeedbackButton => 'Feedback'; + String get studyHot => 'Hot'; @override - String get mobileOkButton => 'OK'; + String get studyDateAddedNewest => 'Date added (newest)'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get studyDateAddedOldest => 'Date added (oldest)'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get studyRecentlyUpdated => 'Recently updated'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get studyMostPopular => 'Most popular'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get studyAlphabetical => 'Alphabetical'; @override - String get mobileAllGames => 'All games'; + String get studyAddNewChapter => 'Add a new chapter'; @override - String get mobileRecentSearches => 'Recent searches'; + String get studyAddMembers => 'Add members'; @override - String get mobileClearButton => 'Clear'; + String get studyInviteToTheStudy => 'Invite to the study'; @override - String get mobileNoSearchResults => 'No results'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get studySearchByUsername => 'Search by username'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; + String get studySpectator => 'Spectator'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get studyContributor => 'Contributor'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get studyKick => 'Kick'; @override - String get mobileShareGameURL => 'Share game URL'; + String get studyLeaveTheStudy => 'Leave the study'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get studyYouAreNowAContributor => 'You are now a contributor'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get studyYouAreNowASpectator => 'You are now a spectator'; @override - String get mobileShowVariations => 'Show variations'; + String get studyPgnTags => 'PGN tags'; @override - String get mobileHideVariation => 'Hide variation'; + String get studyLike => 'Like'; @override - String get mobileShowComments => 'Show comments'; + String get studyUnlike => 'Unlike'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get studyNewTag => 'New tag'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get studyCommentThisPosition => 'Comment on this position'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get studyCommentThisMove => 'Comment on this move'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analysed.'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get studyShareChanges => 'Share changes with spectators and save them on the server'; @override - String get mobileShowResult => 'Show result'; + String get studyPlaying => 'Playing'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get studyShowEvalBar => 'Evaluation bars'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get studyFirst => 'First'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get studyPrevious => 'Previous'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get studyNext => 'Next'; @override - String get activityActivity => 'Activity'; + String get studyLast => 'Last'; @override - String get activityHostedALiveStream => 'Hosted a live stream'; + String get studyShareAndExport => 'Share & export'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return 'Ranked #$param1 in $param2'; - } + String get studyCloneStudy => 'Clone'; @override - String get activitySignedUp => 'Signed up to lichess.org'; + String get studyStudyPgn => 'Study PGN'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Supported lichess.org for $count months as a $param2', - one: 'Supported lichess.org for $count month as a $param2', - ); - return '$_temp0'; - } + String get studyDownloadAllGames => 'Download all games'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Practiced $count positions on $param2', - one: 'Practiced $count position on $param2', - ); - return '$_temp0'; - } + String get studyChapterPgn => 'Chapter PGN'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Solved $count tactical puzzles', - one: 'Solved $count tactical puzzle', - ); - return '$_temp0'; - } + String get studyCopyChapterPgn => 'Copy PGN'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Played $count $param2 games', - one: 'Played $count $param2 game', - ); - return '$_temp0'; - } + String get studyDownloadGame => 'Download game'; @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Posted $count messages in $param2', - one: 'Posted $count message in $param2', - ); - return '$_temp0'; - } + String get studyStudyUrl => 'Study URL'; @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Played $count moves', - one: 'Played $count move', - ); - return '$_temp0'; - } + String get studyCurrentChapterUrl => 'Current chapter URL'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'in $count correspondence games', - one: 'in $count correspondence game', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Completed $count correspondence games', - one: 'Completed $count correspondence game', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Start at initial position'; @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Started following $count players', - one: 'Started following $count player', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Start at $param'; } @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Gained $count new followers', - one: 'Gained $count new follower', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Embed in your website'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Hosted $count simultaneous exhibitions', - one: 'Hosted $count simultaneous exhibition', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Participated in $count simultaneous exhibitions', - one: 'Participated in $count simultaneous exhibition', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Created $count new studies', - one: 'Created $count new study', - ); - return '$_temp0'; - } + String get studyOpen => 'Open'; @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competed in $count tournaments', - one: 'Competed in $count tournament', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; } @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Ranked #$count (top $param2%) with $param3 games in $param4', - one: 'Ranked #$count (top $param2%) with $param3 game in $param4', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Study not found'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competed in $count Swiss tournaments', - one: 'Competed in $count Swiss tournament', - ); - return '$_temp0'; - } + String get studyEditChapter => 'Edit chapter'; @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Joined $count teams', - one: 'Joined $count team', - ); - return '$_temp0'; + String get studyNewChapter => 'New chapter'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; } @override - String get broadcastBroadcasts => 'Broadcasts'; + String get studyOrientation => 'Orientation'; @override - String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; + String get studyAnalysisMode => 'Analysis mode'; @override - String challengeChallengesX(String param1) { - return 'Challenges: $param1'; - } + String get studyPinnedChapterComment => 'Pinned chapter comment'; @override - String get challengeChallengeToPlay => 'Challenge to a game'; + String get studySaveChapter => 'Save chapter'; @override - String get challengeChallengeDeclined => 'Challenge declined'; + String get studyClearAnnotations => 'Clear annotations'; @override - String get challengeChallengeAccepted => 'Challenge accepted!'; + String get studyClearVariations => 'Clear variations'; @override - String get challengeChallengeCanceled => 'Challenge canceled.'; + String get studyDeleteChapter => 'Delete chapter'; @override - String get challengeRegisterToSendChallenges => 'Please register to send challenges.'; + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; @override - String challengeYouCannotChallengeX(String param) { - return 'You cannot challenge $param.'; - } + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param does not accept challenges.'; - } + String get studyRightUnderTheBoard => 'Right under the board'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'Your $param1 rating is too far from $param2.'; - } + String get studyNoPinnedComment => 'None'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Cannot challenge due to provisional $param rating.'; - } + String get studyNormalAnalysis => 'Normal analysis'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param only accepts challenges from friends.'; - } + String get studyHideNextMoves => 'Hide next moves'; @override - String get challengeDeclineGeneric => 'I\'m not accepting challenges at the moment.'; + String get studyInteractiveLesson => 'Interactive lesson'; @override - String get challengeDeclineLater => 'This is not the right time for me, please ask again later.'; + String studyChapterX(String param) { + return 'Chapter $param'; + } @override - String get challengeDeclineTooFast => 'This time control is too fast for me, please challenge again with a slower game.'; + String get studyEmpty => 'Empty'; @override - String get challengeDeclineTooSlow => 'This time control is too slow for me, please challenge again with a faster game.'; + String get studyStartFromInitialPosition => 'Start from initial position'; @override - String get challengeDeclineTimeControl => 'I\'m not accepting challenges with this time control.'; + String get studyEditor => 'Editor'; @override - String get challengeDeclineRated => 'Please send me a rated challenge instead.'; + String get studyStartFromCustomPosition => 'Start from custom position'; @override - String get challengeDeclineCasual => 'Please send me a casual challenge instead.'; + String get studyLoadAGameByUrl => 'Load games by URLs'; @override - String get challengeDeclineStandard => 'I\'m not accepting variant challenges right now.'; + String get studyLoadAPositionFromFen => 'Load a position from FEN'; @override - String get challengeDeclineVariant => 'I\'m not willing to play this variant right now.'; + String get studyLoadAGameFromPgn => 'Load games from PGN'; @override - String get challengeDeclineNoBot => 'I\'m not accepting challenges from bots.'; + String get studyAutomatic => 'Automatic'; @override - String get challengeDeclineOnlyBot => 'I\'m only accepting challenges from bots.'; + String get studyUrlOfTheGame => 'URL of the games, one per line'; @override - String get challengeInviteLichessUser => 'Or invite a Lichess user:'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; + } @override - String get contactContact => 'Contact'; + String get studyCreateChapter => 'Create chapter'; @override - String get contactContactLichess => 'Contact Lichess'; + String get studyCreateStudy => 'Create study'; @override - String get patronDonate => 'Donate'; + String get studyEditStudy => 'Edit study'; @override - String get patronLichessPatron => 'Lichess Patron'; + String get studyVisibility => 'Visibility'; @override - String perfStatPerfStats(String param) { - return '$param stats'; - } + String get studyPublic => 'Public'; @override - String get perfStatViewTheGames => 'View the games'; + String get studyUnlisted => 'Unlisted'; @override - String get perfStatProvisional => 'provisional'; + String get studyInviteOnly => 'Invite only'; @override - String get perfStatNotEnoughRatedGames => 'Not enough rated games have been played to establish a reliable rating.'; + String get studyAllowCloning => 'Allow cloning'; @override - String perfStatProgressOverLastXGames(String param) { - return 'Progression over the last $param games:'; - } + String get studyNobody => 'Nobody'; @override - String perfStatRatingDeviation(String param) { - return 'Rating deviation: $param.'; - } + String get studyOnlyMe => 'Only me'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Lower value means the rating is more stable. Above $param1, the rating is considered provisional. To be included in the rankings, this value should be below $param2 (standard chess) or $param3 (variants).'; - } + String get studyContributors => 'Contributors'; @override - String get perfStatTotalGames => 'Total games'; + String get studyMembers => 'Members'; @override - String get perfStatRatedGames => 'Rated games'; + String get studyEveryone => 'Everyone'; @override - String get perfStatTournamentGames => 'Tournament games'; + String get studyEnableSync => 'Enable sync'; @override - String get perfStatBerserkedGames => 'Berserked games'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; @override - String get perfStatTimeSpentPlaying => 'Time spent playing'; + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; @override - String get perfStatAverageOpponent => 'Average opponent'; + String get studyPinnedStudyComment => 'Pinned study comment'; @override - String get perfStatVictories => 'Victories'; + String get studyStart => 'Start'; @override - String get perfStatDefeats => 'Defeats'; + String get studySave => 'Save'; @override - String get perfStatDisconnections => 'Disconnections'; + String get studyClearChat => 'Clear chat'; @override - String get perfStatNotEnoughGames => 'Not enough games played'; + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; @override - String perfStatHighestRating(String param) { - return 'Highest rating: $param'; - } + String get studyDeleteStudy => 'Delete study'; @override - String perfStatLowestRating(String param) { - return 'Lowest rating: $param'; + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; } @override - String perfStatFromXToY(String param1, String param2) { - return 'from $param1 to $param2'; - } + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; @override - String get perfStatWinningStreak => 'Winning streak'; + String get studyGoodMove => 'Good move'; @override - String get perfStatLosingStreak => 'Losing streak'; + String get studyMistake => 'Mistake'; @override - String perfStatLongestStreak(String param) { - return 'Longest streak: $param'; - } + String get studyBrilliantMove => 'Brilliant move'; @override - String perfStatCurrentStreak(String param) { - return 'Current streak: $param'; - } + String get studyBlunder => 'Blunder'; @override - String get perfStatBestRated => 'Best rated victories'; + String get studyInterestingMove => 'Interesting move'; @override - String get perfStatGamesInARow => 'Games played in a row'; + String get studyDubiousMove => 'Dubious move'; @override - String get perfStatLessThanOneHour => 'Less than one hour between games'; + String get studyOnlyMove => 'Only move'; @override - String get perfStatMaxTimePlaying => 'Max time spent playing'; + String get studyZugzwang => 'Zugzwang'; @override - String get perfStatNow => 'now'; + String get studyEqualPosition => 'Equal position'; @override - String get preferencesPreferences => 'Preferences'; + String get studyUnclearPosition => 'Unclear position'; @override - String get preferencesDisplay => 'Display'; + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; @override - String get preferencesPrivacy => 'Privacy'; + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; @override - String get preferencesNotifications => 'Notifications'; + String get studyWhiteIsBetter => 'White is better'; @override - String get preferencesPieceAnimation => 'Piece animation'; + String get studyBlackIsBetter => 'Black is better'; @override - String get preferencesMaterialDifference => 'Material difference'; + String get studyWhiteIsWinning => 'White is winning'; @override - String get preferencesBoardHighlights => 'Board highlights (last move and check)'; + String get studyBlackIsWinning => 'Black is winning'; @override - String get preferencesPieceDestinations => 'Piece destinations (valid moves and premoves)'; + String get studyNovelty => 'Novelty'; @override - String get preferencesBoardCoordinates => 'Board coordinates (A-H, 1-8)'; + String get studyDevelopment => 'Development'; @override - String get preferencesMoveListWhilePlaying => 'Move list while playing'; + String get studyInitiative => 'Initiative'; @override - String get preferencesPgnPieceNotation => 'Move notation'; + String get studyAttack => 'Attack'; @override - String get preferencesChessPieceSymbol => 'Chess piece symbol'; + String get studyCounterplay => 'Counterplay'; @override - String get preferencesPgnLetter => 'Letter (K, Q, R, B, N)'; + String get studyTimeTrouble => 'Time trouble'; @override - String get preferencesZenMode => 'Zen mode'; + String get studyWithCompensation => 'With compensation'; @override - String get preferencesShowPlayerRatings => 'Show player ratings'; + String get studyWithTheIdea => 'With the idea'; @override - String get preferencesShowFlairs => 'Show player flairs'; + String get studyNextChapter => 'Next chapter'; @override - String get preferencesExplainShowPlayerRatings => 'This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see.'; + String get studyPrevChapter => 'Previous chapter'; @override - String get preferencesDisplayBoardResizeHandle => 'Show board resize handle'; + String get studyStudyActions => 'Study actions'; @override - String get preferencesOnlyOnInitialPosition => 'Only on initial position'; + String get studyTopics => 'Topics'; @override - String get preferencesInGameOnly => 'In-game only'; + String get studyMyTopics => 'My topics'; @override - String get preferencesChessClock => 'Chess clock'; + String get studyPopularTopics => 'Popular topics'; @override - String get preferencesTenthsOfSeconds => 'Tenths of seconds'; + String get studyManageTopics => 'Manage topics'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'When time remaining < 10 seconds'; + String get studyBack => 'Back'; @override - String get preferencesHorizontalGreenProgressBars => 'Horizontal green progress bars'; + String get studyPlayAgain => 'Play again'; @override - String get preferencesSoundWhenTimeGetsCritical => 'Sound when time gets critical'; + String get studyWhatWouldYouPlay => 'What would you play in this position?'; @override - String get preferencesGiveMoreTime => 'Give more time'; + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; @override - String get preferencesGameBehavior => 'Game behavior'; + String studyPerPage(String param) { + return '$param per page'; + } @override - String get preferencesHowDoYouMovePieces => 'How do you move pieces?'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chapters', + one: '$count Chapter', + ); + return '$_temp0'; + } @override - String get preferencesClickTwoSquares => 'Click two squares'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Games', + one: '$count Game', + ); + return '$_temp0'; + } @override - String get preferencesDragPiece => 'Drag a piece'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Members', + one: '$count Member', + ); + return '$_temp0'; + } @override - String get preferencesBothClicksAndDrag => 'Either'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', + ); + return '$_temp0'; + } @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Premoves (playing during opponent turn)'; + String get timeagoJustNow => 'just now'; @override - String get preferencesTakebacksWithOpponentApproval => 'Takebacks (with opponent approval)'; + String get timeagoRightNow => 'right now'; @override - String get preferencesInCasualGamesOnly => 'In casual games only'; + String get timeagoCompleted => 'completed'; @override - String get preferencesPromoteToQueenAutomatically => 'Promote to Queen automatically'; + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } @override - String get preferencesExplainPromoteToQueenAutomatically => 'Hold the key while promoting to temporarily disable auto-promotion'; + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } @override - String get preferencesWhenPremoving => 'When premoving'; + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Claim draw on threefold repetition automatically'; + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'When time remaining < 30 seconds'; + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } @override - String get preferencesMoveConfirmation => 'Move confirmation'; + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } @override - String get preferencesInCorrespondenceGames => 'Correspondence games'; + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } @override - String get preferencesCorrespondenceAndUnlimited => 'Correspondence and unlimited'; + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirm resignation and draw offers'; + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Castling method'; + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingTwoSquares => 'Move king two squares'; + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingOntoTheRook => 'Move king onto rook'; + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } @override - String get preferencesInputMovesWithTheKeyboard => 'Input moves with the keyboard'; + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } @override - String get preferencesInputMovesWithVoice => 'Input moves with your voice'; + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } +} - @override - String get preferencesSnapArrowsToValidMoves => 'Snap arrows to valid moves'; +/// The translations for English, as used in the United States (`en_US`). +class AppLocalizationsEnUs extends AppLocalizationsEn { + AppLocalizationsEnUs(): super('en_US'); @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Say \"Good game, well played\" upon defeat or draw'; + String get mobileAllGames => 'All games'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'Your preferences have been saved.'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Scroll on the board to replay moves'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get preferencesCorrespondenceEmailNotification => 'Daily mail notification listing your correspondence games'; + String get mobileClearButton => 'Clear'; @override - String get preferencesNotifyStreamStart => 'Streamer goes live'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get preferencesNotifyInboxMsg => 'New inbox message'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get preferencesNotifyForumMention => 'Forum comment mentions you'; + String get mobileFeedbackButton => 'Feedback'; @override - String get preferencesNotifyInvitedStudy => 'Study invite'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get preferencesNotifyGameEvent => 'Correspondence game updates'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get preferencesNotifyChallenge => 'Challenges'; + String get mobileHideVariation => 'Hide variation'; @override - String get preferencesNotifyTournamentSoon => 'Tournament starting soon'; + String get mobileHomeTab => 'Home'; @override - String get preferencesNotifyTimeAlarm => 'Correspondence time running out'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get preferencesNotifyBell => 'Bell notification within Lichess'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get preferencesNotifyPush => 'Device notification when you\'re not on Lichess'; + String get mobileNoSearchResults => 'No results'; @override - String get preferencesNotifyWeb => 'Browser'; - - @override - String get preferencesNotifyDevice => 'Device'; - - @override - String get preferencesBellNotificationSound => 'Bell notification sound'; - - @override - String get puzzlePuzzles => 'Chess Puzzles'; - - @override - String get puzzlePuzzleThemes => 'Puzzle Themes'; - - @override - String get puzzleRecommended => 'Recommended'; - - @override - String get puzzlePhases => 'Phases'; - - @override - String get puzzleMotifs => 'Motifs'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get puzzleAdvanced => 'Advanced'; + String get mobileOkButton => 'OK'; @override - String get puzzleLengths => 'Lengths'; + String mobilePlayersMatchingSearchTerm(String param) { + return 'Players with \"$param\"'; + } @override - String get puzzleMates => 'Mates'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get puzzleGoals => 'Goals'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get puzzleOrigin => 'Origin'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override - String get puzzleSpecialMoves => 'Special moves'; + String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get puzzleDidYouLikeThisPuzzle => 'Did you like this puzzle?'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get puzzleVoteToLoadNextOne => 'Vote to load the next one!'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; @override - String get puzzleUpVote => 'Upvote puzzle'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get puzzleDownVote => 'Downvote puzzle'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get puzzleYourPuzzleRatingWillNotChange => 'Your puzzle rating will not change. Note that puzzles are not a competition. Ratings help select the best puzzles for your current skill.'; + String get mobileRecentSearches => 'Recent searches'; @override - String get puzzleFindTheBestMoveForWhite => 'Find the best move for white.'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get puzzleFindTheBestMoveForBlack => 'Find the best move for black.'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get puzzleToGetPersonalizedPuzzles => 'To get personalized puzzles:'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String puzzlePuzzleId(String param) { - return 'Puzzle $param'; - } + String get mobileSettingsTab => 'Settings'; @override - String get puzzlePuzzleOfTheDay => 'Puzzle of the day'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get puzzleDailyPuzzle => 'Daily Puzzle'; + String get mobileShareGameURL => 'Share game URL'; @override - String get puzzleClickToSolve => 'Click to solve'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get puzzleGoodMove => 'Good move'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get puzzleBestMove => 'Best move!'; + String get mobileShowComments => 'Show comments'; @override - String get puzzleKeepGoing => 'Keep going…'; + String get mobileShowResult => 'Show result'; @override - String get puzzlePuzzleSuccess => 'Success!'; + String get mobileShowVariations => 'Show variations'; @override - String get puzzlePuzzleComplete => 'Puzzle complete!'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get puzzleByOpenings => 'By openings'; + String get mobileSystemColors => 'System colors'; @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String get mobileTheme => 'Theme'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; + String get mobileToolsTab => 'Tools'; @override - String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favorite opening!'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get puzzleUseCtrlF => 'Use Ctrl+f to find your favorite opening!'; + String get mobileWatchTab => 'Watch'; @override - String get puzzleNotTheMove => 'That\'s not the move!'; + String get activityActivity => 'Activity'; @override - String get puzzleTrySomethingElse => 'Try something else.'; + String get activityHostedALiveStream => 'Hosted a live stream'; @override - String puzzleRatingX(String param) { - return 'Rating: $param'; + String activityRankedInSwissTournament(String param1, String param2) { + return 'Ranked #$param1 in $param2'; } @override - String get puzzleHidden => 'hidden'; + String get activitySignedUp => 'Signed up to lichess.org'; @override - String puzzleFromGameLink(String param) { - return 'From game $param'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Supported lichess.org for $count months as a $param2', + one: 'Supported lichess.org for $count month as a $param2', + ); + return '$_temp0'; } @override - String get puzzleContinueTraining => 'Continue training'; - - @override - String get puzzleDifficultyLevel => 'Difficulty level'; - - @override - String get puzzleNormal => 'Normal'; - - @override - String get puzzleEasier => 'Easier'; - - @override - String get puzzleEasiest => 'Easiest'; - - @override - String get puzzleHarder => 'Harder'; - - @override - String get puzzleHardest => 'Hardest'; - - @override - String get puzzleExample => 'Example'; - - @override - String get puzzleAddAnotherTheme => 'Add another theme'; - - @override - String get puzzleNextPuzzle => 'Next puzzle'; - - @override - String get puzzleJumpToNextPuzzleImmediately => 'Jump to next puzzle immediately'; - - @override - String get puzzlePuzzleDashboard => 'Puzzle Dashboard'; - - @override - String get puzzleImprovementAreas => 'Improvement areas'; - - @override - String get puzzleStrengths => 'Strengths'; - - @override - String get puzzleHistory => 'Puzzle history'; - - @override - String get puzzleSolved => 'solved'; - - @override - String get puzzleFailed => 'failed'; - - @override - String get puzzleStreakDescription => 'Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it\'s game over! But you can skip one move per session.'; - - @override - String puzzleYourStreakX(String param) { - return 'Your streak: $param'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Practiced $count positions on $param2', + one: 'Practiced $count position on $param2', + ); + return '$_temp0'; } @override - String get puzzleStreakSkipExplanation => 'Skip this move to preserve your streak! Only works once per run.'; - - @override - String get puzzleContinueTheStreak => 'Continue the streak'; - - @override - String get puzzleNewStreak => 'New streak'; - - @override - String get puzzleFromMyGames => 'From my games'; - - @override - String get puzzleLookupOfPlayer => 'Search puzzles from a player\'s games'; - - @override - String puzzleFromXGames(String param) { - return 'Puzzles from $param\'s games'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Solved $count tactical puzzles', + one: 'Solved $count tactical puzzle', + ); + return '$_temp0'; } @override - String get puzzleSearchPuzzles => 'Search puzzles'; - - @override - String get puzzleFromMyGamesNone => 'You have no puzzles in the database, but Lichess still loves you very much.\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!'; - - @override - String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 puzzles found in $param2 games'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count $param2 games', + one: 'Played $count $param2 game', + ); + return '$_temp0'; } @override - String get puzzlePuzzleDashboardDescription => 'Train, analyse, improve'; - - @override - String puzzlePercentSolved(String param) { - return '$param solved'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Posted $count messages in $param2', + one: 'Posted $count message in $param2', + ); + return '$_temp0'; } @override - String get puzzleNoPuzzlesToShow => 'Nothing to show, go play some puzzles first!'; - - @override - String get puzzleImprovementAreasDescription => 'Train these to optimize your progress!'; - - @override - String get puzzleStrengthDescription => 'You perform the best in these themes'; - - @override - String puzzlePlayedXTimes(int count) { + String activityPlayedNbMoves(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Played $count times', - one: 'Played $count time', + other: 'Played $count moves', + one: 'Played $count move', ); return '$_temp0'; } @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { + String activityInNbCorrespondenceGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count points below your puzzle rating', - one: 'One point below your puzzle rating', + other: 'in $count correspondence games', + one: 'in $count correspondence game', ); return '$_temp0'; } @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { + String activityCompletedNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count points above your puzzle rating', - one: 'One point above your puzzle rating', + other: 'Completed $count correspondence games', + one: 'Completed $count correspondence game', ); return '$_temp0'; } @override - String puzzleNbPlayed(int count) { + String activityCompletedNbVariantGames(int count, String param2) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count played', - one: '$count played', + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', ); return '$_temp0'; } @override - String puzzleNbToReplay(int count) { + String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count to replay', - one: '$count to replay', + other: 'Started following $count players', + one: 'Started following $count player', ); return '$_temp0'; } @override - String get puzzleThemeAdvancedPawn => 'Advanced pawn'; - - @override - String get puzzleThemeAdvancedPawnDescription => 'One of your pawns is deep into the opponent position, maybe threatening to promote.'; + String activityGainedNbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Gained $count new followers', + one: 'Gained $count new follower', + ); + return '$_temp0'; + } @override - String get puzzleThemeAdvantage => 'Advantage'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hosted $count simultaneous exhibitions', + one: 'Hosted $count simultaneous exhibition', + ); + return '$_temp0'; + } @override - String get puzzleThemeAdvantageDescription => 'Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp)'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Participated in $count simultaneous exhibitions', + one: 'Participated in $count simultaneous exhibition', + ); + return '$_temp0'; + } @override - String get puzzleThemeAnastasiaMate => 'Anastasia\'s mate'; + String activityCreatedNbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Created $count new studies', + one: 'Created $count new study', + ); + return '$_temp0'; + } @override - String get puzzleThemeAnastasiaMateDescription => 'A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece.'; + String activityCompetedInNbTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competed in $count Arena tournaments', + one: 'Competed in $count Arena tournament', + ); + return '$_temp0'; + } @override - String get puzzleThemeArabianMate => 'Arabian mate'; + String activityRankedInTournament(int count, String param2, String param3, String param4) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ranked #$count (top $param2%) with $param3 games in $param4', + one: 'Ranked #$count (top $param2%) with $param3 game in $param4', + ); + return '$_temp0'; + } @override - String get puzzleThemeArabianMateDescription => 'A knight and a rook team up to trap the opposing king on a corner of the board.'; + String activityCompetedInNbSwissTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competed in $count Swiss tournaments', + one: 'Competed in $count Swiss tournament', + ); + return '$_temp0'; + } @override - String get puzzleThemeAttackingF2F7 => 'Attacking f2 or f7'; + String activityJoinedNbTeams(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Joined $count teams', + one: 'Joined $count team', + ); + return '$_temp0'; + } @override - String get puzzleThemeAttackingF2F7Description => 'An attack focusing on the f2 or f7 pawn, such as in the fried liver opening.'; + String get broadcastBroadcasts => 'Broadcasts'; @override - String get puzzleThemeAttraction => 'Attraction'; + String get broadcastMyBroadcasts => 'My broadcasts'; @override - String get puzzleThemeAttractionDescription => 'An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic.'; + String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; @override - String get puzzleThemeBackRankMate => 'Back rank mate'; + String get broadcastBroadcastCalendar => 'Broadcast calendar'; @override - String get puzzleThemeBackRankMateDescription => 'Checkmate the king on the home rank, when it is trapped there by its own pieces.'; + String get broadcastNewBroadcast => 'New live broadcast'; @override - String get puzzleThemeBishopEndgame => 'Bishop endgame'; + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; @override - String get puzzleThemeBishopEndgameDescription => 'An endgame with only bishops and pawns.'; + String get broadcastAboutBroadcasts => 'About broadcasts'; @override - String get puzzleThemeBodenMate => 'Boden\'s mate'; + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; @override - String get puzzleThemeBodenMateDescription => 'Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces.'; + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; @override - String get puzzleThemeCastling => 'Castling'; + String get broadcastAddRound => 'Add a round'; @override - String get puzzleThemeCastlingDescription => 'Bring the king to safety, and deploy the rook for attack.'; + String get broadcastOngoing => 'Ongoing'; @override - String get puzzleThemeCapturingDefender => 'Capture the defender'; + String get broadcastUpcoming => 'Upcoming'; @override - String get puzzleThemeCapturingDefenderDescription => 'Removing a piece that is critical to defense of another piece, allowing the now undefended piece to be captured on a following move.'; + String get broadcastCompleted => 'Completed'; @override - String get puzzleThemeCrushing => 'Crushing'; + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; @override - String get puzzleThemeCrushingDescription => 'Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp)'; + String get broadcastRoundName => 'Round name'; @override - String get puzzleThemeDoubleBishopMate => 'Double bishop mate'; + String get broadcastRoundNumber => 'Round number'; @override - String get puzzleThemeDoubleBishopMateDescription => 'Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces.'; + String get broadcastTournamentName => 'Tournament name'; @override - String get puzzleThemeDovetailMate => 'Dovetail mate'; + String get broadcastTournamentDescription => 'Short tournament description'; @override - String get puzzleThemeDovetailMateDescription => 'A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces.'; + String get broadcastFullDescription => 'Full tournament description'; @override - String get puzzleThemeEquality => 'Equality'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } @override - String get puzzleThemeEqualityDescription => 'Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp)'; + String get broadcastSourceSingleUrl => 'PGN Source URL'; @override - String get puzzleThemeKingsideAttack => 'Kingside attack'; + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; @override - String get puzzleThemeKingsideAttackDescription => 'An attack of the opponent\'s king, after they castled on the king side.'; + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; @override - String get puzzleThemeClearance => 'Clearance'; + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } @override - String get puzzleThemeClearanceDescription => 'A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea.'; + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; @override - String get puzzleThemeDefensiveMove => 'Defensive move'; + String get broadcastCurrentGameUrl => 'Current game URL'; @override - String get puzzleThemeDefensiveMoveDescription => 'A precise move or sequence of moves that is needed to avoid losing material or another advantage.'; + String get broadcastDownloadAllRounds => 'Download all rounds'; @override - String get puzzleThemeDeflection => 'Deflection'; + String get broadcastResetRound => 'Reset this round'; @override - String get puzzleThemeDeflectionDescription => 'A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called \"overloading\".'; + String get broadcastDeleteRound => 'Delete this round'; @override - String get puzzleThemeDiscoveredAttack => 'Discovered attack'; + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Moving a piece that previously blocked an attack by another long range piece, such as a knight out of the way of a rook.'; + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; @override - String get puzzleThemeDoubleCheck => 'Double check'; + String get broadcastEditRoundStudy => 'Edit round study'; @override - String get puzzleThemeDoubleCheckDescription => 'Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent\'s king.'; + String get broadcastDeleteTournament => 'Delete this tournament'; @override - String get puzzleThemeEndgame => 'Endgame'; + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; @override - String get puzzleThemeEndgameDescription => 'A tactic during the last phase of the game.'; + String get broadcastShowScores => 'Show players\' scores based on game results'; @override - String get puzzleThemeEnPassantDescription => 'A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move.'; + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; @override - String get puzzleThemeExposedKing => 'Exposed king'; + String get broadcastFideFederations => 'FIDE federations'; @override - String get puzzleThemeExposedKingDescription => 'A tactic involving a king with few defenders around it, often leading to checkmate.'; + String get broadcastTop10Rating => 'Top 10 rating'; @override - String get puzzleThemeFork => 'Fork'; + String get broadcastFidePlayers => 'FIDE players'; @override - String get puzzleThemeForkDescription => 'A move where the moved piece attacks two opponent pieces at once.'; + String get broadcastFidePlayerNotFound => 'FIDE player not found'; @override - String get puzzleThemeHangingPiece => 'Hanging piece'; + String get broadcastFideProfile => 'FIDE profile'; @override - String get puzzleThemeHangingPieceDescription => 'A tactic involving an opponent piece being undefended or insufficiently defended and free to capture.'; + String get broadcastFederation => 'Federation'; @override - String get puzzleThemeHookMate => 'Hook mate'; + String get broadcastAgeThisYear => 'Age this year'; @override - String get puzzleThemeHookMateDescription => 'Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king\'s escape.'; + String get broadcastUnrated => 'Unrated'; @override - String get puzzleThemeInterference => 'Interference'; + String get broadcastRecentTournaments => 'Recent tournaments'; @override - String get puzzleThemeInterferenceDescription => 'Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks.'; + String get broadcastOpenLichess => 'Open in Lichess'; @override - String get puzzleThemeIntermezzo => 'Intermezzo'; + String get broadcastTeams => 'Teams'; @override - String get puzzleThemeIntermezzoDescription => 'Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as \"Zwischenzug\" or \"In between\".'; + String get broadcastBoards => 'Boards'; @override - String get puzzleThemeKnightEndgame => 'Knight endgame'; + String get broadcastOverview => 'Overview'; @override - String get puzzleThemeKnightEndgameDescription => 'An endgame with only knights and pawns.'; + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; @override - String get puzzleThemeLong => 'Long puzzle'; + String get broadcastUploadImage => 'Upload tournament image'; @override - String get puzzleThemeLongDescription => 'Three moves to win.'; + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; @override - String get puzzleThemeMaster => 'Master games'; + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } @override - String get puzzleThemeMasterDescription => 'Puzzles from games played by titled players.'; + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } @override - String get puzzleThemeMasterVsMaster => 'Master vs Master games'; + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; @override - String get puzzleThemeMasterVsMasterDescription => 'Puzzles from games between two titled players.'; + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; @override - String get puzzleThemeMate => 'Checkmate'; + String get broadcastOfficialWebsite => 'Official website'; @override - String get puzzleThemeMateDescription => 'Win the game with style.'; + String get broadcastStandings => 'Standings'; @override - String get puzzleThemeMateIn1 => 'Mate in 1'; + String get broadcastOfficialStandings => 'Official Standings'; @override - String get puzzleThemeMateIn1Description => 'Deliver checkmate in one move.'; + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } @override - String get puzzleThemeMateIn2 => 'Mate in 2'; + String get broadcastWebmastersPage => 'webmasters page'; @override - String get puzzleThemeMateIn2Description => 'Deliver checkmate in two moves.'; + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronization.'; + } @override - String get puzzleThemeMateIn3 => 'Mate in 3'; + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; @override - String get puzzleThemeMateIn3Description => 'Deliver checkmate in three moves.'; + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } @override - String get puzzleThemeMateIn4 => 'Mate in 4'; + String get broadcastRatingDiff => 'Rating diff'; @override - String get puzzleThemeMateIn4Description => 'Deliver checkmate in four moves.'; + String get broadcastGamesThisTournament => 'Games in this tournament'; @override - String get puzzleThemeMateIn5 => 'Mate in 5 or more'; + String get broadcastScore => 'Score'; @override - String get puzzleThemeMateIn5Description => 'Figure out a long mating sequence.'; + String get broadcastAllTeams => 'All teams'; @override - String get puzzleThemeMiddlegame => 'Middlegame'; + String get broadcastTournamentFormat => 'Tournament format'; @override - String get puzzleThemeMiddlegameDescription => 'A tactic during the second phase of the game.'; + String get broadcastTournamentLocation => 'Tournament Location'; @override - String get puzzleThemeOneMove => 'One-move puzzle'; + String get broadcastTopPlayers => 'Top players'; @override - String get puzzleThemeOneMoveDescription => 'A puzzle that is only one move long.'; + String get broadcastTimezone => 'Time zone'; @override - String get puzzleThemeOpening => 'Opening'; + String get broadcastFideRatingCategory => 'FIDE rating category'; @override - String get puzzleThemeOpeningDescription => 'A tactic during the first phase of the game.'; + String get broadcastOptionalDetails => 'Optional details'; @override - String get puzzleThemePawnEndgame => 'Pawn endgame'; + String get broadcastPastBroadcasts => 'Past broadcasts'; @override - String get puzzleThemePawnEndgameDescription => 'An endgame with only pawns.'; + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; @override - String get puzzleThemePin => 'Pin'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } @override - String get puzzleThemePinDescription => 'A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece.'; + String challengeChallengesX(String param1) { + return 'Challenges: $param1'; + } @override - String get puzzleThemePromotion => 'Promotion'; + String get challengeChallengeToPlay => 'Challenge to a game'; @override - String get puzzleThemePromotionDescription => 'Promote one of your pawns to a queen or minor piece.'; + String get challengeChallengeDeclined => 'Challenge declined.'; @override - String get puzzleThemeQueenEndgame => 'Queen endgame'; + String get challengeChallengeAccepted => 'Challenge accepted!'; @override - String get puzzleThemeQueenEndgameDescription => 'An endgame with only queens and pawns.'; + String get challengeChallengeCanceled => 'Challenge canceled.'; @override - String get puzzleThemeQueenRookEndgame => 'Queen and Rook'; + String get challengeRegisterToSendChallenges => 'Please register to send challenges to this user.'; @override - String get puzzleThemeQueenRookEndgameDescription => 'An endgame with only queens, rooks, and pawns.'; + String challengeYouCannotChallengeX(String param) { + return 'You cannot challenge $param.'; + } @override - String get puzzleThemeQueensideAttack => 'Queenside attack'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param does not accept challenges.'; + } @override - String get puzzleThemeQueensideAttackDescription => 'An attack of the opponent\'s king, after they castled on the queen side.'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return 'Your $param1 rating is too far from $param2.'; + } @override - String get puzzleThemeQuietMove => 'Quiet move'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return 'Cannot challenge due to provisional $param rating.'; + } @override - String get puzzleThemeQuietMoveDescription => 'A move that does not make a check or capture, but does prepare an unavoidable threat for a later move.'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param only accepts challenges from friends.'; + } @override - String get puzzleThemeRookEndgame => 'Rook endgame'; + String get challengeDeclineGeneric => 'I\'m not accepting challenges at the moment.'; @override - String get puzzleThemeRookEndgameDescription => 'An endgame with only rooks and pawns.'; + String get challengeDeclineLater => 'This is not the right time for me, please ask again later.'; @override - String get puzzleThemeSacrifice => 'Sacrifice'; + String get challengeDeclineTooFast => 'This time control is too fast for me, please challenge again with a slower game.'; @override - String get puzzleThemeSacrificeDescription => 'A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves.'; + String get challengeDeclineTooSlow => 'This time control is too slow for me, please challenge again with a faster game.'; @override - String get puzzleThemeShort => 'Short puzzle'; + String get challengeDeclineTimeControl => 'I\'m not accepting challenges with this time control.'; @override - String get puzzleThemeShortDescription => 'Two moves to win.'; + String get challengeDeclineRated => 'Please send me a rated challenge instead.'; @override - String get puzzleThemeSkewer => 'Skewer'; + String get challengeDeclineCasual => 'Please send me a casual challenge instead.'; @override - String get puzzleThemeSkewerDescription => 'A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin.'; + String get challengeDeclineStandard => 'I\'m not accepting variant challenges right now.'; @override - String get puzzleThemeSmotheredMate => 'Smothered mate'; + String get challengeDeclineVariant => 'I\'m not willing to play this variant right now.'; @override - String get puzzleThemeSmotheredMateDescription => 'A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces.'; + String get challengeDeclineNoBot => 'I\'m not accepting challenges from bots.'; @override - String get puzzleThemeSuperGM => 'Super GM games'; + String get challengeDeclineOnlyBot => 'I\'m only accepting challenges from bots.'; @override - String get puzzleThemeSuperGMDescription => 'Puzzles from games played by the best players in the world.'; + String get challengeInviteLichessUser => 'Or invite a Lichess user:'; @override - String get puzzleThemeTrappedPiece => 'Trapped piece'; + String get contactContact => 'Contact'; @override - String get puzzleThemeTrappedPieceDescription => 'A piece is unable to escape capture as it has limited moves.'; + String get contactContactLichess => 'Contact Lichess'; @override - String get puzzleThemeUnderPromotion => 'Underpromotion'; + String get patronDonate => 'Donate'; @override - String get puzzleThemeUnderPromotionDescription => 'Promotion to a knight, bishop, or rook.'; + String get patronLichessPatron => 'Lichess Patron'; @override - String get puzzleThemeVeryLong => 'Very long puzzle'; + String perfStatPerfStats(String param) { + return '$param stats'; + } @override - String get puzzleThemeVeryLongDescription => 'Four moves or more to win.'; + String get perfStatViewTheGames => 'View the games'; @override - String get puzzleThemeXRayAttack => 'X-Ray attack'; + String get perfStatProvisional => 'provisional'; @override - String get puzzleThemeXRayAttackDescription => 'A piece attacks or defends a square, through an enemy piece.'; + String get perfStatNotEnoughRatedGames => 'Not enough rated games have been played to establish a reliable rating.'; @override - String get puzzleThemeZugzwang => 'Zugzwang'; + String perfStatProgressOverLastXGames(String param) { + return 'Progression over the last $param games:'; + } @override - String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; + String perfStatRatingDeviation(String param) { + return 'Rating deviation: $param.'; + } @override - String get puzzleThemeHealthyMix => 'Healthy mix'; - - @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return 'Lower value means the rating is more stable. Above $param1, the rating is considered provisional. To be included in the rankings, this value should be below $param2 (standard chess) or $param3 (variants).'; + } @override - String get puzzleThemePlayerGames => 'Player games'; + String get perfStatTotalGames => 'Total games'; @override - String get puzzleThemePlayerGamesDescription => 'Lookup puzzles generated from your games, or from another player\'s games.'; + String get perfStatRatedGames => 'Rated games'; @override - String puzzleThemePuzzleDownloadInformation(String param) { - return 'These puzzles are in the public domain, and can be downloaded from $param.'; - } + String get perfStatTournamentGames => 'Tournament games'; @override - String get searchSearch => 'Search'; + String get perfStatBerserkedGames => 'Berserked games'; @override - String get settingsSettings => 'Settings'; + String get perfStatTimeSpentPlaying => 'Time spent playing'; @override - String get settingsCloseAccount => 'Close account'; + String get perfStatAverageOpponent => 'Average opponent'; @override - String get settingsManagedAccountCannotBeClosed => 'Your account is managed and cannot be closed.'; + String get perfStatVictories => 'Victories'; @override - String get settingsClosingIsDefinitive => 'Closing is definitive. There is no going back. Are you sure?'; + String get perfStatDefeats => 'Defeats'; @override - String get settingsCantOpenSimilarAccount => 'You will not be allowed to open a new account with the same name, even if the case is different.'; + String get perfStatDisconnections => 'Disconnections'; @override - String get settingsChangedMindDoNotCloseAccount => 'I changed my mind, don\'t close my account'; + String get perfStatNotEnoughGames => 'Not enough games played'; @override - String get settingsCloseAccountExplanation => 'Are you sure you want to close your account? Closing your account is a permanent decision. You will NEVER be able to log in EVER AGAIN.'; + String perfStatHighestRating(String param) { + return 'Highest rating: $param'; + } @override - String get settingsThisAccountIsClosed => 'This account is closed.'; + String perfStatLowestRating(String param) { + return 'Lowest rating: $param'; + } @override - String get playWithAFriend => 'Play with a friend'; + String perfStatFromXToY(String param1, String param2) { + return 'from $param1 to $param2'; + } @override - String get playWithTheMachine => 'Play with the computer'; + String get perfStatWinningStreak => 'Winning streak'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'To invite someone to play, give this URL'; + String get perfStatLosingStreak => 'Losing streak'; @override - String get gameOver => 'Game Over'; + String perfStatLongestStreak(String param) { + return 'Longest streak: $param'; + } @override - String get waitingForOpponent => 'Waiting for opponent'; + String perfStatCurrentStreak(String param) { + return 'Current streak: $param'; + } @override - String get orLetYourOpponentScanQrCode => 'Or let your opponent scan this QR code'; + String get perfStatBestRated => 'Best rated victories'; @override - String get waiting => 'Waiting'; + String get perfStatGamesInARow => 'Games played in a row'; @override - String get yourTurn => 'Your turn'; + String get perfStatLessThanOneHour => 'Less than one hour between games'; @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1 level $param2'; - } + String get perfStatMaxTimePlaying => 'Max time spent playing'; @override - String get level => 'Level'; + String get perfStatNow => 'now'; @override - String get strength => 'Strength'; + String get preferencesPreferences => 'Preferences'; @override - String get toggleTheChat => 'Toggle the chat'; + String get preferencesDisplay => 'Display'; @override - String get chat => 'Chat'; + String get preferencesPrivacy => 'Privacy'; @override - String get resign => 'Resign'; + String get preferencesNotifications => 'Notifications'; @override - String get checkmate => 'Checkmate'; + String get preferencesPieceAnimation => 'Piece animation'; @override - String get stalemate => 'Stalemate'; + String get preferencesMaterialDifference => 'Material difference'; @override - String get white => 'White'; + String get preferencesBoardHighlights => 'Board highlights (last move and check)'; @override - String get black => 'Black'; + String get preferencesPieceDestinations => 'Piece destinations (valid moves and premoves)'; @override - String get asWhite => 'as white'; + String get preferencesBoardCoordinates => 'Board coordinates (A-H, 1-8)'; @override - String get asBlack => 'as black'; + String get preferencesMoveListWhilePlaying => 'Move list while playing'; @override - String get randomColor => 'Random side'; + String get preferencesPgnPieceNotation => 'Move notation'; @override - String get createAGame => 'Create a game'; + String get preferencesChessPieceSymbol => 'Chess piece symbol'; @override - String get whiteIsVictorious => 'White is victorious'; + String get preferencesPgnLetter => 'Letter (K, Q, R, B, N)'; @override - String get blackIsVictorious => 'Black is victorious'; + String get preferencesZenMode => 'Zen mode'; @override - String get youPlayTheWhitePieces => 'You play the white pieces'; + String get preferencesShowPlayerRatings => 'Show player ratings'; @override - String get youPlayTheBlackPieces => 'You play the black pieces'; + String get preferencesShowFlairs => 'Show player flairs'; @override - String get itsYourTurn => 'It\'s your turn!'; + String get preferencesExplainShowPlayerRatings => 'This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see.'; @override - String get cheatDetected => 'Cheat Detected'; + String get preferencesDisplayBoardResizeHandle => 'Show board resize handle'; @override - String get kingInTheCenter => 'King in the center'; + String get preferencesOnlyOnInitialPosition => 'Only on initial position'; @override - String get threeChecks => 'Three checks'; + String get preferencesInGameOnly => 'In-game only'; @override - String get raceFinished => 'Race finished'; + String get preferencesChessClock => 'Chess clock'; @override - String get variantEnding => 'Variant ending'; + String get preferencesTenthsOfSeconds => 'Tenths of seconds'; @override - String get newOpponent => 'New opponent'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'When time remaining < 10 seconds'; @override - String get yourOpponentWantsToPlayANewGameWithYou => 'Your opponent wants to play a new game with you'; + String get preferencesHorizontalGreenProgressBars => 'Horizontal green progress bars'; @override - String get joinTheGame => 'Join the game'; + String get preferencesSoundWhenTimeGetsCritical => 'Sound when time gets critical'; @override - String get whitePlays => 'White to play'; + String get preferencesGiveMoreTime => 'Give more time'; @override - String get blackPlays => 'Black to play'; + String get preferencesGameBehavior => 'Game behavior'; @override - String get opponentLeftChoices => 'Your opponent left the game. You can claim victory, call the game a draw, or wait.'; + String get preferencesHowDoYouMovePieces => 'How do you move pieces?'; @override - String get forceResignation => 'Claim victory'; + String get preferencesClickTwoSquares => 'Click two squares'; @override - String get forceDraw => 'Call draw'; + String get preferencesDragPiece => 'Drag a piece'; @override - String get talkInChat => 'Please be nice in the chat!'; + String get preferencesBothClicksAndDrag => 'Either'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'The first person to come to this URL will play with you.'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Premoves (playing during opponent turn)'; @override - String get whiteResigned => 'White resigned'; + String get preferencesTakebacksWithOpponentApproval => 'Takebacks (with opponent approval)'; @override - String get blackResigned => 'Black resigned'; + String get preferencesInCasualGamesOnly => 'In casual games only'; @override - String get whiteLeftTheGame => 'White left the game'; + String get preferencesPromoteToQueenAutomatically => 'Promote to Queen automatically'; @override - String get blackLeftTheGame => 'Black left the game'; + String get preferencesExplainPromoteToQueenAutomatically => 'Hold the key while promoting to temporarily disable auto-promotion'; @override - String get whiteDidntMove => 'White didn\'t move'; + String get preferencesWhenPremoving => 'When premoving'; @override - String get blackDidntMove => 'Black didn\'t move'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Claim draw on threefold repetition automatically'; @override - String get requestAComputerAnalysis => 'Request a computer analysis'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'When time remaining < 30 seconds'; @override - String get computerAnalysis => 'Computer analysis'; + String get preferencesMoveConfirmation => 'Move confirmation'; @override - String get computerAnalysisAvailable => 'Computer analysis available'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; @override - String get computerAnalysisDisabled => 'Computer analysis disabled'; + String get preferencesInCorrespondenceGames => 'Correspondence games'; @override - String get analysis => 'Analysis board'; + String get preferencesCorrespondenceAndUnlimited => 'Correspondence and unlimited'; @override - String depthX(String param) { - return 'Depth $param'; - } + String get preferencesConfirmResignationAndDrawOffers => 'Confirm resignation and draw offers'; @override - String get usingServerAnalysis => 'Using server analysis'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Castling method'; @override - String get loadingEngine => 'Loading engine ...'; + String get preferencesCastleByMovingTwoSquares => 'Move king two squares'; @override - String get calculatingMoves => 'Calculating moves...'; + String get preferencesCastleByMovingOntoTheRook => 'Move king onto rook'; @override - String get engineFailed => 'Error loading engine'; + String get preferencesInputMovesWithTheKeyboard => 'Input moves with the keyboard'; @override - String get cloudAnalysis => 'Cloud analysis'; + String get preferencesInputMovesWithVoice => 'Input moves with your voice'; @override - String get goDeeper => 'Go deeper'; + String get preferencesSnapArrowsToValidMoves => 'Snap arrows to valid moves'; @override - String get showThreat => 'Show threat'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Say \"Good game, well played\" upon defeat or draw'; @override - String get inLocalBrowser => 'in local browser'; + String get preferencesYourPreferencesHaveBeenSaved => 'Your preferences have been saved.'; @override - String get toggleLocalEvaluation => 'Toggle local evaluation'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Scroll on the board to replay moves'; @override - String get promoteVariation => 'Promote variation'; + String get preferencesCorrespondenceEmailNotification => 'Daily mail notification listing your correspondence games'; @override - String get makeMainLine => 'Make main line'; + String get preferencesNotifyStreamStart => 'Streamer goes live'; @override - String get deleteFromHere => 'Delete from here'; + String get preferencesNotifyInboxMsg => 'New inbox message'; @override - String get collapseVariations => 'Collapse variations'; + String get preferencesNotifyForumMention => 'Forum comment mentions you'; @override - String get expandVariations => 'Expand variations'; + String get preferencesNotifyInvitedStudy => 'Study invite'; @override - String get forceVariation => 'Force variation'; + String get preferencesNotifyGameEvent => 'Correspondence game updates'; @override - String get copyVariationPgn => 'Copy variation PGN'; + String get preferencesNotifyChallenge => 'Challenges'; @override - String get move => 'Move'; + String get preferencesNotifyTournamentSoon => 'Tournament starting soon'; @override - String get variantLoss => 'Variant loss'; + String get preferencesNotifyTimeAlarm => 'Correspondence time running out'; @override - String get variantWin => 'Variant win'; + String get preferencesNotifyBell => 'Bell notification within Lichess'; @override - String get insufficientMaterial => 'Insufficient material'; + String get preferencesNotifyPush => 'Device notification when you\'re not on Lichess'; @override - String get pawnMove => 'Pawn move'; + String get preferencesNotifyWeb => 'Browser'; @override - String get capture => 'Capture'; + String get preferencesNotifyDevice => 'Device'; @override - String get close => 'Close'; + String get preferencesBellNotificationSound => 'Bell notification sound'; @override - String get winning => 'Winning'; + String get preferencesBlindfold => 'Blindfold'; @override - String get losing => 'Losing'; + String get puzzlePuzzles => 'Puzzles'; @override - String get drawn => 'Drawn'; + String get puzzlePuzzleThemes => 'Puzzle Themes'; @override - String get unknown => 'Unknown'; + String get puzzleRecommended => 'Recommended'; @override - String get database => 'Database'; + String get puzzlePhases => 'Phases'; @override - String get whiteDrawBlack => 'White / Draw / Black'; + String get puzzleMotifs => 'Motifs'; @override - String averageRatingX(String param) { - return 'Average rating: $param'; - } + String get puzzleAdvanced => 'Advanced'; @override - String get recentGames => 'Recent games'; + String get puzzleLengths => 'Lengths'; @override - String get topGames => 'Top games'; + String get puzzleMates => 'Mates'; @override - String masterDbExplanation(String param1, String param2, String param3) { - return 'OTB games of $param1+ FIDE rated players from $param2 to $param3'; - } + String get puzzleGoals => 'Goals'; @override - String get dtzWithRounding => 'DTZ50\'\' with rounding, based on number of half-moves until next capture or pawn move'; + String get puzzleOrigin => 'Origin'; @override - String get noGameFound => 'No game found'; + String get puzzleSpecialMoves => 'Special moves'; @override - String get maxDepthReached => 'Max depth reached!'; + String get puzzleDidYouLikeThisPuzzle => 'Did you like this puzzle?'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Maybe include more games from the preferences menu?'; + String get puzzleVoteToLoadNextOne => 'Vote to load the next one!'; @override - String get openings => 'Openings'; + String get puzzleUpVote => 'Upvote puzzle'; @override - String get openingExplorer => 'Opening explorer'; + String get puzzleDownVote => 'Downvote puzzle'; @override - String get openingEndgameExplorer => 'Opening/endgame explorer'; + String get puzzleYourPuzzleRatingWillNotChange => 'Your puzzle rating will not change. Note that puzzles are not a competition. Your rating helps selecting the best puzzles for your current skill.'; @override - String xOpeningExplorer(String param) { - return '$param opening explorer'; - } + String get puzzleFindTheBestMoveForWhite => 'Find the best move for white.'; @override - String get playFirstOpeningEndgameExplorerMove => 'Play first opening/endgame-explorer move'; + String get puzzleFindTheBestMoveForBlack => 'Find the best move for black.'; @override - String get winPreventedBy50MoveRule => 'Win prevented by 50-move rule'; + String get puzzleToGetPersonalizedPuzzles => 'To get personalized puzzles:'; @override - String get lossSavedBy50MoveRule => 'Loss prevented by 50-move rule'; + String puzzlePuzzleId(String param) { + return 'Puzzle $param'; + } @override - String get winOr50MovesByPriorMistake => 'Win or 50 moves by prior mistake'; + String get puzzlePuzzleOfTheDay => 'Puzzle of the day'; @override - String get lossOr50MovesByPriorMistake => 'Loss or 50 moves by prior mistake'; + String get puzzleDailyPuzzle => 'Daily Puzzle'; @override - String get unknownDueToRounding => 'Due to possible rounding of DTZ values in Syzygy tablebases, a win/loss is only guaranteed if the recommended tablebase line has been followed since the last capture or pawn move.'; + String get puzzleClickToSolve => 'Click to solve'; @override - String get allSet => 'All set!'; + String get puzzleGoodMove => 'Good move'; @override - String get importPgn => 'Import PGN'; + String get puzzleBestMove => 'Best move!'; @override - String get delete => 'Delete'; + String get puzzleKeepGoing => 'Keep going…'; @override - String get deleteThisImportedGame => 'Delete this imported game?'; + String get puzzlePuzzleSuccess => 'Success!'; @override - String get replayMode => 'Replay mode'; + String get puzzlePuzzleComplete => 'Puzzle complete!'; @override - String get realtimeReplay => 'Realtime'; + String get puzzleByOpenings => 'By openings'; @override - String get byCPL => 'By CPL'; + String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; @override - String get openStudy => 'Open study'; + String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; @override - String get enable => 'Enable'; + String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favorite opening!'; @override - String get bestMoveArrow => 'Best move arrow'; + String get puzzleUseCtrlF => 'Use Ctrl+f to find your favorite opening!'; @override - String get showVariationArrows => 'Show variation arrows'; + String get puzzleNotTheMove => 'That\'s not the move!'; @override - String get evaluationGauge => 'Evaluation gauge'; + String get puzzleTrySomethingElse => 'Try something else.'; @override - String get multipleLines => 'Multiple lines'; + String puzzleRatingX(String param) { + return 'Rating: $param'; + } @override - String get cpus => 'CPUs'; + String get puzzleHidden => 'hidden'; @override - String get memory => 'Memory'; + String puzzleFromGameLink(String param) { + return 'From game $param'; + } @override - String get infiniteAnalysis => 'Infinite analysis'; + String get puzzleContinueTraining => 'Continue training'; @override - String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; + String get puzzleDifficultyLevel => 'Difficulty level'; @override - String get engineManager => 'Engine manager'; + String get puzzleNormal => 'Normal'; @override - String get blunder => 'Blunder'; + String get puzzleEasier => 'Easier'; @override - String get mistake => 'Mistake'; + String get puzzleEasiest => 'Easiest'; @override - String get inaccuracy => 'Inaccuracy'; + String get puzzleHarder => 'Harder'; @override - String get moveTimes => 'Move times'; + String get puzzleHardest => 'Hardest'; @override - String get flipBoard => 'Flip board'; + String get puzzleExample => 'Example'; @override - String get threefoldRepetition => 'Threefold repetition'; + String get puzzleAddAnotherTheme => 'Add another theme'; @override - String get claimADraw => 'Claim a draw'; + String get puzzleNextPuzzle => 'Next puzzle'; @override - String get offerDraw => 'Offer draw'; + String get puzzleJumpToNextPuzzleImmediately => 'Jump to next puzzle immediately'; @override - String get draw => 'Draw'; + String get puzzlePuzzleDashboard => 'Puzzle Dashboard'; @override - String get drawByMutualAgreement => 'Draw by mutual agreement'; + String get puzzleImprovementAreas => 'Improvement areas'; @override - String get fiftyMovesWithoutProgress => 'Fifty moves without progress'; + String get puzzleStrengths => 'Strengths'; @override - String get currentGames => 'Current games'; + String get puzzleHistory => 'Puzzle history'; @override - String get viewInFullSize => 'View in full size'; + String get puzzleSolved => 'solved'; @override - String get logOut => 'Sign out'; + String get puzzleFailed => 'incorrect'; @override - String get signIn => 'Sign in'; + String get puzzleStreakDescription => 'Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it\'s game over! But you can skip one move per session.'; @override - String get rememberMe => 'Keep me logged in'; + String puzzleYourStreakX(String param) { + return 'Your streak: $param'; + } @override - String get youNeedAnAccountToDoThat => 'You need an account to do that'; + String get puzzleStreakSkipExplanation => 'Skip this move to preserve your streak! Only works once per run.'; @override - String get signUp => 'Register'; + String get puzzleContinueTheStreak => 'Continue the streak'; @override - String get computersAreNotAllowedToPlay => 'Computers and computer-assisted players are not allowed to play. Please do not get assistance from chess engines, databases, or from other players while playing. Also note that making multiple accounts is strongly discouraged and excessive multi-accounting will lead to being banned.'; + String get puzzleNewStreak => 'New streak'; @override - String get games => 'Games'; + String get puzzleFromMyGames => 'From my games'; @override - String get forum => 'Forum'; + String get puzzleLookupOfPlayer => 'Search puzzles from a player\'s games'; @override - String xPostedInForumY(String param1, String param2) { - return '$param1 posted in topic $param2'; + String puzzleFromXGames(String param) { + return 'Puzzles from $param\'s games'; } @override - String get latestForumPosts => 'Latest forum posts'; + String get puzzleSearchPuzzles => 'Search puzzles'; @override - String get players => 'Players'; + String get puzzleFromMyGamesNone => 'You have no puzzles in the database, but Lichess still loves you very much.\n\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!'; @override - String get friends => 'Friends'; + String puzzleFromXGamesFound(String param1, String param2) { + return '$param1 puzzles found in $param2 games'; + } @override - String get otherPlayers => 'other players'; + String get puzzlePuzzleDashboardDescription => 'Train, analyse, improve'; @override - String get discussions => 'Conversations'; + String puzzlePercentSolved(String param) { + return '$param solved'; + } @override - String get today => 'Today'; + String get puzzleNoPuzzlesToShow => 'Nothing to show, go play some puzzles first!'; @override - String get yesterday => 'Yesterday'; + String get puzzleImprovementAreasDescription => 'Train these to optimize your progress!'; @override - String get minutesPerSide => 'Minutes per side'; + String get puzzleStrengthDescription => 'You perform the best in these themes'; @override - String get variant => 'Variant'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count times', + one: 'Played $count time', + ); + return '$_temp0'; + } @override - String get variants => 'Variants'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count points below your puzzle rating', + one: 'One point below your puzzle rating', + ); + return '$_temp0'; + } @override - String get timeControl => 'Time control'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count points above your puzzle rating', + one: 'One point above your puzzle rating', + ); + return '$_temp0'; + } @override - String get realTime => 'Real time'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count played', + one: '$count played', + ); + return '$_temp0'; + } @override - String get correspondence => 'Correspondence'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count to replay', + one: '$count to replay', + ); + return '$_temp0'; + } @override - String get daysPerTurn => 'Days per turn'; + String get puzzleThemeAdvancedPawn => 'Advanced pawn'; @override - String get oneDay => 'One day'; + String get puzzleThemeAdvancedPawnDescription => 'One of your pawns is deep into the opponent position, maybe threatening to promote.'; @override - String get time => 'Time'; + String get puzzleThemeAdvantage => 'Advantage'; @override - String get rating => 'Rating'; + String get puzzleThemeAdvantageDescription => 'Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp)'; @override - String get ratingStats => 'Rating stats'; + String get puzzleThemeAnastasiaMate => 'Anastasia\'s mate'; @override - String get username => 'User name'; + String get puzzleThemeAnastasiaMateDescription => 'A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece.'; @override - String get usernameOrEmail => 'User name or email'; + String get puzzleThemeArabianMate => 'Arabian mate'; @override - String get changeUsername => 'Change username'; + String get puzzleThemeArabianMateDescription => 'A knight and a rook team up to trap the opposing king on a corner of the board.'; @override - String get changeUsernameNotSame => 'Only the case of the letters can change. For example \"johndoe\" to \"JohnDoe\".'; + String get puzzleThemeAttackingF2F7 => 'Attacking f2 or f7'; @override - String get changeUsernameDescription => 'Change your username. This can only be done once and you are only allowed to change the case of the letters in your username.'; + String get puzzleThemeAttackingF2F7Description => 'An attack focusing on the f2 or f7 pawn, such as in the fried liver opening.'; @override - String get signupUsernameHint => 'Be sure to choose a family-friendly username. You cannot change it later, and any accounts with inappropriate usernames will be closed!'; + String get puzzleThemeAttraction => 'Attraction'; @override - String get signupEmailHint => 'We will only use it for password reset.'; + String get puzzleThemeAttractionDescription => 'An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic.'; @override - String get password => 'Password'; + String get puzzleThemeBackRankMate => 'Back rank mate'; @override - String get changePassword => 'Change password'; + String get puzzleThemeBackRankMateDescription => 'Checkmate the king on the home rank, when it is trapped there by its own pieces.'; @override - String get changeEmail => 'Change email'; + String get puzzleThemeBishopEndgame => 'Bishop endgame'; @override - String get email => 'Email'; + String get puzzleThemeBishopEndgameDescription => 'An endgame with only bishops and pawns.'; @override - String get passwordReset => 'Password reset'; + String get puzzleThemeBodenMate => 'Boden\'s mate'; @override - String get forgotPassword => 'Forgot password?'; + String get puzzleThemeBodenMateDescription => 'Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces.'; @override - String get error_weakPassword => 'This password is extremely common and too easy to guess.'; + String get puzzleThemeCastling => 'Castling'; @override - String get error_namePassword => 'Please don\'t use your username as your password.'; + String get puzzleThemeCastlingDescription => 'Bring the king to safety, and deploy the rook for attack.'; @override - String get blankedPassword => 'You have used the same password on another site, and that site has been compromised. To ensure the safety of your Lichess account, we need you to set a new password. Thank you for your understanding.'; + String get puzzleThemeCapturingDefender => 'Capture the defender'; @override - String get youAreLeavingLichess => 'You are leaving Lichess'; + String get puzzleThemeCapturingDefenderDescription => 'Removing a piece that is critical to defense of another piece, allowing the now undefended piece to be captured on a following move.'; @override - String get neverTypeYourPassword => 'Never type your Lichess password on another site!'; + String get puzzleThemeCrushing => 'Crushing'; @override - String proceedToX(String param) { - return 'Proceed to $param'; - } + String get puzzleThemeCrushingDescription => 'Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp)'; @override - String get passwordSuggestion => 'Do not set a password suggested by someone else. They will use it to steal your account.'; + String get puzzleThemeDoubleBishopMate => 'Double bishop mate'; @override - String get emailSuggestion => 'Do not set an email address suggested by someone else. They will use it to steal your account.'; + String get puzzleThemeDoubleBishopMateDescription => 'Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces.'; @override - String get emailConfirmHelp => 'Help with email confirmation'; + String get puzzleThemeDovetailMate => 'Dovetail mate'; @override - String get emailConfirmNotReceived => 'Didn\'t receive your confirmation email after signing up?'; + String get puzzleThemeDovetailMateDescription => 'A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces.'; @override - String get whatSignupUsername => 'What username did you use to sign up?'; + String get puzzleThemeEquality => 'Equality'; @override - String usernameNotFound(String param) { - return 'We couldn\'t find any user by this name: $param.'; - } + String get puzzleThemeEqualityDescription => 'Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp)'; @override - String get usernameCanBeUsedForNewAccount => 'You can use this username to create a new account'; + String get puzzleThemeKingsideAttack => 'Kingside attack'; @override - String emailSent(String param) { - return 'We have sent an email to $param.'; - } + String get puzzleThemeKingsideAttackDescription => 'An attack of the opponent\'s king, after they castled on the king side.'; @override - String get emailCanTakeSomeTime => 'It can take some time to arrive.'; + String get puzzleThemeClearance => 'Clearance'; @override - String get refreshInboxAfterFiveMinutes => 'Wait 5 minutes and refresh your email inbox.'; + String get puzzleThemeClearanceDescription => 'A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea.'; @override - String get checkSpamFolder => 'Also check your spam folder, it might end up there. If so, mark it as not spam.'; + String get puzzleThemeDefensiveMove => 'Defensive move'; @override - String get emailForSignupHelp => 'If you still have questions, please send us an email:'; + String get puzzleThemeDefensiveMoveDescription => 'A precise move or sequence of moves that is needed to avoid losing material or another advantage.'; @override - String copyTextToEmail(String param) { - return 'Copy and paste the above text and send it to $param'; - } + String get puzzleThemeDeflection => 'Deflection'; @override - String get waitForSignupHelp => 'We will come back to you shortly to help you complete your signup.'; + String get puzzleThemeDeflectionDescription => 'A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called \"overloading\".'; @override - String accountConfirmed(String param) { - return 'The user $param is successfully confirmed.'; - } + String get puzzleThemeDiscoveredAttack => 'Discovered attack'; @override - String accountCanLogin(String param) { - return 'You can login right now as $param.'; - } + String get puzzleThemeDiscoveredAttackDescription => 'Moving a piece that previously blocked an attack by another long range piece, such as a knight out of the way of a rook.'; @override - String get accountConfirmationEmailNotNeeded => 'You do not need a confirmation email.'; + String get puzzleThemeDoubleCheck => 'Double check'; @override - String accountClosed(String param) { - return 'The account $param is closed.'; - } + String get puzzleThemeDoubleCheckDescription => 'Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent\'s king.'; @override - String accountRegisteredWithoutEmail(String param) { - return 'The account $param was registered without an email.'; - } + String get puzzleThemeEndgame => 'Endgame'; @override - String get rank => 'Rank'; + String get puzzleThemeEndgameDescription => 'A tactic during the last phase of the game.'; @override - String rankX(String param) { - return 'Rank: $param'; - } + String get puzzleThemeEnPassantDescription => 'A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move.'; @override - String get gamesPlayed => 'Games played'; + String get puzzleThemeExposedKing => 'Exposed king'; @override - String get cancel => 'Cancel'; + String get puzzleThemeExposedKingDescription => 'A tactic involving a king with few defenders around it, often leading to checkmate.'; @override - String get whiteTimeOut => 'White time out'; + String get puzzleThemeFork => 'Fork'; @override - String get blackTimeOut => 'Black time out'; + String get puzzleThemeForkDescription => 'A move where the moved piece attacks two opponent pieces at once.'; @override - String get drawOfferSent => 'Draw offer sent'; + String get puzzleThemeHangingPiece => 'Hanging piece'; @override - String get drawOfferAccepted => 'Draw offer accepted'; + String get puzzleThemeHangingPieceDescription => 'A tactic involving an opponent piece being undefended or insufficiently defended and free to capture.'; @override - String get drawOfferCanceled => 'Draw offer canceled'; + String get puzzleThemeHookMate => 'Hook mate'; @override - String get whiteOffersDraw => 'White offers draw'; + String get puzzleThemeHookMateDescription => 'Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king\'s escape.'; @override - String get blackOffersDraw => 'Black offers draw'; + String get puzzleThemeInterference => 'Interference'; @override - String get whiteDeclinesDraw => 'White declines draw'; + String get puzzleThemeInterferenceDescription => 'Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks.'; @override - String get blackDeclinesDraw => 'Black declines draw'; + String get puzzleThemeIntermezzo => 'Intermezzo'; @override - String get yourOpponentOffersADraw => 'Your opponent offers a draw'; + String get puzzleThemeIntermezzoDescription => 'Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as \"Zwischenzug\" or \"In between\".'; @override - String get accept => 'Accept'; + String get puzzleThemeKnightEndgame => 'Knight endgame'; @override - String get decline => 'Decline'; + String get puzzleThemeKnightEndgameDescription => 'An endgame with only knights and pawns.'; @override - String get playingRightNow => 'Playing right now'; + String get puzzleThemeLong => 'Long puzzle'; @override - String get eventInProgress => 'Playing right now'; + String get puzzleThemeLongDescription => 'Three moves to win.'; @override - String get finished => 'Finished'; + String get puzzleThemeMaster => 'Master games'; @override - String get abortGame => 'Abort game'; + String get puzzleThemeMasterDescription => 'Puzzles from games played by titled players.'; @override - String get gameAborted => 'Game aborted'; + String get puzzleThemeMasterVsMaster => 'Master vs Master games'; @override - String get standard => 'Standard'; + String get puzzleThemeMasterVsMasterDescription => 'Puzzles from games between two titled players.'; @override - String get customPosition => 'Custom position'; + String get puzzleThemeMate => 'Checkmate'; @override - String get unlimited => 'Unlimited'; + String get puzzleThemeMateDescription => 'Win the game with style.'; @override - String get mode => 'Mode'; + String get puzzleThemeMateIn1 => 'Mate in 1'; @override - String get casual => 'Casual'; + String get puzzleThemeMateIn1Description => 'Deliver checkmate in one move.'; @override - String get rated => 'Rated'; + String get puzzleThemeMateIn2 => 'Mate in 2'; @override - String get casualTournament => 'Casual'; + String get puzzleThemeMateIn2Description => 'Deliver checkmate in two moves.'; @override - String get ratedTournament => 'Rated'; + String get puzzleThemeMateIn3 => 'Mate in 3'; @override - String get thisGameIsRated => 'This game is rated'; + String get puzzleThemeMateIn3Description => 'Deliver checkmate in three moves.'; @override - String get rematch => 'Rematch'; + String get puzzleThemeMateIn4 => 'Mate in 4'; @override - String get rematchOfferSent => 'Rematch offer sent'; + String get puzzleThemeMateIn4Description => 'Deliver checkmate in four moves.'; @override - String get rematchOfferAccepted => 'Rematch offer accepted'; + String get puzzleThemeMateIn5 => 'Mate in 5 or more'; @override - String get rematchOfferCanceled => 'Rematch offer canceled'; + String get puzzleThemeMateIn5Description => 'Figure out a long mating sequence.'; @override - String get rematchOfferDeclined => 'Rematch offer declined'; + String get puzzleThemeMiddlegame => 'Middlegame'; @override - String get cancelRematchOffer => 'Cancel rematch offer'; + String get puzzleThemeMiddlegameDescription => 'A tactic during the second phase of the game.'; @override - String get viewRematch => 'View rematch'; + String get puzzleThemeOneMove => 'One-move puzzle'; @override - String get confirmMove => 'Confirm move'; + String get puzzleThemeOneMoveDescription => 'A puzzle that is only one move long.'; @override - String get play => 'Play'; + String get puzzleThemeOpening => 'Opening'; @override - String get inbox => 'Inbox'; + String get puzzleThemeOpeningDescription => 'A tactic during the first phase of the game.'; @override - String get chatRoom => 'Chat room'; + String get puzzleThemePawnEndgame => 'Pawn endgame'; @override - String get loginToChat => 'Sign in to chat'; + String get puzzleThemePawnEndgameDescription => 'An endgame with only pawns.'; @override - String get youHaveBeenTimedOut => 'You have been timed out.'; + String get puzzleThemePin => 'Pin'; @override - String get spectatorRoom => 'Spectator room'; + String get puzzleThemePinDescription => 'A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece.'; @override - String get composeMessage => 'Compose message'; + String get puzzleThemePromotion => 'Promotion'; @override - String get subject => 'Subject'; + String get puzzleThemePromotionDescription => 'Promote one of your pawns to a queen or minor piece.'; @override - String get send => 'Send'; + String get puzzleThemeQueenEndgame => 'Queen endgame'; @override - String get incrementInSeconds => 'Increment in seconds'; + String get puzzleThemeQueenEndgameDescription => 'An endgame with only queens and pawns.'; @override - String get freeOnlineChess => 'Free Online Chess'; + String get puzzleThemeQueenRookEndgame => 'Queen and Rook'; @override - String get exportGames => 'Export games'; + String get puzzleThemeQueenRookEndgameDescription => 'An endgame with only queens, rooks, and pawns.'; @override - String get ratingRange => 'Rating range'; + String get puzzleThemeQueensideAttack => 'Queenside attack'; @override - String get thisAccountViolatedTos => 'This account violated the Lichess Terms of Service'; + String get puzzleThemeQueensideAttackDescription => 'An attack of the opponent\'s king, after they castled on the queen side.'; @override - String get openingExplorerAndTablebase => 'Opening explorer & tablebase'; + String get puzzleThemeQuietMove => 'Quiet move'; @override - String get takeback => 'Takeback'; + String get puzzleThemeQuietMoveDescription => 'A move that does not make a check or capture, but does prepare an unavoidable threat for a later move.'; @override - String get proposeATakeback => 'Propose a takeback'; + String get puzzleThemeRookEndgame => 'Rook endgame'; @override - String get takebackPropositionSent => 'Takeback sent'; + String get puzzleThemeRookEndgameDescription => 'An endgame with only rooks and pawns.'; @override - String get takebackPropositionDeclined => 'Takeback declined'; + String get puzzleThemeSacrifice => 'Sacrifice'; @override - String get takebackPropositionAccepted => 'Takeback accepted'; + String get puzzleThemeSacrificeDescription => 'A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves.'; @override - String get takebackPropositionCanceled => 'Takeback canceled'; + String get puzzleThemeShort => 'Short puzzle'; @override - String get yourOpponentProposesATakeback => 'Your opponent proposes a takeback'; + String get puzzleThemeShortDescription => 'Two moves to win.'; @override - String get bookmarkThisGame => 'Bookmark this game'; + String get puzzleThemeSkewer => 'Skewer'; @override - String get tournament => 'Tournament'; + String get puzzleThemeSkewerDescription => 'A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin.'; @override - String get tournaments => 'Tournaments'; + String get puzzleThemeSmotheredMate => 'Smothered mate'; @override - String get tournamentPoints => 'Tournament points'; + String get puzzleThemeSmotheredMateDescription => 'A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces.'; @override - String get viewTournament => 'View tournament'; + String get puzzleThemeSuperGM => 'Super GM games'; @override - String get backToTournament => 'Back to tournament'; + String get puzzleThemeSuperGMDescription => 'Puzzles from games played by the best players in the world.'; @override - String get noDrawBeforeSwissLimit => 'You cannot draw before 30 moves are played in a Swiss tournament.'; + String get puzzleThemeTrappedPiece => 'Trapped piece'; @override - String get thematic => 'Thematic'; + String get puzzleThemeTrappedPieceDescription => 'A piece is unable to escape capture as it has limited moves.'; @override - String yourPerfRatingIsProvisional(String param) { - return 'Your $param rating is provisional'; - } + String get puzzleThemeUnderPromotion => 'Underpromotion'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'Your $param1 rating ($param2) is too high'; - } + String get puzzleThemeUnderPromotionDescription => 'Promotion to a knight, bishop, or rook.'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'Your top weekly $param1 rating ($param2) is too high'; - } + String get puzzleThemeVeryLong => 'Very long puzzle'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return 'Your $param1 rating ($param2) is too low'; - } + String get puzzleThemeVeryLongDescription => 'Four moves or more to win.'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return 'Rated ≥ $param1 in $param2'; - } + String get puzzleThemeXRayAttack => 'X-Ray attack'; @override - String ratedLessThanInPerf(String param1, String param2) { - return 'Rated ≤ $param1 in $param2 for the last week'; - } + String get puzzleThemeXRayAttackDescription => 'A piece attacks or defends a square, through an enemy piece.'; @override - String mustBeInTeam(String param) { - return 'Must be in team $param'; - } + String get puzzleThemeZugzwang => 'Zugzwang'; @override - String youAreNotInTeam(String param) { - return 'You are not in the team $param'; - } + String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get backToGame => 'Back to game'; + String get puzzleThemeMix => 'Healthy mix'; @override - String get siteDescription => 'Free online chess server. Play chess in a clean interface. No registration, no ads, no plugin required. Play chess with the computer, friends or random opponents.'; + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1 joined team $param2'; - } + String get puzzleThemePlayerGames => 'Player games'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1 created team $param2'; + String get puzzleThemePlayerGamesDescription => 'Lookup puzzles generated from your games, or from another player\'s games.'; + + @override + String puzzleThemePuzzleDownloadInformation(String param) { + return 'These puzzles are in the public domain, and can be downloaded from $param.'; } @override - String get startedStreaming => 'started streaming'; + String get searchSearch => 'Search'; @override - String xStartedStreaming(String param) { - return '$param started streaming'; - } + String get settingsSettings => 'Settings'; @override - String get averageElo => 'Average rating'; + String get settingsCloseAccount => 'Close account'; @override - String get location => 'Location'; + String get settingsManagedAccountCannotBeClosed => 'Your account is managed and cannot be closed.'; @override - String get filterGames => 'Filter games'; + String get settingsClosingIsDefinitive => 'Closing is definitive. There is no going back. Are you sure?'; @override - String get reset => 'Reset'; + String get settingsCantOpenSimilarAccount => 'You will not be allowed to open a new account with the same name, even if the case is different.'; @override - String get apply => 'Submit'; + String get settingsChangedMindDoNotCloseAccount => 'I changed my mind, don\'t close my account'; @override - String get save => 'Save'; + String get settingsCloseAccountExplanation => 'Are you sure you want to close your account? Closing your account is a permanent decision. You will NEVER be able to log in EVER AGAIN.'; @override - String get leaderboard => 'Leaderboard'; + String get settingsThisAccountIsClosed => 'This account is closed.'; @override - String get screenshotCurrentPosition => 'Screenshot current position'; + String get playWithAFriend => 'Play with a friend'; @override - String get gameAsGIF => 'Game as GIF'; + String get playWithTheMachine => 'Play with the computer'; @override - String get pasteTheFenStringHere => 'Paste the FEN text here'; + String get toInviteSomeoneToPlayGiveThisUrl => 'To invite someone to play, give this URL'; @override - String get pasteThePgnStringHere => 'Paste the PGN text here'; + String get gameOver => 'Game Over'; @override - String get orUploadPgnFile => 'Or upload a PGN file'; + String get waitingForOpponent => 'Waiting for opponent'; @override - String get fromPosition => 'From position'; + String get orLetYourOpponentScanQrCode => 'Or let your opponent scan this QR code'; @override - String get continueFromHere => 'Continue from here'; + String get waiting => 'Waiting'; @override - String get toStudy => 'Study'; + String get yourTurn => 'Your turn'; @override - String get importGame => 'Import game'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1 level $param2'; + } @override - String get importGameExplanation => 'Paste a game PGN to get a browsable replay,\ncomputer analysis, game chat and shareable URL.'; + String get level => 'Level'; @override - String get importGameCaveat => 'Variations will be erased. To keep them, import the PGN via a study.'; + String get strength => 'Strength'; @override - String get importGameDataPrivacyWarning => 'This PGN can be accessed by the public. To import a game privately, use a study.'; + String get toggleTheChat => 'Toggle the chat'; @override - String get thisIsAChessCaptcha => 'This is a chess CAPTCHA.'; + String get chat => 'Chat'; @override - String get clickOnTheBoardToMakeYourMove => 'Click on the board to make your move, and prove you are human.'; + String get resign => 'Resign'; @override - String get captcha_fail => 'Please solve the chess captcha.'; + String get checkmate => 'Checkmate'; @override - String get notACheckmate => 'Not a checkmate'; + String get stalemate => 'Stalemate'; @override - String get whiteCheckmatesInOneMove => 'White to checkmate in one move'; + String get white => 'White'; @override - String get blackCheckmatesInOneMove => 'Black to checkmate in one move'; + String get black => 'Black'; @override - String get retry => 'Retry'; + String get asWhite => 'as white'; @override - String get reconnecting => 'Reconnecting'; + String get asBlack => 'as black'; @override - String get noNetwork => 'Offline'; + String get randomColor => 'Random side'; @override - String get favoriteOpponents => 'Favorite opponents'; + String get createAGame => 'Create a game'; @override - String get follow => 'Follow'; + String get whiteIsVictorious => 'White is victorious'; @override - String get following => 'Following'; + String get blackIsVictorious => 'Black is victorious'; @override - String get unfollow => 'Unfollow'; + String get youPlayTheWhitePieces => 'You play the white pieces'; @override - String followX(String param) { - return 'Follow $param'; - } + String get youPlayTheBlackPieces => 'You play the black pieces'; @override - String unfollowX(String param) { - return 'Unfollow $param'; - } + String get itsYourTurn => 'It\'s your turn!'; @override - String get block => 'Block'; + String get cheatDetected => 'Cheat Detected'; @override - String get blocked => 'Blocked'; + String get kingInTheCenter => 'King in the center'; @override - String get unblock => 'Unblock'; + String get threeChecks => 'Three checks'; @override - String get followsYou => 'Follows you'; + String get raceFinished => 'Race finished'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1 started following $param2'; - } + String get variantEnding => 'Variant ending'; @override - String get more => 'More'; + String get newOpponent => 'New opponent'; @override - String get memberSince => 'Member since'; + String get yourOpponentWantsToPlayANewGameWithYou => 'Your opponent wants to play a new game with you'; @override - String lastSeenActive(String param) { - return 'Active $param'; - } + String get joinTheGame => 'Join the game'; @override - String get player => 'Player'; + String get whitePlays => 'White to play'; @override - String get list => 'List'; + String get blackPlays => 'Black to play'; @override - String get graph => 'Graph'; + String get opponentLeftChoices => 'Your opponent left the game. You can claim victory, call the game a draw, or wait.'; @override - String get required => 'Required.'; + String get forceResignation => 'Claim victory'; @override - String get openTournaments => 'Open tournaments'; + String get forceDraw => 'Call draw'; @override - String get duration => 'Duration'; + String get talkInChat => 'Please be nice in the chat!'; @override - String get winner => 'Winner'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'The first person to come to this URL will play with you.'; @override - String get standing => 'Standing'; + String get whiteResigned => 'White resigned'; @override - String get createANewTournament => 'Create a new tournament'; + String get blackResigned => 'Black resigned'; @override - String get tournamentCalendar => 'Tournament calendar'; + String get whiteLeftTheGame => 'White left the game'; @override - String get conditionOfEntry => 'Entry requirements:'; + String get blackLeftTheGame => 'Black left the game'; @override - String get advancedSettings => 'Advanced settings'; + String get whiteDidntMove => 'White didn\'t move'; @override - String get safeTournamentName => 'Pick a very safe name for the tournament.'; + String get blackDidntMove => 'Black didn\'t move'; @override - String get inappropriateNameWarning => 'Anything even slightly inappropriate could get your account closed.'; + String get requestAComputerAnalysis => 'Request a computer analysis'; @override - String get emptyTournamentName => 'Leave empty to name the tournament after a notable chess player.'; + String get computerAnalysis => 'Computer analysis'; @override - String get makePrivateTournament => 'Make the tournament private, and restrict access with a password'; + String get computerAnalysisAvailable => 'Computer analysis available'; @override - String get join => 'Join'; + String get computerAnalysisDisabled => 'Computer analysis disabled'; @override - String get withdraw => 'Withdraw'; + String get analysis => 'Analysis board'; @override - String get points => 'Points'; + String depthX(String param) { + return 'Depth $param'; + } @override - String get wins => 'Wins'; + String get usingServerAnalysis => 'Using server analysis'; @override - String get losses => 'Losses'; + String get loadingEngine => 'Loading engine...'; @override - String get createdBy => 'Created by'; + String get calculatingMoves => 'Calculating moves...'; @override - String get tournamentIsStarting => 'The tournament is starting'; + String get engineFailed => 'Error loading engine'; @override - String get tournamentPairingsAreNowClosed => 'The tournament pairings are now closed.'; + String get cloudAnalysis => 'Cloud analysis'; @override - String standByX(String param) { - return 'Stand by $param, pairing players, get ready!'; - } + String get goDeeper => 'Go deeper'; @override - String get pause => 'Pause'; + String get showThreat => 'Show threat'; @override - String get resume => 'Resume'; + String get inLocalBrowser => 'in local browser'; @override - String get youArePlaying => 'You are playing!'; + String get toggleLocalEvaluation => 'Toggle local evaluation'; @override - String get winRate => 'Win rate'; + String get promoteVariation => 'Promote variation'; @override - String get berserkRate => 'Berserk rate'; + String get makeMainLine => 'Make main line'; @override - String get performance => 'Performance'; + String get deleteFromHere => 'Delete from here'; @override - String get tournamentComplete => 'Tournament complete'; + String get collapseVariations => 'Collapse variations'; @override - String get movesPlayed => 'Moves played'; + String get expandVariations => 'Expand variations'; @override - String get whiteWins => 'White wins'; + String get forceVariation => 'Force variation'; @override - String get blackWins => 'Black wins'; + String get copyVariationPgn => 'Copy variation PGN'; @override - String get drawRate => 'Draw rate'; + String get move => 'Move'; @override - String get draws => 'Draws'; + String get variantLoss => 'Variant loss'; @override - String nextXTournament(String param) { - return 'Next $param tournament:'; - } + String get variantWin => 'Variant win'; @override - String get averageOpponent => 'Average opponent'; + String get insufficientMaterial => 'Insufficient material'; @override - String get boardEditor => 'Board editor'; + String get pawnMove => 'Pawn move'; @override - String get setTheBoard => 'Set the board'; + String get capture => 'Capture'; @override - String get popularOpenings => 'Popular openings'; + String get close => 'Close'; @override - String get endgamePositions => 'Endgame positions'; + String get winning => 'Winning'; @override - String chess960StartPosition(String param) { - return 'Chess960 start position: $param'; - } + String get losing => 'Losing'; @override - String get startPosition => 'Starting position'; + String get drawn => 'Drawn'; @override - String get clearBoard => 'Clear board'; + String get unknown => 'Unknown'; @override - String get loadPosition => 'Load position'; + String get database => 'Database'; @override - String get isPrivate => 'Private'; + String get whiteDrawBlack => 'White / Draw / Black'; @override - String reportXToModerators(String param) { - return 'Report $param to moderators'; + String averageRatingX(String param) { + return 'Average rating: $param'; } @override - String profileCompletion(String param) { - return 'Profile completion: $param'; - } + String get recentGames => 'Recent games'; @override - String xRating(String param) { - return '$param rating'; - } + String get topGames => 'Top games'; @override - String get ifNoneLeaveEmpty => 'If none, leave empty'; + String masterDbExplanation(String param1, String param2, String param3) { + return 'OTB games of $param1+ FIDE rated players from $param2 to $param3'; + } @override - String get profile => 'Profile'; + String get dtzWithRounding => 'DTZ50\'\' with rounding, based on number of half-moves until next capture or pawn move'; @override - String get editProfile => 'Edit profile'; + String get noGameFound => 'No game found'; @override - String get realName => 'Real name'; + String get maxDepthReached => 'Max depth reached!'; @override - String get setFlair => 'Set your flair'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Maybe include more games from the preferences menu?'; @override - String get flair => 'Flair'; + String get openings => 'Openings'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get openingExplorer => 'Opening explorer'; @override - String get biography => 'Biography'; + String get openingEndgameExplorer => 'Opening/endgame explorer'; @override - String get countryRegion => 'Region or country'; + String xOpeningExplorer(String param) { + return '$param opening explorer'; + } @override - String get thankYou => 'Thank you!'; + String get playFirstOpeningEndgameExplorerMove => 'Play first opening/endgame-explorer move'; @override - String get socialMediaLinks => 'Social media links'; + String get winPreventedBy50MoveRule => 'Win prevented by 50-move rule'; @override - String get oneUrlPerLine => 'One URL per line.'; + String get lossSavedBy50MoveRule => 'Loss prevented by 50-move rule'; @override - String get inlineNotation => 'Inline notation'; + String get winOr50MovesByPriorMistake => 'Win or 50 moves by prior mistake'; @override - String get makeAStudy => 'For safekeeping and sharing, consider making a study.'; + String get lossOr50MovesByPriorMistake => 'Loss or 50 moves by prior mistake'; @override - String get clearSavedMoves => 'Clear moves'; + String get unknownDueToRounding => 'Due to possible rounding of DTZ values in Syzygy tablebases, a win/loss is only guaranteed if the recommended tablebase line has been followed since the last capture or pawn move.'; @override - String get previouslyOnLichessTV => 'Previously on Lichess TV'; + String get allSet => 'All set!'; @override - String get onlinePlayers => 'Online players'; + String get importPgn => 'Import PGN'; @override - String get activePlayers => 'Active players'; + String get delete => 'Delete'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Beware, the game is rated but has no clock!'; + String get deleteThisImportedGame => 'Delete this imported game?'; @override - String get success => 'Success'; + String get replayMode => 'Replay mode'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Automatically proceed to next game after moving'; + String get realtimeReplay => 'Realtime'; @override - String get autoSwitch => 'Auto switch'; + String get byCPL => 'By CPL'; @override - String get puzzles => 'Puzzles'; + String get enable => 'Enable'; @override - String get onlineBots => 'Online bots'; + String get bestMoveArrow => 'Best move arrow'; @override - String get name => 'Name'; + String get showVariationArrows => 'Show variation arrows'; @override - String get description => 'Description'; + String get evaluationGauge => 'Evaluation gauge'; @override - String get descPrivate => 'Private description'; + String get multipleLines => 'Multiple lines'; @override - String get descPrivateHelp => 'Text that only the team members will see. If set, replaces the public description for team members.'; + String get cpus => 'CPUs'; @override - String get no => 'No'; + String get memory => 'Memory'; @override - String get yes => 'Yes'; + String get infiniteAnalysis => 'Infinite analysis'; @override - String get website => 'Website'; + String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; @override - String get mobile => 'Mobile'; + String get blunder => 'Blunder'; @override - String get help => 'Help:'; + String get mistake => 'Mistake'; @override - String get createANewTopic => 'Create a new topic'; + String get inaccuracy => 'Inaccuracy'; @override - String get topics => 'Topics'; + String get moveTimes => 'Move times'; @override - String get posts => 'Posts'; + String get flipBoard => 'Flip board'; @override - String get lastPost => 'Last post'; + String get threefoldRepetition => 'Threefold repetition'; @override - String get views => 'Views'; + String get claimADraw => 'Claim a draw'; @override - String get replies => 'Replies'; + String get offerDraw => 'Offer draw'; @override - String get replyToThisTopic => 'Reply to this topic'; + String get draw => 'Draw'; @override - String get reply => 'Reply'; + String get drawByMutualAgreement => 'Draw by mutual agreement'; @override - String get message => 'Message'; + String get fiftyMovesWithoutProgress => 'Fifty moves without progress'; @override - String get createTheTopic => 'Create the topic'; + String get currentGames => 'Current games'; @override - String get reportAUser => 'Report a user'; + String get viewInFullSize => 'View in full size'; @override - String get user => 'User'; + String get logOut => 'Sign out'; @override - String get reason => 'Reason'; + String get signIn => 'Sign in'; @override - String get whatIsIheMatter => 'What\'s the matter?'; + String get rememberMe => 'Keep me logged in'; @override - String get cheat => 'Cheat'; + String get youNeedAnAccountToDoThat => 'You need an account to do that'; @override - String get troll => 'Troll'; + String get signUp => 'Register'; @override - String get other => 'Other'; + String get computersAreNotAllowedToPlay => 'Computers and computer-assisted players are not allowed to play. Please do not get assistance from chess engines, databases, or from other players while playing. Also note that making multiple accounts is strongly discouraged and excessive multi-accounting will lead to being banned.'; @override - String get reportDescriptionHelp => 'Paste the link to the game(s) and explain what is wrong about this user behavior. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'; + String get games => 'Games'; @override - String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; + String get forum => 'Forum'; @override - String by(String param) { - return 'by $param'; + String xPostedInForumY(String param1, String param2) { + return '$param1 posted in topic $param2'; } @override - String importedByX(String param) { - return 'Imported by $param'; - } + String get latestForumPosts => 'Latest forum posts'; @override - String get thisTopicIsNowClosed => 'This topic is now closed.'; + String get players => 'Players'; @override - String get blog => 'Blog'; + String get friends => 'Friends'; @override - String get notes => 'Notes'; + String get otherPlayers => 'other players'; @override - String get typePrivateNotesHere => 'Type private notes here'; + String get discussions => 'Conversations'; @override - String get writeAPrivateNoteAboutThisUser => 'Write a private note about this user'; + String get today => 'Today'; @override - String get noNoteYet => 'No note yet'; + String get yesterday => 'Yesterday'; @override - String get invalidUsernameOrPassword => 'Invalid username or password'; + String get minutesPerSide => 'Minutes per side'; @override - String get incorrectPassword => 'Incorrect password'; + String get variant => 'Variant'; @override - String get invalidAuthenticationCode => 'Invalid authentication code'; + String get variants => 'Variants'; @override - String get emailMeALink => 'Email me a link'; + String get timeControl => 'Time control'; @override - String get currentPassword => 'Current password'; + String get realTime => 'Real time'; @override - String get newPassword => 'New password'; + String get correspondence => 'Correspondence'; @override - String get newPasswordAgain => 'New password (again)'; + String get daysPerTurn => 'Days per turn'; @override - String get newPasswordsDontMatch => 'The new passwords don\'t match'; + String get oneDay => 'One day'; @override - String get newPasswordStrength => 'Password strength'; + String get time => 'Time'; @override - String get clockInitialTime => 'Clock initial time'; + String get rating => 'Rating'; @override - String get clockIncrement => 'Clock increment'; + String get ratingStats => 'Rating stats'; @override - String get privacy => 'Privacy'; + String get username => 'User name'; @override - String get privacyPolicy => 'Privacy policy'; + String get usernameOrEmail => 'User name or email'; @override - String get letOtherPlayersFollowYou => 'Let other players follow you'; + String get changeUsername => 'Change username'; @override - String get letOtherPlayersChallengeYou => 'Let other players challenge you'; + String get changeUsernameNotSame => 'Only the case of the letters can change. For example \"johndoe\" to \"JohnDoe\".'; @override - String get letOtherPlayersInviteYouToStudy => 'Let other players invite you to study'; + String get changeUsernameDescription => 'Change your username. This can only be done once and you are only allowed to change the case of the letters in your username.'; @override - String get sound => 'Sound'; + String get signupUsernameHint => 'Be sure to choose a family-friendly username. You cannot change it later, and any accounts with inappropriate usernames will be closed!'; @override - String get none => 'None'; + String get signupEmailHint => 'We will only use it for password reset.'; @override - String get fast => 'Fast'; + String get password => 'Password'; @override - String get normal => 'Normal'; + String get changePassword => 'Change password'; @override - String get slow => 'Slow'; + String get changeEmail => 'Change email'; @override - String get insideTheBoard => 'Inside the board'; + String get email => 'Email'; @override - String get outsideTheBoard => 'Outside the board'; + String get passwordReset => 'Password reset'; @override - String get allSquaresOfTheBoard => 'All squares on the board'; + String get forgotPassword => 'Forgot password?'; @override - String get onSlowGames => 'On slow games'; + String get error_weakPassword => 'This password is extremely common and too easy to guess.'; @override - String get always => 'Always'; + String get error_namePassword => 'Please don\'t use your username as your password.'; @override - String get never => 'Never'; + String get blankedPassword => 'You have used the same password on another site, and that site has been compromised. To ensure the safety of your Lichess account, we need you to set a new password. Thank you for your understanding.'; @override - String xCompetesInY(String param1, String param2) { - return '$param1 competes in $param2'; - } + String get youAreLeavingLichess => 'You are leaving Lichess'; @override - String get victory => 'Victory'; + String get neverTypeYourPassword => 'Never type your Lichess password on another site!'; @override - String get defeat => 'Defeat'; + String proceedToX(String param) { + return 'Proceed to $param'; + } @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get passwordSuggestion => 'Do not set a password suggested by someone else. They will use it to steal your account.'; @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get emailSuggestion => 'Do not set an email address suggested by someone else. They will use it to steal your account.'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get emailConfirmHelp => 'Help with email confirmation'; @override - String get timeline => 'Timeline'; + String get emailConfirmNotReceived => 'Didn\'t receive your confirmation email after signing up?'; @override - String get starting => 'Starting:'; + String get whatSignupUsername => 'What username did you use to sign up?'; @override - String get allInformationIsPublicAndOptional => 'All information is public and optional.'; + String usernameNotFound(String param) { + return 'We couldn\'t find any user by this name: $param.'; + } @override - String get biographyDescription => 'Talk about yourself, your interests, what you like in chess, your favorite openings, players, ...'; + String get usernameCanBeUsedForNewAccount => 'You can use this username to create a new account'; @override - String get listBlockedPlayers => 'List players you have blocked'; + String emailSent(String param) { + return 'We have sent an email to $param.'; + } @override - String get human => 'Human'; + String get emailCanTakeSomeTime => 'It can take some time to arrive.'; @override - String get computer => 'Computer'; + String get refreshInboxAfterFiveMinutes => 'Wait 5 minutes and refresh your email inbox.'; @override - String get side => 'Side'; + String get checkSpamFolder => 'Also check your spam folder, it might end up there. If so, mark it as not spam.'; @override - String get clock => 'Clock'; + String get emailForSignupHelp => 'If you still have questions, please send us an email:'; @override - String get opponent => 'Opponent'; + String copyTextToEmail(String param) { + return 'Copy and paste the above text and send it to $param'; + } @override - String get learnMenu => 'Learn'; + String get waitForSignupHelp => 'We will come back to you shortly to help you complete your signup.'; @override - String get studyMenu => 'Study'; + String accountConfirmed(String param) { + return 'The user $param is successfully confirmed.'; + } @override - String get practice => 'Practice'; + String accountCanLogin(String param) { + return 'You can login right now as $param.'; + } @override - String get community => 'Community'; + String get accountConfirmationEmailNotNeeded => 'You do not need a confirmation email.'; @override - String get tools => 'Tools'; + String accountClosed(String param) { + return 'The account $param is closed.'; + } @override - String get increment => 'Increment'; + String accountRegisteredWithoutEmail(String param) { + return 'The account $param was registered without an email.'; + } @override - String get error_unknown => 'Invalid value'; + String get rank => 'Rank'; @override - String get error_required => 'This field is required'; + String rankX(String param) { + return 'Rank: $param'; + } @override - String get error_email => 'This email address is invalid'; + String get gamesPlayed => 'Games played'; @override - String get error_email_acceptable => 'This email address is not acceptable. Please double-check it, and try again.'; + String get ok => 'OK'; @override - String get error_email_unique => 'Email address invalid or already taken'; + String get cancel => 'Cancel'; @override - String get error_email_different => 'This is already your email address'; + String get whiteTimeOut => 'White time out'; @override - String error_minLength(String param) { - return 'Must be at least $param characters long'; - } + String get blackTimeOut => 'Black time out'; @override - String error_maxLength(String param) { - return 'Maximum length is $param'; - } + String get drawOfferSent => 'Draw offer sent'; @override - String error_min(String param) { - return 'Must be at least $param'; - } + String get drawOfferAccepted => 'Draw offer accepted'; @override - String error_max(String param) { - return 'Must be at most $param'; - } + String get drawOfferCanceled => 'Draw offer canceled'; @override - String ifRatingIsPlusMinusX(String param) { - return 'If rating is ± $param'; - } + String get whiteOffersDraw => 'White offers draw'; @override - String get ifRegistered => 'If registered'; + String get blackOffersDraw => 'Black offers draw'; @override - String get onlyExistingConversations => 'Only existing conversations'; + String get whiteDeclinesDraw => 'White declines draw'; @override - String get onlyFriends => 'Only friends'; + String get blackDeclinesDraw => 'Black declines draw'; @override - String get menu => 'Menu'; + String get yourOpponentOffersADraw => 'Your opponent offers a draw'; @override - String get castling => 'Castling'; + String get accept => 'Accept'; @override - String get whiteCastlingKingside => 'White O-O'; + String get decline => 'Decline'; @override - String get blackCastlingKingside => 'Black O-O'; + String get playingRightNow => 'Playing right now'; @override - String tpTimeSpentPlaying(String param) { - return 'Time spent playing: $param'; - } + String get eventInProgress => 'Playing now'; @override - String get watchGames => 'Watch games'; + String get finished => 'Finished'; @override - String tpTimeSpentOnTV(String param) { - return 'Time on TV: $param'; - } + String get abortGame => 'Abort game'; @override - String get watch => 'Watch'; + String get gameAborted => 'Game aborted'; @override - String get videoLibrary => 'Video library'; + String get standard => 'Standard'; @override - String get streamersMenu => 'Streamers'; + String get customPosition => 'Custom position'; @override - String get mobileApp => 'Mobile App'; + String get unlimited => 'Unlimited'; @override - String get webmasters => 'Webmasters'; + String get mode => 'Mode'; @override - String get about => 'About'; + String get casual => 'Casual'; @override - String aboutX(String param) { - return 'About $param'; - } + String get rated => 'Rated'; @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1 is a free ($param2), libre, no-ads, open source chess server.'; - } + String get casualTournament => 'Casual'; @override - String get really => 'really'; + String get ratedTournament => 'Rated'; @override - String get contribute => 'Contribute'; + String get thisGameIsRated => 'This game is rated'; @override - String get termsOfService => 'Terms of Service'; + String get rematch => 'Rematch'; @override - String get sourceCode => 'Source Code'; + String get rematchOfferSent => 'Rematch offer sent'; @override - String get simultaneousExhibitions => 'Simultaneous exhibitions'; + String get rematchOfferAccepted => 'Rematch offer accepted'; @override - String get host => 'Host'; + String get rematchOfferCanceled => 'Rematch offer canceled'; @override - String hostColorX(String param) { - return 'Host color: $param'; - } + String get rematchOfferDeclined => 'Rematch offer declined'; @override - String get yourPendingSimuls => 'Your pending simuls'; + String get cancelRematchOffer => 'Cancel rematch offer'; @override - String get createdSimuls => 'Newly created simuls'; + String get viewRematch => 'View rematch'; @override - String get hostANewSimul => 'Host a new simul'; + String get confirmMove => 'Confirm move'; @override - String get signUpToHostOrJoinASimul => 'Sign up to join or host a simul'; + String get play => 'Play'; @override - String get noSimulFound => 'Simul not found'; + String get inbox => 'Inbox'; @override - String get noSimulExplanation => 'This simultaneous exhibition does not exist.'; + String get chatRoom => 'Chat room'; @override - String get returnToSimulHomepage => 'Return to simul homepage'; + String get loginToChat => 'Sign in to chat'; @override - String get aboutSimul => 'Simuls involve a single player facing several players at once.'; + String get youHaveBeenTimedOut => 'You have been timed out.'; @override - String get aboutSimulImage => 'Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.'; + String get spectatorRoom => 'Spectator room'; @override - String get aboutSimulRealLife => 'The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.'; + String get composeMessage => 'Compose message'; @override - String get aboutSimulRules => 'When the simul starts, every player starts a game with the host, who gets to play the white pieces. The simul ends when all games are complete.'; + String get subject => 'Subject'; @override - String get aboutSimulSettings => 'Simuls are always casual. Rematches, takebacks and adding time are disabled.'; + String get send => 'Send'; @override - String get create => 'Create'; + String get incrementInSeconds => 'Increment in seconds'; @override - String get whenCreateSimul => 'When you create a Simul, you get to play several players at once.'; + String get freeOnlineChess => 'Free Online Chess'; @override - String get simulVariantsHint => 'If you select several variants, each player gets to choose which one to play.'; + String get exportGames => 'Export games'; @override - String get simulClockHint => 'Fischer Clock setup. The more players you take on, the more time you may need.'; + String get ratingRange => 'Rating range'; @override - String get simulAddExtraTime => 'You may add extra time to your clock to help cope with the simul.'; + String get thisAccountViolatedTos => 'This account violated the Lichess Terms of Service'; @override - String get simulHostExtraTime => 'Host extra clock time'; + String get openingExplorerAndTablebase => 'Opening explorer & tablebase'; @override - String get simulAddExtraTimePerPlayer => 'Add initial time to your clock for each player joining the simul.'; + String get takeback => 'Takeback'; @override - String get simulHostExtraTimePerPlayer => 'Host extra clock time per player'; + String get proposeATakeback => 'Propose a takeback'; @override - String get lichessTournaments => 'Lichess tournaments'; + String get takebackPropositionSent => 'Takeback sent'; @override - String get tournamentFAQ => 'Arena tournament FAQ'; + String get takebackPropositionDeclined => 'Takeback declined'; @override - String get timeBeforeTournamentStarts => 'Time before tournament starts'; + String get takebackPropositionAccepted => 'Takeback accepted'; @override - String get averageCentipawnLoss => 'Average centipawn loss'; + String get takebackPropositionCanceled => 'Takeback canceled'; @override - String get accuracy => 'Accuracy'; + String get yourOpponentProposesATakeback => 'Your opponent proposes a takeback'; @override - String get keyboardShortcuts => 'Keyboard shortcuts'; + String get bookmarkThisGame => 'Bookmark this game'; @override - String get keyMoveBackwardOrForward => 'move backward/forward'; + String get tournament => 'Tournament'; @override - String get keyGoToStartOrEnd => 'go to start/end'; + String get tournaments => 'Tournaments'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get tournamentPoints => 'Tournament points'; @override - String get keyShowOrHideComments => 'show/hide comments'; + String get viewTournament => 'View tournament'; @override - String get keyEnterOrExitVariation => 'enter/exit variation'; + String get backToTournament => 'Back to tournament'; @override - String get keyRequestComputerAnalysis => 'Request computer analysis, learn from your mistakes'; + String get noDrawBeforeSwissLimit => 'You cannot draw before 30 moves are played in a Swiss tournament.'; @override - String get keyNextLearnFromYourMistakes => 'Next (learn from your mistakes)'; + String get thematic => 'Thematic'; @override - String get keyNextBlunder => 'Next blunder'; + String yourPerfRatingIsProvisional(String param) { + return 'Your $param rating is provisional'; + } @override - String get keyNextMistake => 'Next mistake'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return 'Your $param1 rating ($param2) is too high'; + } @override - String get keyNextInaccuracy => 'Next inaccuracy'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return 'Your top weekly $param1 rating ($param2) is too high'; + } @override - String get keyPreviousBranch => 'Previous branch'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return 'Your $param1 rating ($param2) is too low'; + } @override - String get keyNextBranch => 'Next branch'; + String ratedMoreThanInPerf(String param1, String param2) { + return 'Rated ≥ $param1 in $param2'; + } @override - String get toggleVariationArrows => 'Toggle variation arrows'; + String ratedLessThanInPerf(String param1, String param2) { + return 'Rated ≤ $param1 in $param2 for the last week'; + } @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String mustBeInTeam(String param) { + return 'Must be in team $param'; + } @override - String get toggleGlyphAnnotations => 'Toggle move annotations'; + String youAreNotInTeam(String param) { + return 'You are not in the team $param'; + } @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get backToGame => 'Back to game'; @override - String get variationArrowsInfo => 'Variation arrows let you navigate without using the move list.'; + String get siteDescription => 'Free online chess server. Play chess in a clean interface. No registration, no ads, no plugin required. Play chess with the computer, friends or random opponents.'; @override - String get playSelectedMove => 'play selected move'; + String xJoinedTeamY(String param1, String param2) { + return '$param1 joined team $param2'; + } @override - String get newTournament => 'New tournament'; + String xCreatedTeamY(String param1, String param2) { + return '$param1 created team $param2'; + } @override - String get tournamentHomeTitle => 'Chess tournaments featuring various time controls and variants'; + String get startedStreaming => 'started streaming'; @override - String get tournamentHomeDescription => 'Play fast-paced chess tournaments! Join an official scheduled tournament, or create your own. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck, and more options available for endless chess fun.'; + String xStartedStreaming(String param) { + return '$param started streaming'; + } @override - String get tournamentNotFound => 'Tournament not found'; + String get averageElo => 'Average rating'; @override - String get tournamentDoesNotExist => 'This tournament does not exist.'; + String get location => 'Location'; @override - String get tournamentMayHaveBeenCanceled => 'The tournament may have been canceled if all players left before it started.'; + String get filterGames => 'Filter games'; @override - String get returnToTournamentsHomepage => 'Return to tournaments homepage'; + String get reset => 'Reset'; @override - String weeklyPerfTypeRatingDistribution(String param) { - return 'Weekly $param rating distribution'; - } + String get apply => 'Submit'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'Your $param1 rating is $param2.'; - } + String get save => 'Save'; @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'You are better than $param1 of $param2 players.'; - } + String get leaderboard => 'Leaderboard'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 is better than $param2 of $param3 players.'; - } + String get screenshotCurrentPosition => 'Screenshot current position'; @override - String betterThanPercentPlayers(String param1, String param2) { - return 'Better than $param1 of $param2 players'; - } + String get gameAsGIF => 'Game as GIF'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'You do not have an established $param rating.'; - } + String get pasteTheFenStringHere => 'Paste the FEN text here'; @override - String get yourRating => 'Your rating'; + String get pasteThePgnStringHere => 'Paste the PGN text here'; @override - String get cumulative => 'Cumulative'; + String get orUploadPgnFile => 'Or upload a PGN file'; @override - String get glicko2Rating => 'Glicko-2 rating'; + String get fromPosition => 'From position'; @override - String get checkYourEmail => 'Check your Email'; + String get continueFromHere => 'Continue from here'; @override - String get weHaveSentYouAnEmailClickTheLink => 'We\'ve sent you an email. Click the link in the email to activate your account.'; + String get toStudy => 'Study'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.'; + String get importGame => 'Import game'; @override - String weHaveSentYouAnEmailTo(String param) { - return 'We\'ve sent an email to $param. Click the link in the email to reset your password.'; - } + String get importGameExplanation => 'Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL.'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'By registering, you agree to be bound by our $param.'; - } + String get importGameCaveat => 'Variations will be erased. To keep them, import the PGN via a study.'; @override - String readAboutOur(String param) { - return 'Read about our $param.'; - } + String get importGameDataPrivacyWarning => 'This PGN can be accessed by the public. To import a game privately, use a study.'; @override - String get networkLagBetweenYouAndLichess => 'Network lag between you and lichess'; + String get thisIsAChessCaptcha => 'This is a chess CAPTCHA.'; @override - String get timeToProcessAMoveOnLichessServer => 'Time to process a move on lichess server'; + String get clickOnTheBoardToMakeYourMove => 'Click on the board to make your move, and prove you are human.'; @override - String get downloadAnnotated => 'Download annotated'; + String get captcha_fail => 'Please solve the chess captcha.'; @override - String get downloadRaw => 'Download raw'; + String get notACheckmate => 'Not a checkmate'; @override - String get downloadImported => 'Download imported'; + String get whiteCheckmatesInOneMove => 'White to checkmate in one move'; @override - String get crosstable => 'Crosstable'; + String get blackCheckmatesInOneMove => 'Black to checkmate in one move'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'You can also scroll over the board to move in the game.'; + String get retry => 'Retry'; @override - String get scrollOverComputerVariationsToPreviewThem => 'Scroll over computer variations to preview them.'; + String get reconnecting => 'Reconnecting'; @override - String get analysisShapesHowTo => 'Press shift+click or right-click to draw circles and arrows on the board.'; + String get noNetwork => 'Offline'; @override - String get letOtherPlayersMessageYou => 'Let other players message you'; + String get favoriteOpponents => 'Favorite opponents'; @override - String get receiveForumNotifications => 'Receive notifications when mentioned in the forum'; + String get follow => 'Follow'; @override - String get shareYourInsightsData => 'Share your chess insights data'; + String get following => 'Following'; @override - String get withNobody => 'With nobody'; + String get unfollow => 'Unfollow'; @override - String get withFriends => 'With friends'; + String followX(String param) { + return 'Follow $param'; + } @override - String get withEverybody => 'With everybody'; + String unfollowX(String param) { + return 'Unfollow $param'; + } @override - String get kidMode => 'Kid mode'; + String get block => 'Block'; @override - String get kidModeIsEnabled => 'Child-mode is enabled.'; + String get blocked => 'Blocked'; @override - String get kidModeExplanation => 'This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.'; + String get unblock => 'Unblock'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return 'In kid mode, the lichess logo gets a $param icon, so you know your kids are safe.'; + String xStartedFollowingY(String param1, String param2) { + return '$param1 started following $param2'; } @override - String get askYourChessTeacherAboutLiftingKidMode => 'Your account is managed. Ask your chess teacher about lifting kid mode.'; + String get more => 'More'; @override - String get enableKidMode => 'Enable Kid mode'; + String get memberSince => 'Member since'; @override - String get disableKidMode => 'Disable Kid mode'; + String lastSeenActive(String param) { + return 'Active $param'; + } @override - String get security => 'Security'; + String get player => 'Player'; @override - String get sessions => 'Sessions'; + String get list => 'List'; @override - String get revokeAllSessions => 'revoke all sessions'; + String get graph => 'Graph'; @override - String get playChessEverywhere => 'Play chess everywhere'; + String get required => 'Required.'; @override - String get asFreeAsLichess => 'As free as lichess'; + String get openTournaments => 'Open tournaments'; @override - String get builtForTheLoveOfChessNotMoney => 'Built for the love of chess, not money'; + String get duration => 'Duration'; @override - String get everybodyGetsAllFeaturesForFree => 'Everybody gets all features for free'; + String get winner => 'Winner'; @override - String get zeroAdvertisement => 'Zero advertisement'; + String get standing => 'Standing'; @override - String get fullFeatured => 'Full featured'; + String get createANewTournament => 'Create a new tournament'; @override - String get phoneAndTablet => 'Phone and tablet'; + String get tournamentCalendar => 'Tournament calendar'; @override - String get bulletBlitzClassical => 'Bullet, blitz, classical'; + String get conditionOfEntry => 'Entry requirements:'; @override - String get correspondenceChess => 'Correspondence chess'; + String get advancedSettings => 'Advanced settings'; @override - String get onlineAndOfflinePlay => 'Online and offline play'; + String get safeTournamentName => 'Pick a very safe name for the tournament.'; @override - String get viewTheSolution => 'View the solution'; + String get inappropriateNameWarning => 'Anything even slightly inappropriate could get your account closed.'; @override - String get followAndChallengeFriends => 'Follow and challenge friends'; + String get emptyTournamentName => 'Leave empty to name the tournament after a notable chess player.'; @override - String get gameAnalysis => 'Game analysis'; + String get makePrivateTournament => 'Make the tournament private, and restrict access with a password'; @override - String xHostsY(String param1, String param2) { - return '$param1 hosts $param2'; - } + String get join => 'Join'; @override - String xJoinsY(String param1, String param2) { - return '$param1 joins $param2'; - } + String get withdraw => 'Withdraw'; @override - String xLikesY(String param1, String param2) { - return '$param1 likes $param2'; - } + String get points => 'Points'; @override - String get quickPairing => 'Quick pairing'; + String get wins => 'Wins'; @override - String get lobby => 'Lobby'; + String get losses => 'Losses'; @override - String get anonymous => 'Anonymous'; + String get createdBy => 'Created by'; @override - String yourScore(String param) { - return 'Your score: $param'; + String get tournamentIsStarting => 'The tournament is starting'; + + @override + String get tournamentPairingsAreNowClosed => 'The tournament pairings are now closed.'; + + @override + String standByX(String param) { + return 'Stand by $param, pairing players, get ready!'; } @override - String get language => 'Language'; + String get pause => 'Pause'; @override - String get background => 'Background'; + String get resume => 'Resume'; @override - String get light => 'Light'; + String get youArePlaying => 'You are playing!'; @override - String get dark => 'Dark'; + String get winRate => 'Win rate'; @override - String get transparent => 'Transparent'; + String get berserkRate => 'Berserk rate'; @override - String get deviceTheme => 'Device theme'; + String get performance => 'Performance'; @override - String get backgroundImageUrl => 'Background image URL:'; + String get tournamentComplete => 'Tournament complete'; @override - String get board => 'Board'; + String get movesPlayed => 'Moves played'; @override - String get size => 'Size'; + String get whiteWins => 'White wins'; + + @override + String get blackWins => 'Black wins'; + + @override + String get drawRate => 'Draw rate'; + + @override + String get draws => 'Draws'; + + @override + String nextXTournament(String param) { + return 'Next $param tournament:'; + } + + @override + String get averageOpponent => 'Average opponent'; + + @override + String get boardEditor => 'Board editor'; + + @override + String get setTheBoard => 'Set the board'; + + @override + String get popularOpenings => 'Popular openings'; + + @override + String get endgamePositions => 'Endgame positions'; + + @override + String chess960StartPosition(String param) { + return 'Chess960 start position: $param'; + } + + @override + String get startPosition => 'Starting position'; + + @override + String get clearBoard => 'Clear board'; + + @override + String get loadPosition => 'Load position'; + + @override + String get isPrivate => 'Private'; + + @override + String reportXToModerators(String param) { + return 'Report $param to moderators'; + } + + @override + String profileCompletion(String param) { + return 'Profile completion: $param'; + } + + @override + String xRating(String param) { + return '$param rating'; + } + + @override + String get ifNoneLeaveEmpty => 'If none, leave empty'; + + @override + String get profile => 'Profile'; + + @override + String get editProfile => 'Edit profile'; + + @override + String get realName => 'Real name'; + + @override + String get setFlair => 'Set your flair'; + + @override + String get flair => 'Flair'; + + @override + String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + + @override + String get biography => 'Biography'; + + @override + String get countryRegion => 'Country or region'; + + @override + String get thankYou => 'Thank you!'; + + @override + String get socialMediaLinks => 'Social media links'; + + @override + String get oneUrlPerLine => 'One URL per line.'; + + @override + String get inlineNotation => 'Inline notation'; + + @override + String get makeAStudy => 'For safekeeping and sharing, consider making a study.'; + + @override + String get clearSavedMoves => 'Clear moves'; + + @override + String get previouslyOnLichessTV => 'Previously on Lichess TV'; + + @override + String get onlinePlayers => 'Online players'; + + @override + String get activePlayers => 'Active players'; + + @override + String get bewareTheGameIsRatedButHasNoClock => 'Beware, the game is rated but has no clock!'; + + @override + String get success => 'Success'; + + @override + String get automaticallyProceedToNextGameAfterMoving => 'Automatically proceed to next game after moving'; + + @override + String get autoSwitch => 'Auto switch'; + + @override + String get puzzles => 'Puzzles'; + + @override + String get onlineBots => 'Online bots'; + + @override + String get name => 'Name'; + + @override + String get description => 'Description'; + + @override + String get descPrivate => 'Private description'; + + @override + String get descPrivateHelp => 'Text that only the team members will see. If set, replaces the public description for team members.'; + + @override + String get no => 'No'; + + @override + String get yes => 'Yes'; + + @override + String get website => 'Website'; + + @override + String get mobile => 'Mobile'; + + @override + String get help => 'Help:'; + + @override + String get createANewTopic => 'Create a new topic'; + + @override + String get topics => 'Topics'; + + @override + String get posts => 'Posts'; + + @override + String get lastPost => 'Last post'; + + @override + String get views => 'Views'; + + @override + String get replies => 'Replies'; + + @override + String get replyToThisTopic => 'Reply to this topic'; + + @override + String get reply => 'Reply'; + + @override + String get message => 'Message'; + + @override + String get createTheTopic => 'Create the topic'; + + @override + String get reportAUser => 'Report a user'; + + @override + String get user => 'User'; + + @override + String get reason => 'Reason'; + + @override + String get whatIsIheMatter => 'What\'s the matter?'; + + @override + String get cheat => 'Cheat'; + + @override + String get troll => 'Troll'; + + @override + String get other => 'Other'; + + @override + String get reportCheatBoostHelp => 'Paste a link to the game(s) and explain what is wrong with this user\'s behavior. Don\'t just say \"they cheat,\" but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain why this username is offensive. Don\'t just say \"it\'s offensive/inappropriate,\" but tell us how you came to this conclusion, especially if the offense is obscure, not in English, in slang, or a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; + + @override + String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; + + @override + String by(String param) { + return 'by $param'; + } + + @override + String importedByX(String param) { + return 'Imported by $param'; + } + + @override + String get thisTopicIsNowClosed => 'This topic is now closed.'; + + @override + String get blog => 'Blog'; + + @override + String get notes => 'Notes'; + + @override + String get typePrivateNotesHere => 'Type private notes here'; + + @override + String get writeAPrivateNoteAboutThisUser => 'Write a private note about this user'; + + @override + String get noNoteYet => 'No note yet'; + + @override + String get invalidUsernameOrPassword => 'Invalid username or password'; + + @override + String get incorrectPassword => 'Incorrect password'; + + @override + String get invalidAuthenticationCode => 'Invalid authentication code'; + + @override + String get emailMeALink => 'Email me a link'; + + @override + String get currentPassword => 'Current password'; + + @override + String get newPassword => 'New password'; + + @override + String get newPasswordAgain => 'New password (again)'; + + @override + String get newPasswordsDontMatch => 'The new passwords don\'t match'; + + @override + String get newPasswordStrength => 'Password strength'; + + @override + String get clockInitialTime => 'Clock initial time'; + + @override + String get clockIncrement => 'Clock increment'; + + @override + String get privacy => 'Privacy'; + + @override + String get privacyPolicy => 'Privacy policy'; + + @override + String get letOtherPlayersFollowYou => 'Let other players follow you'; + + @override + String get letOtherPlayersChallengeYou => 'Let other players challenge you'; + + @override + String get letOtherPlayersInviteYouToStudy => 'Let other players invite you to study'; + + @override + String get sound => 'Sound'; + + @override + String get none => 'None'; + + @override + String get fast => 'Fast'; + + @override + String get normal => 'Normal'; + + @override + String get slow => 'Slow'; + + @override + String get insideTheBoard => 'Inside the board'; + + @override + String get outsideTheBoard => 'Outside the board'; + + @override + String get allSquaresOfTheBoard => 'All squares on the board'; + + @override + String get onSlowGames => 'On slow games'; + + @override + String get always => 'Always'; + + @override + String get never => 'Never'; + + @override + String xCompetesInY(String param1, String param2) { + return '$param1 competes in $param2'; + } + + @override + String get victory => 'Victory'; + + @override + String get defeat => 'Defeat'; + + @override + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } + + @override + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } + + @override + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } + + @override + String get timeline => 'Timeline'; + + @override + String get starting => 'Starting:'; + + @override + String get allInformationIsPublicAndOptional => 'All information is public and optional.'; + + @override + String get biographyDescription => 'Talk about yourself, your interests, what you like in chess, your favorite openings, players, ...'; + + @override + String get listBlockedPlayers => 'List players you have blocked'; + + @override + String get human => 'Human'; + + @override + String get computer => 'Computer'; + + @override + String get side => 'Side'; + + @override + String get clock => 'Clock'; + + @override + String get opponent => 'Opponent'; + + @override + String get learnMenu => 'Learn'; + + @override + String get studyMenu => 'Study'; + + @override + String get practice => 'Practice'; + + @override + String get community => 'Community'; + + @override + String get tools => 'Tools'; + + @override + String get increment => 'Increment'; + + @override + String get error_unknown => 'Invalid value'; + + @override + String get error_required => 'This field is required'; + + @override + String get error_email => 'This email address is invalid'; + + @override + String get error_email_acceptable => 'This email address is not acceptable. Please double-check it, and try again.'; + + @override + String get error_email_unique => 'Email address invalid or already taken'; + + @override + String get error_email_different => 'This is already your email address'; + + @override + String error_minLength(String param) { + return 'Must be at least $param characters long'; + } + + @override + String error_maxLength(String param) { + return 'Must be at most $param characters long'; + } + + @override + String error_min(String param) { + return 'Must be at least $param'; + } + + @override + String error_max(String param) { + return 'Must be at most $param'; + } + + @override + String ifRatingIsPlusMinusX(String param) { + return 'If rating is ± $param'; + } + + @override + String get ifRegistered => 'If registered'; + + @override + String get onlyExistingConversations => 'Only existing conversations'; + + @override + String get onlyFriends => 'Only friends'; + + @override + String get menu => 'Menu'; + + @override + String get castling => 'Castling'; + + @override + String get whiteCastlingKingside => 'White O-O'; + + @override + String get blackCastlingKingside => 'Black O-O'; + + @override + String tpTimeSpentPlaying(String param) { + return 'Time spent playing: $param'; + } + + @override + String get watchGames => 'Watch games'; + + @override + String tpTimeSpentOnTV(String param) { + return 'Time featured on TV: $param'; + } + + @override + String get watch => 'Watch'; + + @override + String get videoLibrary => 'Video library'; + + @override + String get streamersMenu => 'Streamers'; + + @override + String get mobileApp => 'Mobile App'; + + @override + String get webmasters => 'Webmasters'; + + @override + String get about => 'About'; + + @override + String aboutX(String param) { + return 'About $param'; + } + + @override + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1 is a free ($param2), libre, no-ads, open source chess server.'; + } + + @override + String get really => 'really'; + + @override + String get contribute => 'Contribute'; + + @override + String get termsOfService => 'Terms of Service'; + + @override + String get sourceCode => 'Source Code'; + + @override + String get simultaneousExhibitions => 'Simultaneous exhibitions'; + + @override + String get host => 'Host'; + + @override + String hostColorX(String param) { + return 'Host color: $param'; + } + + @override + String get yourPendingSimuls => 'Your pending simuls'; + + @override + String get createdSimuls => 'Newly created simuls'; + + @override + String get hostANewSimul => 'Host a new simul'; + + @override + String get signUpToHostOrJoinASimul => 'Sign up to join or host a simul'; + + @override + String get noSimulFound => 'Simul not found'; + + @override + String get noSimulExplanation => 'This simultaneous exhibition does not exist.'; + + @override + String get returnToSimulHomepage => 'Return to simul homepage'; + + @override + String get aboutSimul => 'Simuls involve a single player facing several players at once.'; + + @override + String get aboutSimulImage => 'Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.'; + + @override + String get aboutSimulRealLife => 'The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.'; + + @override + String get aboutSimulRules => 'When the simul starts, every player starts a game with the host. The simul ends when all games are complete.'; + + @override + String get aboutSimulSettings => 'Simuls are always casual. Rematches, takebacks and adding time are disabled.'; + + @override + String get create => 'Create'; + + @override + String get whenCreateSimul => 'When you create a Simul, you get to play several players at once.'; + + @override + String get simulVariantsHint => 'If you select several variants, each player gets to choose which one to play.'; + + @override + String get simulClockHint => 'Fischer Clock setup. The more players you take on, the more time you may need.'; + + @override + String get simulAddExtraTime => 'You may add extra initial time to your clock to help you cope with the simul.'; + + @override + String get simulHostExtraTime => 'Host extra initial clock time'; + + @override + String get simulAddExtraTimePerPlayer => 'Add initial time to your clock for each player joining the simul.'; + + @override + String get simulHostExtraTimePerPlayer => 'Host extra clock time per player'; + + @override + String get lichessTournaments => 'Lichess tournaments'; + + @override + String get tournamentFAQ => 'Arena tournament FAQ'; + + @override + String get timeBeforeTournamentStarts => 'Time before tournament starts'; + + @override + String get averageCentipawnLoss => 'Average centipawn loss'; + + @override + String get accuracy => 'Accuracy'; + + @override + String get keyboardShortcuts => 'Keyboard shortcuts'; + + @override + String get keyMoveBackwardOrForward => 'move backward/forward'; + + @override + String get keyGoToStartOrEnd => 'go to start/end'; + + @override + String get keyCycleSelectedVariation => 'Cycle selected variation'; + + @override + String get keyShowOrHideComments => 'show/hide comments'; + + @override + String get keyEnterOrExitVariation => 'enter/exit variation'; + + @override + String get keyRequestComputerAnalysis => 'Request computer analysis, Learn from your mistakes'; + + @override + String get keyNextLearnFromYourMistakes => 'Next (Learn from your mistakes)'; + + @override + String get keyNextBlunder => 'Next blunder'; + + @override + String get keyNextMistake => 'Next mistake'; + + @override + String get keyNextInaccuracy => 'Next inaccuracy'; + + @override + String get keyPreviousBranch => 'Previous branch'; + + @override + String get keyNextBranch => 'Next branch'; + + @override + String get toggleVariationArrows => 'Toggle variation arrows'; + + @override + String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + + @override + String get toggleGlyphAnnotations => 'Toggle move annotations'; + + @override + String get togglePositionAnnotations => 'Toggle position annotations'; + + @override + String get variationArrowsInfo => 'Variation arrows let you navigate without using the move list.'; + + @override + String get playSelectedMove => 'play selected move'; + + @override + String get newTournament => 'New tournament'; + + @override + String get tournamentHomeTitle => 'Chess tournaments featuring various time controls and variants'; + + @override + String get tournamentHomeDescription => 'Play fast-paced chess tournaments! Join an official scheduled tournament, or create your own. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck, and more options available for endless chess fun.'; + + @override + String get tournamentNotFound => 'Tournament not found'; + + @override + String get tournamentDoesNotExist => 'This tournament does not exist.'; + + @override + String get tournamentMayHaveBeenCanceled => 'The tournament may have been canceled if all players left before it started.'; + + @override + String get returnToTournamentsHomepage => 'Return to tournaments homepage'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return 'Weekly $param rating distribution'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return 'Your $param1 rating is $param2.'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return 'You are better than $param1 of $param2 players.'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1 is better than $param2 of $param3 players.'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return 'Better than $param1 of $param2 players'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return 'You do not have an established $param rating.'; + } + + @override + String get yourRating => 'Your rating'; + + @override + String get cumulative => 'Cumulative'; + + @override + String get glicko2Rating => 'Glicko-2 rating'; + + @override + String get checkYourEmail => 'Check your Email'; + + @override + String get weHaveSentYouAnEmailClickTheLink => 'We\'ve sent you an email. Click the link in the email to activate your account.'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return 'We\'ve sent an email to $param. Click the link in the email to reset your password.'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return 'By registering, you agree to the $param.'; + } + + @override + String readAboutOur(String param) { + return 'Read about our $param.'; + } + + @override + String get networkLagBetweenYouAndLichess => 'Network lag between you and Lichess'; + + @override + String get timeToProcessAMoveOnLichessServer => 'Time to process a move on Lichess\'s server'; + + @override + String get downloadAnnotated => 'Download annotated'; + + @override + String get downloadRaw => 'Download raw'; + + @override + String get downloadImported => 'Download imported'; + + @override + String get crosstable => 'Crosstable'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'You can also scroll over the board to move in the game.'; + + @override + String get scrollOverComputerVariationsToPreviewThem => 'Scroll over computer variations to preview them.'; + + @override + String get analysisShapesHowTo => 'Press shift+click or right-click to draw circles and arrows on the board.'; + + @override + String get letOtherPlayersMessageYou => 'Let other players message you'; + + @override + String get receiveForumNotifications => 'Receive notifications when mentioned in the forum'; + + @override + String get shareYourInsightsData => 'Share your chess insights data'; + + @override + String get withNobody => 'With nobody'; + + @override + String get withFriends => 'With friends'; + + @override + String get withEverybody => 'With everybody'; + + @override + String get kidMode => 'Kid mode'; + + @override + String get kidModeIsEnabled => 'Kid mode is enabled.'; + + @override + String get kidModeExplanation => 'This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return 'In kid mode, the Lichess logo gets a $param icon, so you know your kids are safe.'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => 'Your account is managed. Ask your chess teacher about lifting kid mode.'; + + @override + String get enableKidMode => 'Enable Kid mode'; + + @override + String get disableKidMode => 'Disable Kid mode'; + + @override + String get security => 'Security'; + + @override + String get sessions => 'Sessions'; + + @override + String get revokeAllSessions => 'revoke all sessions'; + + @override + String get playChessEverywhere => 'Play chess everywhere'; + + @override + String get asFreeAsLichess => 'As free as Lichess'; + + @override + String get builtForTheLoveOfChessNotMoney => 'Built for the love of chess, not money'; + + @override + String get everybodyGetsAllFeaturesForFree => 'Everybody gets all features for free'; + + @override + String get zeroAdvertisement => 'Zero advertisement'; + + @override + String get fullFeatured => 'Full featured'; + + @override + String get phoneAndTablet => 'Phone and tablet'; + + @override + String get bulletBlitzClassical => 'Bullet, blitz, classical'; + + @override + String get correspondenceChess => 'Correspondence chess'; + + @override + String get onlineAndOfflinePlay => 'Online and offline play'; + + @override + String get viewTheSolution => 'View the solution'; + + @override + String get followAndChallengeFriends => 'Follow and challenge friends'; + + @override + String get gameAnalysis => 'Game analysis'; + + @override + String xHostsY(String param1, String param2) { + return '$param1 hosts $param2'; + } + + @override + String xJoinsY(String param1, String param2) { + return '$param1 joins $param2'; + } + + @override + String xLikesY(String param1, String param2) { + return '$param1 likes $param2'; + } + + @override + String get quickPairing => 'Quick pairing'; + + @override + String get lobby => 'Lobby'; + + @override + String get anonymous => 'Anonymous'; + + @override + String yourScore(String param) { + return 'Your score: $param'; + } + + @override + String get language => 'Language'; + + @override + String get background => 'Background'; + + @override + String get light => 'Light'; + + @override + String get dark => 'Dark'; + + @override + String get transparent => 'Transparent'; + + @override + String get deviceTheme => 'Device theme'; + + @override + String get backgroundImageUrl => 'Background image URL:'; + + @override + String get board => 'Board'; + + @override + String get size => 'Size'; + + @override + String get opacity => 'Opacity'; + + @override + String get brightness => 'Brightness'; + + @override + String get hue => 'Hue'; + + @override + String get boardReset => 'Reset colors to default'; + + @override + String get pieceSet => 'Piece set'; + + @override + String get embedInYourWebsite => 'Embed in your website'; + + @override + String get usernameAlreadyUsed => 'This username is already in use, please try another one.'; + + @override + String get usernamePrefixInvalid => 'The username must start with a letter.'; + + @override + String get usernameSuffixInvalid => 'The username must end with a letter or a number.'; + + @override + String get usernameCharsInvalid => 'The username must only contain letters, numbers, underscores, and hyphens. Consecutive underscores and hyphens are not allowed.'; + + @override + String get usernameUnacceptable => 'This username is not acceptable.'; + + @override + String get playChessInStyle => 'Play chess in style'; + + @override + String get chessBasics => 'Chess basics'; + + @override + String get coaches => 'Coaches'; + + @override + String get invalidPgn => 'Invalid PGN'; + + @override + String get invalidFen => 'Invalid FEN'; + + @override + String get custom => 'Custom'; + + @override + String get notifications => 'Notifications'; + + @override + String notificationsX(String param1) { + return 'Notifications: $param1'; + } + + @override + String perfRatingX(String param) { + return 'Rating: $param'; + } + + @override + String get practiceWithComputer => 'Practice with computer'; + + @override + String anotherWasX(String param) { + return 'Another was $param'; + } + + @override + String bestWasX(String param) { + return 'Best was $param'; + } + + @override + String get youBrowsedAway => 'You browsed away'; + + @override + String get resumePractice => 'Resume practice'; + + @override + String get drawByFiftyMoves => 'The game has been drawn by the fifty-move rule.'; + + @override + String get theGameIsADraw => 'The game is a draw.'; + + @override + String get computerThinking => 'Computer thinking ...'; + + @override + String get seeBestMove => 'See best move'; + + @override + String get hideBestMove => 'Hide the best move'; + + @override + String get getAHint => 'Get a hint'; + + @override + String get evaluatingYourMove => 'Evaluating your move ...'; + + @override + String get whiteWinsGame => 'White wins'; + + @override + String get blackWinsGame => 'Black wins'; + + @override + String get learnFromYourMistakes => 'Learn from your mistakes'; + + @override + String get learnFromThisMistake => 'Learn from this mistake'; + + @override + String get skipThisMove => 'Skip this move'; + + @override + String get next => 'Next'; + + @override + String xWasPlayed(String param) { + return '$param was played'; + } + + @override + String get findBetterMoveForWhite => 'Find a better move for white'; + + @override + String get findBetterMoveForBlack => 'Find a better move for black'; + + @override + String get resumeLearning => 'Resume learning'; + + @override + String get youCanDoBetter => 'You can do better'; + + @override + String get tryAnotherMoveForWhite => 'Try another move for white'; + + @override + String get tryAnotherMoveForBlack => 'Try another move for black'; + + @override + String get solution => 'Solution'; + + @override + String get waitingForAnalysis => 'Waiting for analysis'; + + @override + String get noMistakesFoundForWhite => 'No significant mistakes found for White'; + + @override + String get noMistakesFoundForBlack => 'No significant mistakes found for Black'; + + @override + String get doneReviewingWhiteMistakes => 'Done reviewing White mistakes'; + + @override + String get doneReviewingBlackMistakes => 'Done reviewing Black mistakes'; + + @override + String get doItAgain => 'Do it again'; + + @override + String get reviewWhiteMistakes => 'Review White mistakes'; + + @override + String get reviewBlackMistakes => 'Review Black mistakes'; + + @override + String get advantage => 'Advantage'; + + @override + String get opening => 'Opening'; + + @override + String get middlegame => 'Middlegame'; + + @override + String get endgame => 'Endgame'; + + @override + String get conditionalPremoves => 'Conditional premoves'; + + @override + String get addCurrentVariation => 'Add current variation'; + + @override + String get playVariationToCreateConditionalPremoves => 'Play a variation to create conditional premoves'; + + @override + String get noConditionalPremoves => 'No conditional premoves'; + + @override + String playX(String param) { + return 'Play $param'; + } + + @override + String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; + + @override + String get clickHereToReadIt => 'Click here to read it'; + + @override + String get sorry => 'Sorry :('; + + @override + String get weHadToTimeYouOutForAWhile => 'We had to time you out for a while.'; + + @override + String get why => 'Why?'; + + @override + String get pleasantChessExperience => 'We aim to provide a pleasant chess experience for everyone.'; + + @override + String get goodPractice => 'To that effect, we must ensure that all players follow good practice.'; + + @override + String get potentialProblem => 'When a potential problem is detected, we display this message.'; + + @override + String get howToAvoidThis => 'How to avoid this?'; + + @override + String get playEveryGame => 'Play every game you start.'; + + @override + String get tryToWin => 'Try to win (or at least draw) every game you play.'; + + @override + String get resignLostGames => 'Resign lost games (don\'t let the clock run down).'; + + @override + String get temporaryInconvenience => 'We apologize for the temporary inconvenience,'; + + @override + String get wishYouGreatGames => 'and wish you great games on lichess.org.'; + + @override + String get thankYouForReading => 'Thank you for reading!'; + + @override + String get lifetimeScore => 'Lifetime score'; + + @override + String get currentMatchScore => 'Current match score'; + + @override + String get agreementAssistance => 'I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).'; + + @override + String get agreementNice => 'I agree that I will always be respectful to other players.'; + + @override + String agreementMultipleAccounts(String param) { + return 'I agree that I will not create multiple accounts (except for the reasons stated in the $param).'; + } + + @override + String get agreementPolicy => 'I agree that I will follow all Lichess policies.'; + + @override + String get searchOrStartNewDiscussion => 'Search or start new conversation'; + + @override + String get edit => 'Edit'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => 'Rapid'; + + @override + String get classical => 'Classical'; + + @override + String get ultraBulletDesc => 'Insanely fast games: less than 30 seconds'; + + @override + String get bulletDesc => 'Very fast games: less than 3 minutes'; + + @override + String get blitzDesc => 'Fast games: 3 to 8 minutes'; + + @override + String get rapidDesc => 'Rapid games: 8 to 25 minutes'; + + @override + String get classicalDesc => 'Classical games: 25 minutes and more'; + + @override + String get correspondenceDesc => 'Correspondence games: one or several days per move'; + + @override + String get puzzleDesc => 'Chess tactics trainer'; + + @override + String get important => 'Important'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return 'Your question may already have an answer $param1'; + } + + @override + String get inTheFAQ => 'in the F.A.Q.'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return 'To report a user for cheating or bad behavior, $param1'; + } + + @override + String get useTheReportForm => 'use the report form'; + + @override + String toRequestSupport(String param1) { + return 'To request support, $param1'; + } + + @override + String get tryTheContactPage => 'try the contact page'; + + @override + String makeSureToRead(String param1) { + return 'Make sure to read $param1'; + } + + @override + String get theForumEtiquette => 'the forum etiquette'; + + @override + String get thisTopicIsArchived => 'This topic has been archived and can no longer be replied to.'; + + @override + String joinTheTeamXToPost(String param1) { + return 'Join the $param1, to post in this forum'; + } + + @override + String teamNamedX(String param1) { + return '$param1 team'; + } + + @override + String get youCannotPostYetPlaySomeGames => 'You can\'t post in the forums yet. Play some games!'; + + @override + String get subscribe => 'Subscribe'; + + @override + String get unsubscribe => 'Unsubscribe'; + + @override + String mentionedYouInX(String param1) { + return 'mentioned you in \"$param1\".'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 mentioned you in \"$param2\".'; + } + + @override + String invitedYouToX(String param1) { + return 'invited you to \"$param1\".'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 invited you to \"$param2\".'; + } + + @override + String get youAreNowPartOfTeam => 'You are now part of the team.'; + + @override + String youHaveJoinedTeamX(String param1) { + return 'You have joined \"$param1\".'; + } + + @override + String get someoneYouReportedWasBanned => 'Someone you reported was banned'; + + @override + String get congratsYouWon => 'Congratulations, you won!'; + + @override + String gameVsX(String param1) { + return 'Game vs $param1'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => 'You lost rating points to someone who violated the Lichess TOS'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return 'Refund: $param1 $param2 rating points.'; + } + + @override + String get timeAlmostUp => 'Time is almost up!'; + + @override + String get clickToRevealEmailAddress => '[Click to reveal email address]'; + + @override + String get download => 'Download'; + + @override + String get coachManager => 'Coach manager'; + + @override + String get streamerManager => 'Streamer manager'; + + @override + String get cancelTournament => 'Cancel the tournament'; + + @override + String get tournDescription => 'Tournament description'; + + @override + String get tournDescriptionHelp => 'Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)'; + + @override + String get ratedFormHelp => 'Games are rated and impact players ratings'; + + @override + String get onlyMembersOfTeam => 'Only members of team'; + + @override + String get noRestriction => 'No restriction'; + + @override + String get minimumRatedGames => 'Minimum rated games'; + + @override + String get minimumRating => 'Minimum rating'; + + @override + String get maximumWeeklyRating => 'Maximum weekly rating'; + + @override + String positionInputHelp(String param) { + return 'Paste a valid FEN to start every game from a given position.\nIt only works for standard games, not with variants.\nYou can use the $param to generate a FEN position, then paste it here.\nLeave empty to start games from the normal initial position.'; + } + + @override + String get cancelSimul => 'Cancel the simul'; + + @override + String get simulHostcolor => 'Host color for each game'; + + @override + String get estimatedStart => 'Estimated start time'; + + @override + String simulFeatured(String param) { + return 'Feature on $param'; + } + + @override + String simulFeaturedHelp(String param) { + return 'Show your simul to everyone on $param. Disable for private simuls.'; + } + + @override + String get simulDescription => 'Simul description'; + + @override + String get simulDescriptionHelp => 'Anything you want to tell the participants?'; + + @override + String markdownAvailable(String param) { + return '$param is available for more advanced syntax.'; + } + + @override + String get embedsAvailable => 'Paste a game URL or a study chapter URL to embed it.'; + + @override + String get inYourLocalTimezone => 'In your own local timezone'; + + @override + String get tournChat => 'Tournament chat'; + + @override + String get noChat => 'No chat'; + + @override + String get onlyTeamLeaders => 'Only team leaders'; + + @override + String get onlyTeamMembers => 'Only team members'; + + @override + String get navigateMoveTree => 'Navigate the move tree'; + + @override + String get mouseTricks => 'Mouse tricks'; + + @override + String get toggleLocalAnalysis => 'Toggle local computer analysis'; + + @override + String get toggleAllAnalysis => 'Toggle all computer analysis'; + + @override + String get playComputerMove => 'Play best computer move'; + + @override + String get analysisOptions => 'Analysis options'; + + @override + String get focusChat => 'Focus chat'; + + @override + String get showHelpDialog => 'Show this help dialog'; + + @override + String get reopenYourAccount => 'Reopen your account'; + + @override + String get closedAccountChangedMind => 'If you closed your account, but have since changed your mind, you get one chance of getting your account back.'; + + @override + String get onlyWorksOnce => 'This will only work once.'; + + @override + String get cantDoThisTwice => 'If you close your account a second time, there will be no way of recovering it.'; + + @override + String get emailAssociatedToaccount => 'Email address associated to the account'; + + @override + String get sentEmailWithLink => 'We\'ve sent you an email with a link.'; + + @override + String get tournamentEntryCode => 'Tournament entry code'; + + @override + String get hangOn => 'Hang on!'; + + @override + String gameInProgress(String param) { + return 'You have a game in progress with $param.'; + } + + @override + String get abortTheGame => 'Abort the game'; + + @override + String get resignTheGame => 'Resign the game'; + + @override + String get youCantStartNewGame => 'You can\'t start a new game until this one is finished.'; + + @override + String get since => 'Since'; + + @override + String get until => 'Until'; + + @override + String get lichessDbExplanation => 'Rated games sampled from all Lichess players'; + + @override + String get switchSides => 'Switch sides'; + + @override + String get closingAccountWithdrawAppeal => 'Closing your account will withdraw your appeal'; + + @override + String get ourEventTips => 'Our tips for organizing events'; + + @override + String get instructions => 'Instructions'; + + @override + String get showMeEverything => 'Show me everything'; + + @override + String get lichessPatronInfo => 'Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.'; + + @override + String get nothingToSeeHere => 'Nothing to see here at the moment.'; + + @override + String get stats => 'Stats'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Your opponent left the game. You can claim victory in $count seconds.', + one: 'Your opponent left the game. You can claim victory in $count second.', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Mate in $count half-moves', + one: 'Mate in $count half-move', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count blunders', + one: '$count blunder', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mistakes', + one: '$count mistake', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count inaccuracies', + one: '$count inaccuracy', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count players', + one: '$count player', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games', + one: '$count game', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rating over $param2 games', + one: '$count rating over $param2 game', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bookmarks', + one: '$count bookmark', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days', + one: '$count day', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours', + one: '$count hour', + ); + return '$_temp0'; + } + + @override + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes', + one: '$count minute', + ); + return '$_temp0'; + } + + @override + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Rank is updated every $count minutes', + one: 'Rank is updated every minute', + ); + return '$_temp0'; + } + + @override + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count puzzles', + one: '$count puzzle', + ); + return '$_temp0'; + } + + @override + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games with you', + one: '$count game with you', + ); + return '$_temp0'; + } + + @override + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rated', + one: '$count rated', + ); + return '$_temp0'; + } + + @override + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count wins', + one: '$count win', + ); + return '$_temp0'; + } + + @override + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count losses', + one: '$count loss', + ); + return '$_temp0'; + } + + @override + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count draws', + one: '$count draw', + ); + return '$_temp0'; + } + + @override + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playing', + one: '$count playing', + ); + return '$_temp0'; + } + + @override + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Give $count seconds', + one: 'Give $count second', + ); + return '$_temp0'; + } + + @override + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tournament points', + one: '$count tournament point', + ); + return '$_temp0'; + } + + @override + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count studies', + one: '$count study', + ); + return '$_temp0'; + } + + @override + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count simuls', + one: '$count simul', + ); + return '$_temp0'; + } + + @override + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count rated games', + one: '≥ $count rated game', + ); + return '$_temp0'; + } + + @override + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 rated games', + one: '≥ $count $param2 rated game', + ); + return '$_temp0'; + } + + @override + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'You need to play $count more $param2 rated games', + one: 'You need to play $count more $param2 rated game', + ); + return '$_temp0'; + } + + @override + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'You need to play $count more rated games', + one: 'You need to play $count more rated game', + ); + return '$_temp0'; + } + + @override + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count imported games', + one: '$count imported game', + ); + return '$_temp0'; + } + + @override + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count friends online', + one: '$count friend online', + ); + return '$_temp0'; + } + + @override + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count followers', + one: '$count follower', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count following', + one: '$count following', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Less than $count minutes', + one: 'Less than $count minute', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games in play', + one: '$count game in play', + ); + return '$_temp0'; + } + + @override + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Maximum: $count characters.', + one: 'Maximum: $count character.', + ); + return '$_temp0'; + } + + @override + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count blocks', + one: '$count block', + ); + return '$_temp0'; + } + + @override + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count forum posts', + one: '$count forum post', + ); + return '$_temp0'; + } + + @override + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 players this week.', + one: '$count $param2 player this week.', + ); + return '$_temp0'; + } + + @override + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Available in $count languages!', + one: 'Available in $count language!', + ); + return '$_temp0'; + } + + @override + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seconds to play the first move', + one: '$count second to play the first move', + ); + return '$_temp0'; + } + + @override + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seconds', + one: '$count second', + ); + return '$_temp0'; + } + + @override + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'and save $count premove lines', + one: 'and save $count premove line', + ); + return '$_temp0'; + } @override - String get opacity => 'Opacity'; + String get stormMoveToStart => 'Move to start'; @override - String get brightness => 'Brightness'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'You play the white pieces in all puzzles'; @override - String get hue => 'Hue'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'You play the black pieces in all puzzles'; @override - String get boardReset => 'Reset colors to default'; + String get stormPuzzlesSolved => 'puzzles solved'; @override - String get pieceSet => 'Piece set'; + String get stormNewDailyHighscore => 'New daily highscore!'; @override - String get embedInYourWebsite => 'Embed in your website'; + String get stormNewWeeklyHighscore => 'New weekly highscore!'; @override - String get usernameAlreadyUsed => 'This username is already in use, please try another one.'; + String get stormNewMonthlyHighscore => 'New monthly highscore!'; @override - String get usernamePrefixInvalid => 'The username must start with a letter.'; + String get stormNewAllTimeHighscore => 'New all-time highscore!'; @override - String get usernameSuffixInvalid => 'The username must end with a letter or a number.'; + String stormPreviousHighscoreWasX(String param) { + return 'Previous highscore was $param'; + } @override - String get usernameCharsInvalid => 'The username must only contain letters, numbers, underscores, and hyphens. Consecutive underscores and hyphens are not allowed.'; + String get stormPlayAgain => 'Play again'; @override - String get usernameUnacceptable => 'This username is not acceptable.'; + String stormHighscoreX(String param) { + return 'Highscore: $param'; + } @override - String get playChessInStyle => 'Play chess in style'; + String get stormScore => 'Score'; @override - String get chessBasics => 'Chess basics'; + String get stormMoves => 'Moves'; @override - String get coaches => 'Coaches'; + String get stormAccuracy => 'Accuracy'; @override - String get invalidPgn => 'Invalid PGN'; + String get stormCombo => 'Combo'; @override - String get invalidFen => 'Invalid FEN'; + String get stormTime => 'Time'; @override - String get custom => 'Custom'; + String get stormTimePerMove => 'Time per move'; @override - String get notifications => 'Notifications'; + String get stormHighestSolved => 'Highest solved'; @override - String notificationsX(String param1) { - return 'Notifications: $param1'; - } + String get stormPuzzlesPlayed => 'Puzzles played'; @override - String perfRatingX(String param) { - return 'Rating: $param'; - } + String get stormNewRun => 'New run (hotkey: Space)'; @override - String get practiceWithComputer => 'Practice with computer'; + String get stormEndRun => 'End run (hotkey: Enter)'; @override - String anotherWasX(String param) { - return 'Another was $param'; - } + String get stormHighscores => 'Highscores'; @override - String bestWasX(String param) { - return 'Best was $param'; - } + String get stormViewBestRuns => 'View best runs'; @override - String get youBrowsedAway => 'You browsed away'; + String get stormBestRunOfDay => 'Best run of day'; @override - String get resumePractice => 'Resume practice'; + String get stormRuns => 'Runs'; @override - String get drawByFiftyMoves => 'The game has been drawn by the fifty-move rule.'; + String get stormGetReady => 'Get ready!'; @override - String get theGameIsADraw => 'The game is a draw.'; + String get stormWaitingForMorePlayers => 'Waiting for more players to join...'; @override - String get computerThinking => 'Computer thinking ...'; + String get stormRaceComplete => 'Race complete!'; @override - String get seeBestMove => 'See best move'; + String get stormSpectating => 'Spectating'; @override - String get hideBestMove => 'Hide the best move'; + String get stormJoinTheRace => 'Join the race!'; @override - String get getAHint => 'Get a hint'; + String get stormStartTheRace => 'Start the race'; @override - String get evaluatingYourMove => 'Evaluating your move ...'; + String stormYourRankX(String param) { + return 'Your rank: $param'; + } @override - String get whiteWinsGame => 'White wins'; + String get stormWaitForRematch => 'Wait for rematch'; @override - String get blackWinsGame => 'Black wins'; + String get stormNextRace => 'Next race'; @override - String get learnFromYourMistakes => 'Learn from your mistakes'; + String get stormJoinRematch => 'Join rematch'; @override - String get learnFromThisMistake => 'Learn from this mistake'; + String get stormWaitingToStart => 'Waiting to start'; @override - String get skipThisMove => 'Skip this move'; + String get stormCreateNewGame => 'Create a new game'; @override - String get next => 'Next'; + String get stormJoinPublicRace => 'Join a public race'; @override - String xWasPlayed(String param) { - return '$param was played'; - } + String get stormRaceYourFriends => 'Race your friends'; @override - String get findBetterMoveForWhite => 'Find a better move for white'; + String get stormSkip => 'skip'; @override - String get findBetterMoveForBlack => 'Find a better move for black'; + String get stormSkipHelp => 'You can skip one move per race:'; @override - String get resumeLearning => 'Resume learning'; + String get stormSkipExplanation => 'Skip this move to preserve your combo! Only works once per race.'; @override - String get youCanDoBetter => 'You can do better'; + String get stormFailedPuzzles => 'Failed puzzles'; @override - String get tryAnotherMoveForWhite => 'Try another move for white'; + String get stormSlowPuzzles => 'Slow puzzles'; @override - String get tryAnotherMoveForBlack => 'Try another move for black'; + String get stormSkippedPuzzle => 'Skipped puzzle'; @override - String get solution => 'Solution'; + String get stormThisWeek => 'This week'; @override - String get waitingForAnalysis => 'Waiting for analysis'; + String get stormThisMonth => 'This month'; @override - String get noMistakesFoundForWhite => 'No significant mistakes found for White'; + String get stormAllTime => 'All-time'; @override - String get noMistakesFoundForBlack => 'No significant mistakes found for Black'; + String get stormClickToReload => 'Click to reload'; @override - String get doneReviewingWhiteMistakes => 'Done reviewing White mistakes'; + String get stormThisRunHasExpired => 'This run has expired!'; @override - String get doneReviewingBlackMistakes => 'Done reviewing Black mistakes'; + String get stormThisRunWasOpenedInAnotherTab => 'This run was opened in another tab!'; @override - String get doItAgain => 'Do it again'; + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count runs', + one: '1 run', + ); + return '$_temp0'; + } @override - String get reviewWhiteMistakes => 'Review White mistakes'; + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count runs of $param2', + one: 'Played one run of $param2', + ); + return '$_temp0'; + } @override - String get reviewBlackMistakes => 'Review Black mistakes'; + String get streamerLichessStreamers => 'Lichess streamers'; @override - String get advantage => 'Advantage'; + String get studyPrivate => 'Private'; @override - String get opening => 'Opening'; + String get studyMyStudies => 'My studies'; @override - String get middlegame => 'Middlegame'; + String get studyStudiesIContributeTo => 'Studies I contribute to'; @override - String get endgame => 'Endgame'; + String get studyMyPublicStudies => 'My public studies'; @override - String get conditionalPremoves => 'Conditional premoves'; + String get studyMyPrivateStudies => 'My private studies'; @override - String get addCurrentVariation => 'Add current variation'; + String get studyMyFavoriteStudies => 'My favorite studies'; @override - String get playVariationToCreateConditionalPremoves => 'Play a variation to create conditional premoves'; + String get studyWhatAreStudies => 'What are studies?'; @override - String get noConditionalPremoves => 'No conditional premoves'; + String get studyAllStudies => 'All studies'; @override - String playX(String param) { - return 'Play $param'; + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; } @override - String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; - - @override - String get clickHereToReadIt => 'Click here to read it'; + String get studyNoneYet => 'None yet.'; @override - String get sorry => 'Sorry :('; + String get studyHot => 'Hot'; @override - String get weHadToTimeYouOutForAWhile => 'We had to time you out for a while.'; + String get studyDateAddedNewest => 'Date added (newest)'; @override - String get why => 'Why?'; + String get studyDateAddedOldest => 'Date added (oldest)'; @override - String get pleasantChessExperience => 'We aim to provide a pleasant chess experience for everyone.'; + String get studyRecentlyUpdated => 'Recently updated'; @override - String get goodPractice => 'To that effect, we must ensure that all players follow good practice.'; + String get studyMostPopular => 'Most popular'; @override - String get potentialProblem => 'When a potential problem is detected, we display this message.'; + String get studyAlphabetical => 'Alphabetical'; @override - String get howToAvoidThis => 'How to avoid this?'; + String get studyAddNewChapter => 'Add a new chapter'; @override - String get playEveryGame => 'Play every game you start.'; + String get studyAddMembers => 'Add members'; @override - String get tryToWin => 'Try to win (or at least draw) every game you play.'; + String get studyInviteToTheStudy => 'Invite to the study'; @override - String get resignLostGames => 'Resign lost games (don\'t let the clock run down).'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; @override - String get temporaryInconvenience => 'We apologize for the temporary inconvenience,'; + String get studySearchByUsername => 'Search by username'; @override - String get wishYouGreatGames => 'and wish you great games on lichess.org.'; + String get studySpectator => 'Spectator'; @override - String get thankYouForReading => 'Thank you for reading!'; + String get studyContributor => 'Contributor'; @override - String get lifetimeScore => 'Lifetime score'; + String get studyKick => 'Kick'; @override - String get currentMatchScore => 'Current match score'; + String get studyLeaveTheStudy => 'Leave the study'; @override - String get agreementAssistance => 'I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).'; + String get studyYouAreNowAContributor => 'You are now a contributor'; @override - String get agreementNice => 'I agree that I will always be nice to other players.'; + String get studyYouAreNowASpectator => 'You are now a spectator'; @override - String agreementMultipleAccounts(String param) { - return 'I agree that I will not create multiple accounts (except for the reasons stated in the $param).'; - } + String get studyPgnTags => 'PGN tags'; @override - String get agreementPolicy => 'I agree that I will follow all Lichess policies.'; + String get studyLike => 'Like'; @override - String get searchOrStartNewDiscussion => 'Search or start new discussion'; + String get studyUnlike => 'Unlike'; @override - String get edit => 'Edit'; + String get studyNewTag => 'New tag'; @override - String get bullet => 'Bullet'; + String get studyCommentThisPosition => 'Comment on this position'; @override - String get blitz => 'Blitz'; + String get studyCommentThisMove => 'Comment on this move'; @override - String get rapid => 'Rapid'; + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; @override - String get classical => 'Classical'; + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analyzed.'; @override - String get ultraBulletDesc => 'Insanely fast games: less than 30 seconds'; + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; @override - String get bulletDesc => 'Very fast games: less than 3 minutes'; + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; @override - String get blitzDesc => 'Fast games: 3 to 8 minutes'; + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; @override - String get rapidDesc => 'Rapid games: 8 to 25 minutes'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; @override - String get classicalDesc => 'Classical games: 25 minutes and more'; + String get studyShareChanges => 'Share changes with spectators and save them on the server'; @override - String get correspondenceDesc => 'Correspondence games: one or several days per move'; + String get studyPlaying => 'Playing'; @override - String get puzzleDesc => 'Chess tactics trainer'; + String get studyShowEvalBar => 'Evaluation gauge'; @override - String get important => 'Important'; + String get studyFirst => 'First'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return 'Your question may already have an answer $param1'; - } + String get studyPrevious => 'Previous'; @override - String get inTheFAQ => 'in the F.A.Q.'; + String get studyNext => 'Next'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'To report a user for cheating or bad behavior, $param1'; - } + String get studyLast => 'Last'; @override - String get useTheReportForm => 'use the report form'; + String get studyShareAndExport => 'Share & export'; @override - String toRequestSupport(String param1) { - return 'To request support, $param1'; - } + String get studyCloneStudy => 'Clone'; @override - String get tryTheContactPage => 'try the contact page'; + String get studyStudyPgn => 'Study PGN'; @override - String makeSureToRead(String param1) { - return 'Make sure to read $param1'; - } + String get studyDownloadAllGames => 'Download all games'; @override - String get theForumEtiquette => 'the forum etiquette'; + String get studyChapterPgn => 'Chapter PGN'; @override - String get thisTopicIsArchived => 'This topic has been archived and can no longer be replied to.'; + String get studyCopyChapterPgn => 'Copy PGN'; @override - String joinTheTeamXToPost(String param1) { - return 'Join the $param1, to post in this forum'; - } + String get studyDownloadGame => 'Download game'; @override - String teamNamedX(String param1) { - return '$param1 team'; - } + String get studyStudyUrl => 'Study URL'; @override - String get youCannotPostYetPlaySomeGames => 'You can\'t post in the forums yet. Play some games!'; + String get studyCurrentChapterUrl => 'Current chapter URL'; @override - String get subscribe => 'Subscribe'; + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; @override - String get unsubscribe => 'Unsubscribe'; + String get studyStartAtInitialPosition => 'Start at initial position'; @override - String mentionedYouInX(String param1) { - return 'mentioned you in \"$param1\".'; + String studyStartAtX(String param) { + return 'Start at $param'; } @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 mentioned you in \"$param2\".'; - } + String get studyEmbedInYourWebsite => 'Embed in your website'; @override - String invitedYouToX(String param1) { - return 'invited you to \"$param1\".'; - } + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 invited you to \"$param2\".'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; @override - String get youAreNowPartOfTeam => 'You are now part of the team.'; + String get studyOpen => 'Open'; @override - String youHaveJoinedTeamX(String param1) { - return 'You have joined \"$param1\".'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; } @override - String get someoneYouReportedWasBanned => 'Someone you reported was banned'; + String get studyStudyNotFound => 'Study not found'; @override - String get congratsYouWon => 'Congratulations, you won!'; + String get studyEditChapter => 'Edit chapter'; @override - String gameVsX(String param1) { - return 'Game vs $param1'; - } + String get studyNewChapter => 'New chapter'; @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; + String studyImportFromChapterX(String param) { + return 'Import from $param'; } @override - String get lostAgainstTOSViolator => 'You lost rating points to someone who violated the Lichess TOS'; + String get studyOrientation => 'Orientation'; @override - String refundXpointsTimeControlY(String param1, String param2) { - return 'Refund: $param1 $param2 rating points.'; - } + String get studyAnalysisMode => 'Analysis mode'; @override - String get timeAlmostUp => 'Time is almost up!'; + String get studyPinnedChapterComment => 'Pinned chapter comment'; @override - String get clickToRevealEmailAddress => '[Click to reveal email address.]'; + String get studySaveChapter => 'Save chapter'; @override - String get download => 'Download'; + String get studyClearAnnotations => 'Clear annotations'; @override - String get coachManager => 'Coach manager'; + String get studyClearVariations => 'Clear variations'; @override - String get streamerManager => 'Streamer manager'; + String get studyDeleteChapter => 'Delete chapter'; @override - String get cancelTournament => 'Cancel the tournament'; + String get studyDeleteThisChapter => 'Delete this chapter? There is no going back!'; @override - String get tournDescription => 'Tournament description'; + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter?'; @override - String get tournDescriptionHelp => 'Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)'; + String get studyRightUnderTheBoard => 'Right under the board'; @override - String get ratedFormHelp => 'Games are rated\nand impact players ratings'; + String get studyNoPinnedComment => 'None'; @override - String get onlyMembersOfTeam => 'Only members of team'; + String get studyNormalAnalysis => 'Normal analysis'; @override - String get noRestriction => 'No restriction'; + String get studyHideNextMoves => 'Hide next moves'; @override - String get minimumRatedGames => 'Minimum rated games'; + String get studyInteractiveLesson => 'Interactive lesson'; @override - String get minimumRating => 'Minimum rating'; + String studyChapterX(String param) { + return 'Chapter $param'; + } @override - String get maximumWeeklyRating => 'Maximum weekly rating'; + String get studyEmpty => 'Empty'; @override - String positionInputHelp(String param) { - return 'Paste a valid FEN to start every game from a given position.\nIt only works for standard games, not with variants.\nYou can use the $param to generate a FEN position, then paste it here.\nLeave empty to start games from the normal initial position.'; - } + String get studyStartFromInitialPosition => 'Start from initial position'; @override - String get cancelSimul => 'Cancel the simul'; + String get studyEditor => 'Editor'; @override - String get simulHostcolor => 'Host color for each game'; + String get studyStartFromCustomPosition => 'Start from custom position'; @override - String get estimatedStart => 'Estimated start time'; + String get studyLoadAGameByUrl => 'Load games by URL'; @override - String simulFeatured(String param) { - return 'Feature on $param'; - } + String get studyLoadAPositionFromFen => 'Load a position from FEN'; @override - String simulFeaturedHelp(String param) { - return 'Show your simul to everyone on $param. Disable for private simuls.'; - } + String get studyLoadAGameFromPgn => 'Load games from PGN'; @override - String get simulDescription => 'Simul description'; + String get studyAutomatic => 'Automatic'; @override - String get simulDescriptionHelp => 'Anything you want to tell the participants?'; + String get studyUrlOfTheGame => 'URL of the games, one per line'; @override - String markdownAvailable(String param) { - return '$param is available for more advanced syntax.'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; } @override - String get embedsAvailable => 'Paste a game URL or a study chapter URL to embed it.'; + String get studyCreateChapter => 'Create chapter'; @override - String get inYourLocalTimezone => 'In your own local timezone'; + String get studyCreateStudy => 'Create study'; @override - String get tournChat => 'Tournament chat'; + String get studyEditStudy => 'Edit study'; @override - String get noChat => 'No chat'; + String get studyVisibility => 'Visibility'; @override - String get onlyTeamLeaders => 'Only team leaders'; + String get studyPublic => 'Public'; @override - String get onlyTeamMembers => 'Only team members'; + String get studyUnlisted => 'Unlisted'; @override - String get navigateMoveTree => 'Navigate the move tree'; + String get studyInviteOnly => 'Invite only'; @override - String get mouseTricks => 'Mouse tricks'; + String get studyAllowCloning => 'Allow cloning'; @override - String get toggleLocalAnalysis => 'Toggle local computer analysis'; + String get studyNobody => 'Nobody'; @override - String get toggleAllAnalysis => 'Toggle all computer analysis'; + String get studyOnlyMe => 'Only me'; @override - String get playComputerMove => 'Play best computer move'; + String get studyContributors => 'Contributors'; @override - String get analysisOptions => 'Analysis options'; + String get studyMembers => 'Members'; @override - String get focusChat => 'Focus chat'; + String get studyEveryone => 'Everyone'; @override - String get showHelpDialog => 'Show this help dialog'; + String get studyEnableSync => 'Enable sync'; @override - String get reopenYourAccount => 'Reopen your account'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; @override - String get closedAccountChangedMind => 'If you closed your account, but have since changed your mind, you get one chance of getting your account back.'; + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; @override - String get onlyWorksOnce => 'This will only work once.'; + String get studyPinnedStudyComment => 'Pinned study comment'; @override - String get cantDoThisTwice => 'If you close your account a second time, there will be no way of recovering it.'; + String get studyStart => 'Start'; @override - String get emailAssociatedToaccount => 'Email address associated to the account'; + String get studySave => 'Save'; @override - String get sentEmailWithLink => 'We\'ve sent you an email with a link.'; + String get studyClearChat => 'Clear chat'; @override - String get tournamentEntryCode => 'Tournament entry code'; + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; @override - String get hangOn => 'Hang on!'; + String get studyDeleteStudy => 'Delete study'; @override - String gameInProgress(String param) { - return 'You have a game in progress with $param.'; + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; } @override - String get abortTheGame => 'Abort the game'; + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; @override - String get resignTheGame => 'Resign the game'; + String get studyGoodMove => 'Good move'; @override - String get youCantStartNewGame => 'You can\'t start a new game until this one is finished.'; + String get studyMistake => 'Mistake'; @override - String get since => 'Since'; + String get studyBrilliantMove => 'Brilliant move'; @override - String get until => 'Until'; + String get studyBlunder => 'Blunder'; @override - String get lichessDbExplanation => 'Rated games sampled from all Lichess players'; + String get studyInterestingMove => 'Interesting move'; @override - String get switchSides => 'Switch sides'; + String get studyDubiousMove => 'Dubious move'; @override - String get closingAccountWithdrawAppeal => 'Closing your account will withdraw your appeal'; + String get studyOnlyMove => 'Only move'; @override - String get ourEventTips => 'Our tips for organizing events'; + String get studyZugzwang => 'Zugzwang'; @override - String get instructions => 'Instructions'; + String get studyEqualPosition => 'Equal position'; @override - String get showMeEverything => 'Show me everything'; + String get studyUnclearPosition => 'Unclear position'; @override - String get lichessPatronInfo => 'Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.'; + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Your opponent left the game. You can claim victory in $count seconds.', - one: 'Your opponent left the game. You can claim victory in $count second.', - ); - return '$_temp0'; - } + String get studyWhiteIsBetter => 'White is better'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Mate in $count half-moves', - one: 'Mate in $count half-move', - ); - return '$_temp0'; - } + String get studyBlackIsBetter => 'Black is better'; @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count blunders', - one: '$count blunder', - ); - return '$_temp0'; - } + String get studyWhiteIsWinning => 'White is winning'; @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count mistakes', - one: '$count mistake', - ); - return '$_temp0'; - } + String get studyBlackIsWinning => 'Black is winning'; @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count inaccuracies', - one: '$count inaccuracy', - ); - return '$_temp0'; - } + String get studyNovelty => 'Novelty'; @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count players', - one: '$count player', - ); - return '$_temp0'; - } + String get studyDevelopment => 'Development'; @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count games', - one: '$count game', - ); - return '$_temp0'; - } + String get studyInitiative => 'Initiative'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count rating over $param2 games', - one: '$count rating over $param2 game', - ); - return '$_temp0'; - } + String get studyAttack => 'Attack'; @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count bookmarks', - one: '$count bookmark', - ); - return '$_temp0'; - } + String get studyCounterplay => 'Counterplay'; @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count days', - one: '$count day', - ); - return '$_temp0'; - } + String get studyTimeTrouble => 'Time trouble'; @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count hours', - one: '$count hour', - ); - return '$_temp0'; - } + String get studyWithCompensation => 'With compensation'; @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count minutes', - one: '$count minute', - ); - return '$_temp0'; - } + String get studyWithTheIdea => 'With the idea'; @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Rank is updated every $count minutes', - one: 'Rank is updated every minute', - ); - return '$_temp0'; - } + String get studyNextChapter => 'Next chapter'; @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count puzzles', - one: '$count puzzle', - ); - return '$_temp0'; - } + String get studyPrevChapter => 'Previous chapter'; @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count games with you', - one: '$count game with you', - ); - return '$_temp0'; - } + String get studyStudyActions => 'Study actions'; @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count rated', - one: '$count rated', - ); - return '$_temp0'; - } + String get studyTopics => 'Topics'; @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count wins', - one: '$count win', - ); - return '$_temp0'; - } + String get studyMyTopics => 'My topics'; @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count losses', - one: '$count loss', - ); - return '$_temp0'; - } + String get studyPopularTopics => 'Popular topics'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count draws', - one: '$count draw', - ); - return '$_temp0'; - } + String get studyManageTopics => 'Manage topics'; @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count playing', - one: '$count playing', - ); - return '$_temp0'; - } + String get studyBack => 'Back'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Give $count seconds', - one: 'Give $count second', - ); - return '$_temp0'; - } + String get studyPlayAgain => 'Play again'; - @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tournament points', - one: '$count tournament point', - ); - return '$_temp0'; - } + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; @override - String nbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count studies', - one: '$count study', - ); - return '$_temp0'; - } + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; @override - String nbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count simuls', - one: '$count simul', - ); - return '$_temp0'; + String studyPerPage(String param) { + return '$param per page'; } @override - String moreThanNbRatedGames(int count) { + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count rated games', - one: '≥ $count rated game', + other: '$count Chapters', + one: '$count Chapter', ); return '$_temp0'; } @override - String moreThanNbPerfRatedGames(int count, String param2) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count $param2 rated games', - one: '≥ $count $param2 rated game', + other: '$count Games', + one: '$count Game', ); return '$_temp0'; } @override - String needNbMorePerfGames(int count, String param2) { + String studyNbMembers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'You need to play $count more $param2 rated games', - one: 'You need to play $count more $param2 rated game', + other: '$count Members', + one: '$count Member', ); return '$_temp0'; } @override - String needNbMoreGames(int count) { + String studyPasteYourPgnTextHereUpToNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'You need to play $count more rated games', - one: 'You need to play $count more rated game', + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', ); return '$_temp0'; } @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count imported games', - one: '$count imported game', - ); - return '$_temp0'; - } + String get timeagoJustNow => 'just now'; @override - String nbFriendsOnline(int count) { + String get timeagoRightNow => 'right now'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count friends online', - one: '$count friend online', + other: 'in $count seconds', + one: 'in $count second', ); return '$_temp0'; } @override - String nbFollowers(int count) { + String timeagoInNbMinutes(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count followers', - one: '$count follower', + other: 'in $count minutes', + one: 'in $count minute', ); return '$_temp0'; } @override - String nbFollowing(int count) { + String timeagoInNbHours(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count following', - one: '$count following', + other: 'in $count hours', + one: 'in $count hour', ); return '$_temp0'; } @override - String lessThanNbMinutes(int count) { + String timeagoInNbDays(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Less than $count minutes', - one: 'Less than $count minute', + other: 'in $count days', + one: 'in $count day', ); return '$_temp0'; } @override - String nbGamesInPlay(int count) { + String timeagoInNbWeeks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count games in play', - one: '$count game in play', + other: 'in $count weeks', + one: 'in $count week', ); return '$_temp0'; } @override - String maximumNbCharacters(int count) { + String timeagoInNbMonths(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Maximum: $count characters.', - one: 'Maximum: $count character.', + other: 'in $count months', + one: 'in $count month', ); return '$_temp0'; } @override - String blocks(int count) { + String timeagoInNbYears(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count blocks', - one: '$count block', + other: 'in $count years', + one: 'in $count year', ); return '$_temp0'; } @override - String nbForumPosts(int count) { + String timeagoNbMinutesAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count forum posts', - one: '$count forum post', + other: '$count minutes ago', + one: '$count minute ago', ); return '$_temp0'; } @override - String nbPerfTypePlayersThisWeek(int count, String param2) { + String timeagoNbHoursAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count $param2 players this week.', - one: '$count $param2 player this week.', + other: '$count hours ago', + one: '$count hour ago', ); return '$_temp0'; } @override - String availableInNbLanguages(int count) { + String timeagoNbDaysAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Available in $count languages!', - one: 'Available in $count language!', + other: '$count days ago', + one: '$count day ago', ); return '$_temp0'; } @override - String nbSecondsToPlayTheFirstMove(int count) { + String timeagoNbWeeksAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count seconds to play the first move', - one: '$count second to play the first move', + other: '$count weeks ago', + one: '$count week ago', ); return '$_temp0'; } @override - String nbSeconds(int count) { + String timeagoNbMonthsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count seconds', - one: '$count second', + other: '$count months ago', + one: '$count month ago', ); return '$_temp0'; } @override - String andSaveNbPremoveLines(int count) { + String timeagoNbYearsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'and save $count premove lines', - one: 'and save $count premove line', + other: '$count years ago', + one: '$count year ago', ); return '$_temp0'; } @override - String get stormMoveToStart => 'Move to start'; - - @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'You play the white pieces in all puzzles'; - - @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'You play the black pieces in all puzzles'; - - @override - String get stormPuzzlesSolved => 'puzzles solved'; - - @override - String get stormNewDailyHighscore => 'New daily highscore!'; - - @override - String get stormNewWeeklyHighscore => 'New weekly highscore!'; - - @override - String get stormNewMonthlyHighscore => 'New monthly highscore!'; - - @override - String get stormNewAllTimeHighscore => 'New all-time highscore!'; - - @override - String stormPreviousHighscoreWasX(String param) { - return 'Previous highscore was $param'; - } - - @override - String get stormPlayAgain => 'Play again'; - - @override - String stormHighscoreX(String param) { - return 'Highscore: $param'; - } - - @override - String get stormScore => 'Score'; - - @override - String get stormMoves => 'Moves'; - - @override - String get stormAccuracy => 'Accuracy'; - - @override - String get stormCombo => 'Combo'; - - @override - String get stormTime => 'Time'; - - @override - String get stormTimePerMove => 'Time per move'; - - @override - String get stormHighestSolved => 'Highest solved'; - - @override - String get stormPuzzlesPlayed => 'Puzzles played'; - - @override - String get stormNewRun => 'New run (hotkey: Space)'; - - @override - String get stormEndRun => 'End run (hotkey: Enter)'; - - @override - String get stormHighscores => 'Highscores'; - - @override - String get stormViewBestRuns => 'View best runs'; - - @override - String get stormBestRunOfDay => 'Best run of day'; - - @override - String get stormRuns => 'Runs'; - - @override - String get stormGetReady => 'Get ready!'; - - @override - String get stormWaitingForMorePlayers => 'Waiting for more players to join...'; - - @override - String get stormRaceComplete => 'Race complete!'; - - @override - String get stormSpectating => 'Spectating'; - - @override - String get stormJoinTheRace => 'Join the race!'; - - @override - String get stormStartTheRace => 'Start the race'; - - @override - String stormYourRankX(String param) { - return 'Your rank: $param'; - } - - @override - String get stormWaitForRematch => 'Wait for rematch'; - - @override - String get stormNextRace => 'Next race'; - - @override - String get stormJoinRematch => 'Join rematch'; - - @override - String get stormWaitingToStart => 'Waiting to start'; - - @override - String get stormCreateNewGame => 'Create a new game'; - - @override - String get stormJoinPublicRace => 'Join a public race'; - - @override - String get stormRaceYourFriends => 'Race your friends'; - - @override - String get stormSkip => 'skip'; - - @override - String get stormSkipHelp => 'You can skip one move per race:'; - - @override - String get stormSkipExplanation => 'Skip this move to preserve your combo! Only works once per race.'; - - @override - String get stormFailedPuzzles => 'Failed puzzles'; - - @override - String get stormSlowPuzzles => 'Slow puzzles'; - - @override - String get stormSkippedPuzzle => 'Skipped puzzle'; - - @override - String get stormThisWeek => 'This week'; - - @override - String get stormThisMonth => 'This month'; - - @override - String get stormAllTime => 'All-time'; - - @override - String get stormClickToReload => 'Click to reload'; - - @override - String get stormThisRunHasExpired => 'This run has expired!'; - - @override - String get stormThisRunWasOpenedInAnotherTab => 'This run was opened in another tab!'; - - @override - String stormXRuns(int count) { + String timeagoNbMinutesRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count runs', - one: '1 run', + other: '$count minutes remaining', + one: '$count minute remaining', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String timeagoNbHoursRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Played $count runs of $param2', - one: 'Played one run of $param2', + other: '$count hours remaining', + one: '$count hour remaining', ); return '$_temp0'; } - - @override - String get streamerLichessStreamers => 'Lichess streamers'; - - @override - String get studyShareAndExport => 'Share & export'; - - @override - String get studyStart => 'Start'; } diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index e1f48e2589..fcb9e4cc78 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsEo extends AppLocalizations { AppLocalizationsEo([String locale = 'eo']) : super(locale); @override - String get mobileHomeTab => 'Hejmo'; + String get mobileAllGames => 'Ĉiuj ludoj'; @override - String get mobilePuzzlesTab => 'Puzloj'; + String get mobileAreYouSure => 'Ĉu vi certas?'; @override - String get mobileToolsTab => 'Iloj'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Spekti'; + String get mobileClearButton => 'Malplenigi'; @override - String get mobileSettingsTab => 'Agordoj'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'Vi devas esti ensalutata por spekti ĉi tiun paĝon.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'Sistemaj koloroj'; + String get mobileFeedbackButton => 'Prikomentado'; @override - String get mobileFeedbackButton => 'Prikomentado'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'Bone'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Tuŝ-retrokuplado'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Enakviĝa reĝimo'; + String get mobileHomeTab => 'Hejmo'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'Vi ne abonas ĉiun uzanton.'; + String get mobileMustBeLoggedIn => 'Vi devas esti ensalutata por spekti ĉi tiun paĝon.'; @override - String get mobileAllGames => 'Ĉiuj ludoj'; + String get mobileNoSearchResults => 'Neniu rezultoj'; @override - String get mobileRecentSearches => 'Lastaj serĉoj'; + String get mobileNotFollowingAnyUser => 'Vi ne abonas ĉiun uzanton.'; @override - String get mobileClearButton => 'Malplenigi'; + String get mobileOkButton => 'Bone'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsEo extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Neniu rezultoj'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Ĉu vi certas?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzloj'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Lastaj serĉoj'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Tuŝ-retrokuplado'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Enakviĝa reĝimo'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Agordoj'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Sistemaj koloroj'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Iloj'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Spekti'; @override String get activityActivity => 'Aktiveco'; @@ -246,6 +243,17 @@ class AppLocalizationsEo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEo extends AppLocalizations { @override String get broadcastBroadcasts => 'Elsendoj'; + @override + String get broadcastMyBroadcasts => 'Miaj elsendoj'; + @override String get broadcastLiveBroadcasts => 'Vivaj turniraj elsendoj'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nova viva elsendo'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonitaj elsendoj'; + + @override + String get broadcastAboutBroadcasts => 'Pri elsendoj'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kiel uzi Lichess Elsendojn.'; + + @override + String get broadcastTheNewRoundHelp => 'La nova raŭndo havos la samajn membrojn kaj kontribuantojn, kiom la antaŭa.'; + + @override + String get broadcastAddRound => 'Aldoni raŭndon'; + + @override + String get broadcastOngoing => 'Nun funkcianta'; + + @override + String get broadcastUpcoming => 'Baldaŭ'; + + @override + String get broadcastCompleted => 'Kompletigita'; + + @override + String get broadcastCompletedHelp => 'Lichess detektas raŭndan finiĝon baze sur la fontaj ludoj. Uzu ĉi tiun baskuligo, se ne estas fonto.'; + + @override + String get broadcastRoundName => 'Raŭndnomo'; + + @override + String get broadcastRoundNumber => 'Rondnumero'; + + @override + String get broadcastTournamentName => 'Nomo de la turniro'; + + @override + String get broadcastTournamentDescription => 'Mallonga turnira priskribo'; + + @override + String get broadcastFullDescription => 'Plena eventa priskribo'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Laŭvola longa priskribo de la elsendo. $param1 haveblas. Longeco devas esti malpli ol $param2 literoj.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL kiun Lichess kontrolos por akiri PGN ĝisdatigojn. Ĝi devas esti publike alirebla en interreto.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Laŭvola, se vi scias, kiam komenciĝas la evento'; + + @override + String get broadcastCurrentGameUrl => 'Nuna luda URL'; + + @override + String get broadcastDownloadAllRounds => 'Elŝuti ĉiujn raŭndojn'; + + @override + String get broadcastResetRound => 'Restarigi ĉi tiun raŭndon'; + + @override + String get broadcastDeleteRound => 'Forigi ĉi tiun raŭndon'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Sendube forigi la raŭndon kaj ĉiujn ĝiajn ludojn.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Forigi ĉiujn ludojn de ĉi tiu raŭndo. La fonto devos esti aktiva por rekrei ilin.'; + + @override + String get broadcastEditRoundStudy => 'Redakti raŭndan studon'; + + @override + String get broadcastDeleteTournament => 'Forigi ĉi tiun turniron'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Sendube forigi la tuta turniro, kaj ĝiajn raŭndojn kaj ĉiujn ĝiajn ludojn.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count elsendoj', + one: '$count elsendo', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Defioj: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur en ludo'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ŝakhorloĝo'; @@ -750,6 +1008,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Sonorile sciiga sono'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Puzloj'; @@ -1390,10 +1651,10 @@ class AppLocalizationsEo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'La opcioj de la kontraŭulo por moviĝi estas limigitaj, kaj ĉiuj movoj plimalbonigas rian pozicion.'; @override - String get puzzleThemeHealthyMix => 'Sana miksaĵo'; + String get puzzleThemeMix => 'Sana miksaĵo'; @override - String get puzzleThemeHealthyMixDescription => 'Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.'; + String get puzzleThemeMixDescription => 'Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.'; @override String get puzzleThemePlayerGames => 'Ludantaj ludoj'; @@ -1767,9 +2028,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get byCPL => 'Per eraroj'; - @override - String get openStudy => 'Malfermi analizon'; - @override String get enable => 'Ebligi'; @@ -1797,9 +2055,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get removesTheDepthLimit => 'Forigas la profundlimon, kaj tenas vian komputilon varma'; - @override - String get engineManager => 'Motora administranto'; - @override String get blunder => 'Erarego'; @@ -2063,6 +2318,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get gamesPlayed => 'Luditaj ludoj'; + @override + String get ok => 'OK'; + @override String get cancel => 'Nuligi'; @@ -2437,9 +2695,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get unblock => 'Malbloki'; - @override - String get followsYou => 'Sekvas vin'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 eksekvis $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsEo extends AppLocalizations { String get other => 'Io alia'; @override - String get reportDescriptionHelp => 'Inkluzivu la ligilon al la ludo(j) kaj ekspliku tion, kio malbonas pri la konduto de ĉi tiu uzanto.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Bonvolu doni almenaŭ unu ligilon al ludo en kiu oni friponis.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get nothingToSeeHere => 'Nenio videbla ĉi tie nuntempe.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsEo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess filmprezentistoj'; + @override + String get studyPrivate => 'Privata'; + + @override + String get studyMyStudies => 'Miaj studoj'; + + @override + String get studyStudiesIContributeTo => 'Studoj en kiuj mi kontribuas'; + + @override + String get studyMyPublicStudies => 'Miaj publikaj studoj'; + + @override + String get studyMyPrivateStudies => 'Miaj privataj studoj'; + + @override + String get studyMyFavoriteStudies => 'Miaj preferataj studoj'; + + @override + String get studyWhatAreStudies => 'Kio estas la studoj?'; + + @override + String get studyAllStudies => 'Ĉiuj studoj'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studoj kreitaj de $param'; + } + + @override + String get studyNoneYet => 'Neniu ankoraŭ.'; + + @override + String get studyHot => 'Tendenca'; + + @override + String get studyDateAddedNewest => 'Dato aldonita (plej novaj)'; + + @override + String get studyDateAddedOldest => 'Dato aldonita (plej malnovaj)'; + + @override + String get studyRecentlyUpdated => 'Lastatempe ĝisdatigita'; + + @override + String get studyMostPopular => 'Plej popularaj'; + + @override + String get studyAlphabetical => 'Alfabete'; + + @override + String get studyAddNewChapter => 'Aldoni novan ĉapitron'; + + @override + String get studyAddMembers => 'Aldoni membrojn'; + + @override + String get studyInviteToTheStudy => 'Inviti al la studo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bonvolu inviti nur homojn, kiujn vi konas kaj kiuj aktive volas aliĝi al tiu ĉi studo.'; + + @override + String get studySearchByUsername => 'Serĉi laŭ uzantnomo'; + + @override + String get studySpectator => 'Spektanto'; + + @override + String get studyContributor => 'Kontribuanto'; + + @override + String get studyKick => 'Forpuŝi'; + + @override + String get studyLeaveTheStudy => 'Forlasi la studon'; + + @override + String get studyYouAreNowAContributor => 'Nun vi estas kunlaboranto'; + + @override + String get studyYouAreNowASpectator => 'Nun vi estas spektanto'; + + @override + String get studyPgnTags => 'PGN etikedoj'; + + @override + String get studyLike => 'Ŝati'; + + @override + String get studyUnlike => 'Malŝati'; + + @override + String get studyNewTag => 'Nova etikedo'; + + @override + String get studyCommentThisPosition => 'Komenti tiun posicion'; + + @override + String get studyCommentThisMove => 'Komenti tiun movon'; + + @override + String get studyAnnotateWithGlyphs => 'Komenti per signobildo'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'La ĉapitro estas tro mallonga por esti analizita.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur la kontribuantoj de la studo povas peti komputilan analizon.'; + + @override + String get studyGetAFullComputerAnalysis => 'Akiru kompletan servilan komputilan analizon de la ĉefa linio.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Certiĝu, ke la ĉapitro estas kompleta. Vi nur povas peti analizon unu foje.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Ĉiuj sinkronigitaj membroj restas ĉe la sama pozicio'; + + @override + String get studyShareChanges => 'Diskonigi ŝanĝojn al spektantoj kaj konservi tiujn ĉe la servilo'; + + @override + String get studyPlaying => 'Ludanta'; + + @override + String get studyShowEvalBar => 'Taksaj stangoj'; + + @override + String get studyFirst => 'Al la unua'; + + @override + String get studyPrevious => 'Antaŭa'; + + @override + String get studyNext => 'Sekva'; + + @override + String get studyLast => 'Al la lasta'; + @override String get studyShareAndExport => 'Konigi & eksporti'; + @override + String get studyCloneStudy => 'Kloni'; + + @override + String get studyStudyPgn => 'PGN de la studo'; + + @override + String get studyDownloadAllGames => 'Elŝuti ĉiujn ludojn'; + + @override + String get studyChapterPgn => 'PGN de la ĉapitro'; + + @override + String get studyCopyChapterPgn => 'Kopii PGN'; + + @override + String get studyDownloadGame => 'Elŝuti ludon'; + + @override + String get studyStudyUrl => 'URL de la studo'; + + @override + String get studyCurrentChapterUrl => 'URL de tiu ĉi ĉapitro'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vi povas alglui ĉi tiun en la forumo aŭ via Lichess blogo por enkorpigi'; + + @override + String get studyStartAtInitialPosition => 'Starti ekde komenca pozicio'; + + @override + String studyStartAtX(String param) { + return 'Komenci je $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Enkorpigi en via retejo'; + + @override + String get studyReadMoreAboutEmbedding => 'Legi pli pri enkorpigo'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Nur publikaj studoj eblas enkorpiĝi!'; + + @override + String get studyOpen => 'Malfermi'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, provizia al vi de $param2'; + } + + @override + String get studyStudyNotFound => 'Studo ne trovita'; + + @override + String get studyEditChapter => 'Redakti ĉapitron'; + + @override + String get studyNewChapter => 'Nova ĉapitro'; + + @override + String studyImportFromChapterX(String param) { + return 'Importi el $param'; + } + + @override + String get studyOrientation => 'Orientiĝo'; + + @override + String get studyAnalysisMode => 'Analiza modo'; + + @override + String get studyPinnedChapterComment => 'Alpinglita ĉapitra komento'; + + @override + String get studySaveChapter => 'Konservi ĉapitron'; + + @override + String get studyClearAnnotations => 'Forigi notojn'; + + @override + String get studyClearVariations => 'Forigi variaĵojn'; + + @override + String get studyDeleteChapter => 'Forigi ĉapitron'; + + @override + String get studyDeleteThisChapter => 'Ĉu forigi ĉi tiun ĉapitron? Tiun agon vi ne povos malfari!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Forigi ĉiujn komentojn, signobildoj, kaj skribintaj formoj en ĉi tiu ĉapitro'; + + @override + String get studyRightUnderTheBoard => 'Ĝuste sub la tabulo'; + + @override + String get studyNoPinnedComment => 'Neniu'; + + @override + String get studyNormalAnalysis => 'Normala analizo'; + + @override + String get studyHideNextMoves => 'Kaŝi la sekvajn movojn'; + + @override + String get studyInteractiveLesson => 'Interaga leciono'; + + @override + String studyChapterX(String param) { + return 'Ĉapitro $param'; + } + + @override + String get studyEmpty => 'Malplena'; + + @override + String get studyStartFromInitialPosition => 'Starti el la komenca pozicio'; + + @override + String get studyEditor => 'Redaktanto'; + + @override + String get studyStartFromCustomPosition => 'Starti el propra pozicio'; + + @override + String get studyLoadAGameByUrl => 'Ŝarĝi ludon el URL'; + + @override + String get studyLoadAPositionFromFen => 'Ŝarĝi posicion el FEN kodo'; + + @override + String get studyLoadAGameFromPgn => 'Ŝarĝi ludon el PGN'; + + @override + String get studyAutomatic => 'Aŭtomata'; + + @override + String get studyUrlOfTheGame => 'URL de la ludo'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ŝarĝu ludon el $param1 aŭ $param2'; + } + + @override + String get studyCreateChapter => 'Krei ĉapitron'; + + @override + String get studyCreateStudy => 'Krei studon'; + + @override + String get studyEditStudy => 'Redakti studon'; + + @override + String get studyVisibility => 'Videbleco'; + + @override + String get studyPublic => 'Publika'; + + @override + String get studyUnlisted => 'Nelistigita'; + + @override + String get studyInviteOnly => 'Per invito'; + + @override + String get studyAllowCloning => 'Permesi klonadon'; + + @override + String get studyNobody => 'Neniu'; + + @override + String get studyOnlyMe => 'Nur mi'; + + @override + String get studyContributors => 'Kontribuantoj'; + + @override + String get studyMembers => 'Membroj'; + + @override + String get studyEveryone => 'Ĉiuj'; + + @override + String get studyEnableSync => 'Ebligi sinkronigon'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jes: ĉiuj vidas la saman pozicion'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: lasu homojn esplori libere'; + + @override + String get studyPinnedStudyComment => 'Komento alpinglita al la studo'; + @override String get studyStart => 'Komenci'; + + @override + String get studySave => 'Konservi'; + + @override + String get studyClearChat => 'Vakigi babiladon'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ĉu forigi la historian babilejon de la ĉapitro? Tiun agon vi ne povos malfari!'; + + @override + String get studyDeleteStudy => 'Forigi studon'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ĉu forigi la tuta studo? Ne estas reiro! Tajpi la nomon de la studo por konfirmi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kie vi volas studi tion?'; + + @override + String get studyGoodMove => 'Bona movo'; + + @override + String get studyMistake => 'Eraro'; + + @override + String get studyBrilliantMove => 'Brilianta movo'; + + @override + String get studyBlunder => 'Erarego'; + + @override + String get studyInterestingMove => 'Interesa movo'; + + @override + String get studyDubiousMove => 'Dubinda movo'; + + @override + String get studyOnlyMove => 'Nura movo'; + + @override + String get studyZugzwang => 'Movdevigo'; + + @override + String get studyEqualPosition => 'Egala aranĝo'; + + @override + String get studyUnclearPosition => 'Malklara aranĝo'; + + @override + String get studyWhiteIsSlightlyBetter => 'Blanko estas iomete pli bona'; + + @override + String get studyBlackIsSlightlyBetter => 'Nigro estas iomete pli bona'; + + @override + String get studyWhiteIsBetter => 'Blanko estas pli bona'; + + @override + String get studyBlackIsBetter => 'Nigro estas pli bona'; + + @override + String get studyWhiteIsWinning => 'Blanko estas gajnanta'; + + @override + String get studyBlackIsWinning => 'Nigro estas gajnanta'; + + @override + String get studyNovelty => 'Novaĵo'; + + @override + String get studyDevelopment => 'Programado'; + + @override + String get studyInitiative => 'Iniciato'; + + @override + String get studyAttack => 'Atako'; + + @override + String get studyCounterplay => 'Kontraŭludo'; + + @override + String get studyTimeTrouble => 'Tempa ĝeno'; + + @override + String get studyWithCompensation => 'Kun kompenso'; + + @override + String get studyWithTheIdea => 'Kun la ideo'; + + @override + String get studyNextChapter => 'Sekva ĉapitro'; + + @override + String get studyPrevChapter => 'Antaŭa ĉapitro'; + + @override + String get studyStudyActions => 'Studaj agoj'; + + @override + String get studyTopics => 'Temoj'; + + @override + String get studyMyTopics => 'Miaj temoj'; + + @override + String get studyPopularTopics => 'Popularaj temoj'; + + @override + String get studyManageTopics => 'Administri temojn'; + + @override + String get studyBack => 'Reen'; + + @override + String get studyPlayAgain => 'Reludi'; + + @override + String get studyWhatWouldYouPlay => 'Kion vi ludus en ĉi tiu pozicio?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulon! Vi kompletigis la lecionon.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ĉapitroj', + one: '$count Ĉapitro', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ludoj', + one: '$count Ludo', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membroj', + one: '$count Membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Algluu ĉi tie vian PGN kodon, ĝis maksimume $count ludoj', + one: 'Algluu ĉi tie vian PGN kodon, maksimume ĝis $count ludo', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'ĵus nun'; + + @override + String get timeagoRightNow => 'ĵuse'; + + @override + String get timeagoCompleted => 'finita'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count sekundoj', + one: 'en $count sekundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutoj', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horoj', + one: 'en $count horo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count tagoj', + one: 'en $count tago', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semajnoj', + one: 'en $count semajno', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count monatoj', + one: 'en $count monato', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count jaroj', + one: 'en $count jaro', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count minutoj', + one: 'antaŭ $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count horoj', + one: 'antaŭ $count horo', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count tagoj', + one: 'antaŭ $count tago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count semajnoj', + one: 'antaŭ $count semajno', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count monatoj', + one: 'antaŭ $count monato', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count jaroj', + one: 'antaŭ $count jaro', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutoj restas', + one: '$count minuto restas', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horoj restas', + one: '$count horo restas', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 7f65a608d6..e8dd5f79ea 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsEs extends AppLocalizations { AppLocalizationsEs([String locale = 'es']) : super(locale); @override - String get mobileHomeTab => 'Inicio'; + String get mobileAllGames => 'Todas las partidas'; @override - String get mobilePuzzlesTab => 'Ejercicios'; + String get mobileAreYouSure => '¿Estás seguro?'; @override - String get mobileToolsTab => 'Herramientas'; + String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; @override - String get mobileWatchTab => 'Ver'; + String get mobileClearButton => 'Limpiar'; @override - String get mobileSettingsTab => 'Ajustes'; + String get mobileCorrespondenceClearSavedMove => 'Borrar movimiento guardado'; @override - String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta página.'; + String get mobileCustomGameJoinAGame => 'Únete a una partida'; @override - String get mobileSystemColors => 'Colores del sistema'; + String get mobileFeedbackButton => 'Comentarios'; @override - String get mobileFeedbackButton => 'Comentarios'; + String mobileGreeting(String param) { + return 'Hola $param'; + } @override - String get mobileOkButton => 'Aceptar'; + String get mobileGreetingWithoutName => 'Hola'; @override - String get mobileSettingsHapticFeedback => 'Respuesta táctil'; + String get mobileHideVariation => 'Ocultar variación'; @override - String get mobileSettingsImmersiveMode => 'Modo inmersivo'; + String get mobileHomeTab => 'Inicio'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.'; + String get mobileLiveStreamers => 'Presentadores en vivo'; @override - String get mobileNotFollowingAnyUser => 'No estás siguiendo a ningún usuario.'; + String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta página.'; @override - String get mobileAllGames => 'Todas las partidas'; + String get mobileNoSearchResults => 'No hay resultados'; @override - String get mobileRecentSearches => 'Búsquedas recientes'; + String get mobileNotFollowingAnyUser => 'No estás siguiendo a ningún usuario.'; @override - String get mobileClearButton => 'Limpiar'; + String get mobileOkButton => 'Aceptar'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No hay resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Aumentar la pieza arrastrada'; @override - String get mobileAreYouSure => '¿Estás seguro?'; + String get mobilePuzzleStormConfirmEndRun => '¿Quieres finalizar esta ronda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderás tu racha actual y tu puntuación será guardada.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada que mostrar, por favor cambia los filtros'; @override String get mobilePuzzleStormNothingToShow => 'Nada que mostrar. Juega algunas rondas de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Compartir este ejercicio'; + String get mobilePuzzleStormSubtitle => 'Resuelve tantos ejercicios como puedas en 3 minutos.'; @override - String get mobileShareGameURL => 'Compartir enlace de la partida'; + String get mobilePuzzleStreakAbortWarning => 'Perderás tu racha actual y tu puntuación será guardada.'; @override - String get mobileShareGamePGN => 'Compartir PGN'; + String get mobilePuzzleThemesSubtitle => 'Realiza ejercicios de tus aperturas favoritas o elige un tema.'; @override - String get mobileSharePositionAsFEN => 'Compartir posición como FEN'; + String get mobilePuzzlesTab => 'Ejercicios'; @override - String get mobileShowVariations => 'Mostrar variaciones'; + String get mobileRecentSearches => 'Búsquedas recientes'; @override - String get mobileHideVariation => 'Ocultar variación'; + String get mobileSettingsHapticFeedback => 'Respuesta táctil'; @override - String get mobileShowComments => 'Mostrar comentarios'; + String get mobileSettingsImmersiveMode => 'Modo inmersivo'; @override - String get mobilePuzzleStormConfirmEndRun => '¿Quieres finalizar esta ronda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada que mostrar, por favor cambia los filtros'; + String get mobileSettingsTab => 'Ajustes'; @override - String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; + String get mobileShareGamePGN => 'Compartir PGN'; @override - String get mobileCancelDrawOffer => 'Cancelar ofertas de tablas'; + String get mobileShareGameURL => 'Compartir enlace de la partida'; @override - String get mobileWaitingForOpponentToJoin => 'Esperando a que se una un oponente...'; + String get mobileSharePositionAsFEN => 'Compartir posición como FEN'; @override - String get mobileBlindfoldMode => 'A ciegas'; + String get mobileSharePuzzle => 'Compartir este ejercicio'; @override - String get mobileLiveStreamers => 'Presentadores en vivo'; + String get mobileShowComments => 'Mostrar comentarios'; @override - String get mobileCustomGameJoinAGame => 'Únete a una partida'; + String get mobileShowResult => 'Ver resultado'; @override - String get mobileCorrespondenceClearSavedMove => 'Borrar movimiento guardado'; + String get mobileShowVariations => 'Mostrar variaciones'; @override String get mobileSomethingWentWrong => 'Algo salió mal.'; @override - String get mobileShowResult => 'Ver resultado'; - - @override - String get mobilePuzzleThemesSubtitle => 'Realiza ejercicios de tus aperturas favoritas o elige un tema.'; + String get mobileSystemColors => 'Colores del sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resuelve tantos ejercicios como puedas en 3 minutos.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hola $param'; - } + String get mobileToolsTab => 'Herramientas'; @override - String get mobileGreetingWithoutName => 'Hola'; + String get mobileWaitingForOpponentToJoin => 'Esperando a que se una un oponente...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Ver'; @override String get activityActivity => 'Actividad'; @@ -246,6 +243,17 @@ class AppLocalizationsEs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ha completado $count $param2 partidas por correspondencia', + one: 'Ha completado $count $param2 partida por correspondencia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEs extends AppLocalizations { @override String get broadcastBroadcasts => 'Emisiones'; + @override + String get broadcastMyBroadcasts => 'Mis transmisiones'; + @override String get broadcastLiveBroadcasts => 'Emisiones de torneos en directo'; + @override + String get broadcastBroadcastCalendar => 'Calendario de transmisiones'; + + @override + String get broadcastNewBroadcast => 'Nueva emisión en directo'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmisiones suscritas'; + + @override + String get broadcastAboutBroadcasts => 'Acerca de las transmisiones'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como utilizar las transmisiones de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nueva ronda tendrá los mismos miembros y contribuyentes que la anterior.'; + + @override + String get broadcastAddRound => 'Añadir una ronda'; + + @override + String get broadcastOngoing => 'En curso'; + + @override + String get broadcastUpcoming => 'Próximamente'; + + @override + String get broadcastCompleted => 'Completadas'; + + @override + String get broadcastCompletedHelp => 'Lichess detecta la terminación de la ronda según las partidas de origen. Usa este interruptor si no hay ninguna.'; + + @override + String get broadcastRoundName => 'Nombre de la ronda'; + + @override + String get broadcastRoundNumber => 'Número de ronda'; + + @override + String get broadcastTournamentName => 'Nombre del torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descripción del torneo'; + + @override + String get broadcastFullDescription => 'Descripción completa del evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descripción larga opcional de la emisión. $param1 está disponible. La longitud debe ser inferior a $param2 caracteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL origen del archivo PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess comprobará para obtener actualizaciones PGN. Debe ser públicamente accesible desde Internet.'; + + @override + String get broadcastSourceGameIds => 'Hasta 64 identificadores de partidas de Lichess, separados por espacios.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Fecha de inicio en la zona horaria local del torneo: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, si sabes cuando comienza el evento'; + + @override + String get broadcastCurrentGameUrl => 'Enlace de la partida actual'; + + @override + String get broadcastDownloadAllRounds => 'Descargar todas las rondas'; + + @override + String get broadcastResetRound => 'Restablecer esta ronda'; + + @override + String get broadcastDeleteRound => 'Eliminar esta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente la ronda y sus partidas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todas las partidas de esta ronda. La fuente tendrá que estar activa para volver a crearlos.'; + + @override + String get broadcastEditRoundStudy => 'Editar estudio de ronda'; + + @override + String get broadcastDeleteTournament => 'Elimina este torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina definitivamente todo el torneo, rondas y partidas incluidas.'; + + @override + String get broadcastShowScores => 'Mostrar las puntuaciones de los jugadores según los resultados de las partidas'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: reemplazar nombres de jugadores, puntuaciones y títulos'; + + @override + String get broadcastFideFederations => 'Federaciones FIDE'; + + @override + String get broadcastTop10Rating => 'Los 10 mejores'; + + @override + String get broadcastFidePlayers => 'Jugadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jugador FIDE no encontrado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federación'; + + @override + String get broadcastAgeThisYear => 'Edad actual'; + + @override + String get broadcastUnrated => 'Sin puntuación'; + + @override + String get broadcastRecentTournaments => 'Torneos recientes'; + + @override + String get broadcastOpenLichess => 'Abrir en Lichess'; + + @override + String get broadcastTeams => 'Equipos'; + + @override + String get broadcastBoards => 'Tableros'; + + @override + String get broadcastOverview => 'Resumen'; + + @override + String get broadcastSubscribeTitle => 'Suscríbete para ser notificado cuando comience cada ronda. Puedes alternar entre notificaciones de campana o de dispositivo para emisiones en las preferencias de tu cuenta.'; + + @override + String get broadcastUploadImage => 'Subir imagen del torneo'; + + @override + String get broadcastNoBoardsYet => 'Aún no hay tableros. Estos aparecerán una vez se suban las partidas.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Los tableros pueden cargarse gracias a una fuente o a través de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Comienza en $param'; + } + + @override + String get broadcastStartVerySoon => 'La transmisión comenzará muy pronto.'; + + @override + String get broadcastNotYetStarted => 'La transmisión aún no ha comenzado.'; + + @override + String get broadcastOfficialWebsite => 'Sitio oficial'; + + @override + String get broadcastStandings => 'Clasificación'; + + @override + String get broadcastOfficialStandings => 'Clasificación oficial'; + + @override + String broadcastIframeHelp(String param) { + return 'Más opciones en $param'; + } + + @override + String get broadcastWebmastersPage => 'la página del webmaster'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Una fuente PGN pública en tiempo real para esta ronda. También ofrecemos $param para una sincronización más rápida y eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Inserta esta transmisión en tu sitio web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Inserta la $param en tu sitio web'; + } + + @override + String get broadcastRatingDiff => 'Diferencia de valoración'; + + @override + String get broadcastGamesThisTournament => 'Partidas en este torneo'; + + @override + String get broadcastScore => 'Resultado'; + + @override + String get broadcastAllTeams => 'Todos los equipos'; + + @override + String get broadcastTournamentFormat => 'Formato del torneo'; + + @override + String get broadcastTournamentLocation => 'Ubicación del torneo'; + + @override + String get broadcastTopPlayers => 'Mejores jugadores'; + + @override + String get broadcastTimezone => 'Zona horaria'; + + @override + String get broadcastFideRatingCategory => 'Categoría de calificación de FIDE'; + + @override + String get broadcastOptionalDetails => 'Detalles opcionales'; + + @override + String get broadcastPastBroadcasts => 'Transmisiones pasadas'; + + @override + String get broadcastAllBroadcastsByMonth => 'Ver todas las transmisiones por mes'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count retransmisiones', + one: '$count retransmisión', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafíos: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get preferencesInGameOnly => 'Solo durante la partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Reloj de ajedrez'; @@ -750,6 +1008,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Campana de notificación'; + @override + String get preferencesBlindfold => 'A ciegas'; + @override String get puzzlePuzzles => 'Ejercicios'; @@ -1390,10 +1651,10 @@ class AppLocalizationsEs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'El oponente está limitado en los movimientos que puede realizar, y todos los movimientos empeoran su posición.'; @override - String get puzzleThemeHealthyMix => 'Mezcla equilibrada'; + String get puzzleThemeMix => 'Mezcla equilibrada'; @override - String get puzzleThemeHealthyMixDescription => 'Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.'; + String get puzzleThemeMixDescription => 'Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.'; @override String get puzzleThemePlayerGames => 'Partidas de jugadores'; @@ -1767,9 +2028,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get byCPL => 'Por PCP'; - @override - String get openStudy => 'Abrir estudio'; - @override String get enable => 'Activar'; @@ -1797,9 +2055,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimina el límite de profundidad del análisis y hace trabajar a tu ordenador'; - @override - String get engineManager => 'Gestor de motores'; - @override String get blunder => 'Error grave'; @@ -2063,6 +2318,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get gamesPlayed => 'Partidas jugadas'; + @override + String get ok => 'Aceptar'; + @override String get cancel => 'Cancelar'; @@ -2437,9 +2695,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Te sigue'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 comenzó a seguir a $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsEs extends AppLocalizations { String get other => 'Otro'; @override - String get reportDescriptionHelp => 'Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\"; explícanos cómo has llegado a esta conclusión. Tu informe será procesado más rápido si está escrito en inglés.'; + String get reportCheatBoostHelp => 'Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\", sino cómo has llegado a esta conclusión.'; + + @override + String get reportUsernameHelp => 'Explica qué es lo que te resulta ofensivo de este nombre de usuario. No digas solo \"es ofensivo\" o \"inapropiado\", sino cómo llegaste a esta conclusión, sobre todo si el insulto no es tan obvio, no está en inglés, es jerga o una referencia histórica o cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'Tu informe será procesado más rápido si está escrito en inglés.'; @override String get error_provideOneCheatedGameLink => 'Por favor, proporciona al menos un enlace a una partida en la que se hicieron trampas.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get nothingToSeeHere => 'Nada que ver aquí por ahora.'; + @override + String get stats => 'Estadísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4160,7 +4424,7 @@ class AppLocalizationsEs extends AppLocalizations { count, locale: localeName, other: 'Puntuación de $count en $param2 partidas', - one: 'puntuación $count en $param2 partida', + one: 'Puntuación $count en $param2 partida', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsEs extends AppLocalizations { @override String get streamerLichessStreamers => 'Presentadores de Lichess'; + @override + String get studyPrivate => 'Privado'; + + @override + String get studyMyStudies => 'Mis estudios'; + + @override + String get studyStudiesIContributeTo => 'Estudios en los que colaboro'; + + @override + String get studyMyPublicStudies => 'Mis estudios públicos'; + + @override + String get studyMyPrivateStudies => 'Mis estudios privados'; + + @override + String get studyMyFavoriteStudies => 'Mis estudios favoritos'; + + @override + String get studyWhatAreStudies => '¿Qué son los estudios?'; + + @override + String get studyAllStudies => 'Todos los estudios'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudios creados por $param'; + } + + @override + String get studyNoneYet => 'Ninguno por ahora.'; + + @override + String get studyHot => 'De interés actualmente'; + + @override + String get studyDateAddedNewest => 'Fecha (más recientes)'; + + @override + String get studyDateAddedOldest => 'Fecha (más antiguos)'; + + @override + String get studyRecentlyUpdated => 'Actualizados recientemente'; + + @override + String get studyMostPopular => 'Más populares'; + + @override + String get studyAlphabetical => 'Alfabético'; + + @override + String get studyAddNewChapter => 'Añadir nuevo capítulo'; + + @override + String get studyAddMembers => 'Añadir miembros'; + + @override + String get studyInviteToTheStudy => 'Invitar al estudio'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, invita sólo a personas que conozcas y que deseen unirse a este estudio.'; + + @override + String get studySearchByUsername => 'Buscar por nombre de usuario'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Colaborador'; + + @override + String get studyKick => 'Expulsar'; + + @override + String get studyLeaveTheStudy => 'Dejar el estudio'; + + @override + String get studyYouAreNowAContributor => 'Ahora eres un colaborador'; + + @override + String get studyYouAreNowASpectator => 'Ahora eres un espectador'; + + @override + String get studyPgnTags => 'Etiquetas PGN'; + + @override + String get studyLike => 'Me gusta'; + + @override + String get studyUnlike => 'No me gusta'; + + @override + String get studyNewTag => 'Nueva etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar esta posición'; + + @override + String get studyCommentThisMove => 'Comentar este movimiento'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar con iconos'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'El capítulo es demasiado corto para analizarlo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Sólo los colaboradores del estudio pueden solicitar un análisis por ordenador.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtén un análisis completo de la línea principal en el servidor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asegúrate de que el capítulo está completo. Sólo puede solicitar el análisis una vez.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos los miembros de SYNC permanecen en la misma posición'; + + @override + String get studyShareChanges => 'Comparte cambios con los espectadores y guárdalos en el servidor'; + + @override + String get studyPlaying => 'Jugando'; + + @override + String get studyShowEvalBar => 'Barras de evaluación'; + + @override + String get studyFirst => 'Primero'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Siguiente'; + + @override + String get studyLast => 'Último'; + @override String get studyShareAndExport => 'Compartir y exportar'; + @override + String get studyCloneStudy => 'Clonar'; + + @override + String get studyStudyPgn => 'PGN del estudio'; + + @override + String get studyDownloadAllGames => 'Descargar todas las partidas'; + + @override + String get studyChapterPgn => 'PGN del capítulo'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descargar partida'; + + @override + String get studyStudyUrl => 'URL del estudio'; + + @override + String get studyCurrentChapterUrl => 'URL del capítulo actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Puedes pegar esto en el foro para insertar la partida'; + + @override + String get studyStartAtInitialPosition => 'Comenzar desde la posición inicial'; + + @override + String studyStartAtX(String param) { + return 'Comenzar en $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Insértalo en tu página o blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Leer más sobre insertar contenido'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '¡Solo los estudios públicos pueden ser insertados!'; + + @override + String get studyOpen => 'Abrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, proporcionado por $param2'; + } + + @override + String get studyStudyNotFound => 'No se encontró el estudio'; + + @override + String get studyEditChapter => 'Editar capítulo'; + + @override + String get studyNewChapter => 'Capítulo nuevo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientación'; + + @override + String get studyAnalysisMode => 'Modo de análisis'; + + @override + String get studyPinnedChapterComment => 'Comentario fijo para el capítulo'; + + @override + String get studySaveChapter => 'Guardar capítulo'; + + @override + String get studyClearAnnotations => 'Borrar anotaciones'; + + @override + String get studyClearVariations => 'Borrar variantes'; + + @override + String get studyDeleteChapter => 'Borrar capítulo'; + + @override + String get studyDeleteThisChapter => '¿Realmente quieres borrar el capítulo? ¡Esta acción no se puede deshacer!'; + + @override + String get studyClearAllCommentsInThisChapter => '¿Borrar todos los comentarios, iconos y marcas de este capítulo?'; + + @override + String get studyRightUnderTheBoard => 'Justo debajo del tablero'; + + @override + String get studyNoPinnedComment => 'Ninguno'; + + @override + String get studyNormalAnalysis => 'Análisis normal'; + + @override + String get studyHideNextMoves => 'Ocultar los siguientes movimientos'; + + @override + String get studyInteractiveLesson => 'Lección interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítulo $param'; + } + + @override + String get studyEmpty => 'Vacío'; + + @override + String get studyStartFromInitialPosition => 'Comenzar desde la posición inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Comenzar desde una posición personalizada'; + + @override + String get studyLoadAGameByUrl => 'Cargar una partida desde una URL'; + + @override + String get studyLoadAPositionFromFen => 'Cargar una posición vía código FEN'; + + @override + String get studyLoadAGameFromPgn => 'Cargar una partida vía código PGN'; + + @override + String get studyAutomatic => 'Automática'; + + @override + String get studyUrlOfTheGame => 'URL de la partida'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Cargar una partida desde $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítulo'; + + @override + String get studyCreateStudy => 'Crear estudio'; + + @override + String get studyEditStudy => 'Editar estudio'; + + @override + String get studyVisibility => 'Visibilidad'; + + @override + String get studyPublic => 'Público'; + + @override + String get studyUnlisted => 'Sin listar'; + + @override + String get studyInviteOnly => 'Acceso mediante invitación'; + + @override + String get studyAllowCloning => 'Permitir clonado'; + + @override + String get studyNobody => 'Nadie'; + + @override + String get studyOnlyMe => 'Sólo yo'; + + @override + String get studyContributors => 'Colaboradores'; + + @override + String get studyMembers => 'Miembros'; + + @override + String get studyEveryone => 'Todo el mundo'; + + @override + String get studyEnableSync => 'Habilitar sincronización'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sí: todo el mundo ve la misma posición'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: permitir que la gente navegue libremente'; + + @override + String get studyPinnedStudyComment => 'Comentario fijado del estudio'; + @override String get studyStart => 'Comenzar'; + + @override + String get studySave => 'Guardar'; + + @override + String get studyClearChat => 'Limpiar el chat'; + + @override + String get studyDeleteTheStudyChatHistory => '¿Realmente quieres borrar el historial de chat? ¡Esta acción no se puede deshacer!'; + + @override + String get studyDeleteStudy => 'Borrar estudio'; + + @override + String studyConfirmDeleteStudy(String param) { + return '¿Seguro que quieres eliminar el estudio? Ten en cuenta que esta acción no se puede deshacer. Para confirmar, escribe el nombre del estudio: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => '¿Dónde quieres estudiar eso?'; + + @override + String get studyGoodMove => 'Jugada buena'; + + @override + String get studyMistake => 'Error'; + + @override + String get studyBrilliantMove => 'Jugada muy buena'; + + @override + String get studyBlunder => 'Error grave'; + + @override + String get studyInterestingMove => 'Jugada interesante'; + + @override + String get studyDubiousMove => 'Jugada dudosa'; + + @override + String get studyOnlyMove => 'Movimiento único'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posición igualada'; + + @override + String get studyUnclearPosition => 'Posición poco clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'Las blancas están ligeramente mejor'; + + @override + String get studyBlackIsSlightlyBetter => 'Las negras están ligeramente mejor'; + + @override + String get studyWhiteIsBetter => 'Las blancas están mejor'; + + @override + String get studyBlackIsBetter => 'Las negras están mejor'; + + @override + String get studyWhiteIsWinning => 'Las blancas están ganando'; + + @override + String get studyBlackIsWinning => 'Las negras están ganando'; + + @override + String get studyNovelty => 'Novedad'; + + @override + String get studyDevelopment => 'Desarrollo'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Ataque'; + + @override + String get studyCounterplay => 'Contrajuego'; + + @override + String get studyTimeTrouble => 'Problema de tiempo'; + + @override + String get studyWithCompensation => 'Con compensación'; + + @override + String get studyWithTheIdea => 'Con la idea'; + + @override + String get studyNextChapter => 'Capítulo siguiente'; + + @override + String get studyPrevChapter => 'Capítulo anterior'; + + @override + String get studyStudyActions => 'Acciones de estudio'; + + @override + String get studyTopics => 'Temas'; + + @override + String get studyMyTopics => 'Mis temas'; + + @override + String get studyPopularTopics => 'Temas populares'; + + @override + String get studyManageTopics => 'Administrar temas'; + + @override + String get studyBack => 'Volver'; + + @override + String get studyPlayAgain => 'Jugar de nuevo'; + + @override + String get studyWhatWouldYouPlay => '¿Qué jugarías en esta posición?'; + + @override + String get studyYouCompletedThisLesson => '¡Felicidades! Has completado esta lección.'; + + @override + String studyPerPage(String param) { + return '$param por página'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítulos', + one: '$count Capítulo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partidas', + one: '$count Partida', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Miembros', + one: '$count Miembro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pega aquí el código PGN, $count partidas como máximo', + one: 'Pega aquí el código PGN, $count partida como máximo', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'ahora mismo'; + + @override + String get timeagoRightNow => 'ahora mismo'; + + @override + String get timeagoCompleted => 'completado'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segundos', + one: 'en $count segundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutos', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horas', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count días', + one: 'en $count día', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semanas', + one: 'en $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count meses', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count años', + one: 'en $count año', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count minutos', + one: 'hace $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count horas', + one: 'hace $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count días', + one: 'hace $count día', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count semanas', + one: 'hace $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count meses', + one: 'hace $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count años', + one: 'hace $count año', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minutos restantes', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count horas restantes', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index fe1ec9d9f3..99c21d4fdd 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsEt extends AppLocalizations { AppLocalizationsEt([String locale = 'et']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsEt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktiivsus'; @@ -246,6 +243,17 @@ class AppLocalizationsEt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEt extends AppLocalizations { @override String get broadcastBroadcasts => 'Otseülekanded'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Otseülekanded turniirilt'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Uus otseülekanne'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Lisa voor'; + + @override + String get broadcastOngoing => 'Käimas'; + + @override + String get broadcastUpcoming => 'Tulemas'; + + @override + String get broadcastCompleted => 'Lõppenud'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Vooru nimi'; + + @override + String get broadcastRoundNumber => 'Vooru number'; + + @override + String get broadcastTournamentName => 'Turniiri nimi'; + + @override + String get broadcastTournamentDescription => 'Lühike turniiri kirjeldus'; + + @override + String get broadcastFullDescription => 'Sündmuse täielik kirjeldus'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valikuline otseülekande kirjeldus. $param1 on saadaval. Pikkus peab olema maksimaalselt $param2 tähemärki.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, kust Lichess saab PGN-i värskenduse. See peab olema Internetist kättesaadav.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valikuline, kui tead millal sündmus algab'; + + @override + String get broadcastCurrentGameUrl => 'Praeguse mängu URL'; + + @override + String get broadcastDownloadAllRounds => 'Lae alla kõik voorud'; + + @override + String get broadcastResetRound => 'Lähtesta see voor'; + + @override + String get broadcastDeleteRound => 'Kustuta see voor'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Kustuta lõplikult voor ja selle mängud.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Kustuta kõik mängud sellest voorust. Allikas peab olema aktiveeritud nende taastamiseks.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Malekell'; @@ -750,6 +1008,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Teavituste heli'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Pusled'; @@ -1390,10 +1651,10 @@ class AppLocalizationsEt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Vastasel on piiratud võimalused teha lubatud käike ja kõik halvendavad vastase olukorda.'; @override - String get puzzleThemeHealthyMix => 'Tervislik segu'; + String get puzzleThemeMix => 'Tervislik segu'; @override - String get puzzleThemeHealthyMixDescription => 'Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.'; + String get puzzleThemeMixDescription => 'Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1767,9 +2028,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get byCPL => 'CPL järgi'; - @override - String get openStudy => 'Ava uuring'; - @override String get enable => 'Luba'; @@ -1797,9 +2055,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get removesTheDepthLimit => 'Eemaldab sügavuslimiidi ja hoiab arvuti soojana'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Viga'; @@ -2063,6 +2318,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get gamesPlayed => 'Mänge mängitud'; + @override + String get ok => 'OK'; + @override String get cancel => 'Katkesta'; @@ -2437,9 +2695,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get unblock => 'Tühista blokeering'; - @override - String get followsYou => 'Jälgib sind'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 hakkas jälgima $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsEt extends AppLocalizations { String get other => 'Muu'; @override - String get reportDescriptionHelp => 'Kleebi link mängu(de)st ja selgita, mis on valesti selle kasutaja käitumises. Ära ütle lihtsalt \"ta teeb sohki\", vaid seleta, kuidas sa selle järelduseni jõudsid. Sõnum käsitletakse kiiremini kui see on kirjutatud inglise keeles.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Palun andke vähemalt üks link pettust sisaldavale mängule.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4093,8 +4357,8 @@ class AppLocalizationsEt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Šahh ja matt $count käiguga', - one: 'Šahh ja Matt $count käiguga', + other: 'Matt $count käiguga', + one: 'Matt $count käiguga', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsEt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichessi striimijad'; + @override + String get studyPrivate => 'Privaatne'; + + @override + String get studyMyStudies => 'Minu uuringud'; + + @override + String get studyStudiesIContributeTo => 'Uuringud, milles osalen'; + + @override + String get studyMyPublicStudies => 'Minu avalikud uuringud'; + + @override + String get studyMyPrivateStudies => 'Minu privaatsed uuringud'; + + @override + String get studyMyFavoriteStudies => 'Minu lemmikuuringud'; + + @override + String get studyWhatAreStudies => 'Mis on uuringud?'; + + @override + String get studyAllStudies => 'Kõik uuringud'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param loodud uuringud'; + } + + @override + String get studyNoneYet => 'Veel mitte ühtegi.'; + + @override + String get studyHot => 'Kuum'; + + @override + String get studyDateAddedNewest => 'Lisamisaeg (uusimad)'; + + @override + String get studyDateAddedOldest => 'Lisamisaeg (vanimad)'; + + @override + String get studyRecentlyUpdated => 'Hiljuti uuendatud'; + + @override + String get studyMostPopular => 'Kõige populaarsemad'; + + @override + String get studyAlphabetical => 'Tähestikuline'; + + @override + String get studyAddNewChapter => 'Lisa uus peatükk'; + + @override + String get studyAddMembers => 'Lisa liikmeid'; + + @override + String get studyInviteToTheStudy => 'Kutsu uuringule'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Palun kutsuge ainult inimesi keda te teate ning kes soovivad aktiivselt selle uuringuga liituda.'; + + @override + String get studySearchByUsername => 'Otsi kasutajanime järgi'; + + @override + String get studySpectator => 'Vaatleja'; + + @override + String get studyContributor => 'Panustaja'; + + @override + String get studyKick => 'Viska välja'; + + @override + String get studyLeaveTheStudy => 'Lahku uuringust'; + + @override + String get studyYouAreNowAContributor => 'Te olete nüüd panustaja'; + + @override + String get studyYouAreNowASpectator => 'Te olete nüüd vaatleja'; + + @override + String get studyPgnTags => 'PGN sildid'; + + @override + String get studyLike => 'Meeldib'; + + @override + String get studyUnlike => 'Eemalda meeldimine'; + + @override + String get studyNewTag => 'Uus silt'; + + @override + String get studyCommentThisPosition => 'Kommenteeri seda seisu'; + + @override + String get studyCommentThisMove => 'Kommenteeri seda käiku'; + + @override + String get studyAnnotateWithGlyphs => 'Annoteerige glüüfidega'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'See peatükk on liiga lühike analüüsimiseks.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Ainult selle uuringu panustajad saavad taotleda arvuti analüüsi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Taotle täielikku serveripoolset arvuti analüüsi põhiliinist.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'Mängimas'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Esimene'; + + @override + String get studyPrevious => 'Eelmine'; + + @override + String get studyNext => 'Järgmine'; + + @override + String get studyLast => 'Viimane'; + @override String get studyShareAndExport => 'Jaga & ekspordi'; + @override + String get studyCloneStudy => 'Klooni'; + + @override + String get studyStudyPgn => 'Uuringu PGN'; + + @override + String get studyDownloadAllGames => 'Lae alla kõik mängud'; + + @override + String get studyChapterPgn => 'Peatüki PGN'; + + @override + String get studyCopyChapterPgn => 'Kopeeri PGN'; + + @override + String get studyDownloadGame => 'Lae alla mäng'; + + @override + String get studyStudyUrl => 'Uuringu URL'; + + @override + String get studyCurrentChapterUrl => 'Praeguse peatüki URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Te saate selle asetada foorumisse või oma Lichessi blogisse sängitamiseks'; + + @override + String get studyStartAtInitialPosition => 'Alusta algseisus'; + + @override + String studyStartAtX(String param) { + return 'Alusta $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Sängita oma veebilehele'; + + @override + String get studyReadMoreAboutEmbedding => 'Loe rohkem sängitamisest'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ainult avalikud uurimused on sängitatavad!'; + + @override + String get studyOpen => 'Ava'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, leheküljelt $param2'; + } + + @override + String get studyStudyNotFound => 'Uuringut ei leitud'; + + @override + String get studyEditChapter => 'Muuda peatükki'; + + @override + String get studyNewChapter => 'Uus peatükk'; + + @override + String studyImportFromChapterX(String param) { + return 'Too peatükist $param'; + } + + @override + String get studyOrientation => 'Suund'; + + @override + String get studyAnalysisMode => 'Analüüsirežiim'; + + @override + String get studyPinnedChapterComment => 'Kinnitatud peatüki kommentaar'; + + @override + String get studySaveChapter => 'Salvesta peatükk'; + + @override + String get studyClearAnnotations => 'Eemalda kommentaarid'; + + @override + String get studyClearVariations => 'Eemalda variatsioonid'; + + @override + String get studyDeleteChapter => 'Kustuta peatükk'; + + @override + String get studyDeleteThisChapter => 'Kustuta see peatükk? Seda ei saa tühistada!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Puhasta kõik kommentaarid, glüüfid ja joonistatud kujundid sellest peatükist'; + + @override + String get studyRightUnderTheBoard => 'Otse laua all'; + + @override + String get studyNoPinnedComment => 'Puudub'; + + @override + String get studyNormalAnalysis => 'Tavaline analüüs'; + + @override + String get studyHideNextMoves => 'Peida järgmised käigud'; + + @override + String get studyInteractiveLesson => 'Interaktiivne õppetund'; + + @override + String studyChapterX(String param) { + return 'Peatükk $param'; + } + + @override + String get studyEmpty => 'Tühi'; + + @override + String get studyStartFromInitialPosition => 'Alusta algsest positsioonist'; + + @override + String get studyEditor => 'Muuda'; + + @override + String get studyStartFromCustomPosition => 'Alusta kohandatud positsioonist'; + + @override + String get studyLoadAGameByUrl => 'Lae mäng alla URL-ist'; + + @override + String get studyLoadAPositionFromFen => 'Laadi alla positsioon FEN-ist'; + + @override + String get studyLoadAGameFromPgn => 'Lae mänge PGN-ist'; + + @override + String get studyAutomatic => 'Automaatne'; + + @override + String get studyUrlOfTheGame => 'URL mängu'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lae mäng alla $param1 või $param2'; + } + + @override + String get studyCreateChapter => 'Alusta peatükk'; + + @override + String get studyCreateStudy => 'Koosta uuring'; + + @override + String get studyEditStudy => 'Muuda uuringut'; + + @override + String get studyVisibility => 'Nähtavus'; + + @override + String get studyPublic => 'Avalik'; + + @override + String get studyUnlisted => 'Mitte avalik'; + + @override + String get studyInviteOnly => 'Ainult kutsega'; + + @override + String get studyAllowCloning => 'Luba kloneerimine'; + + @override + String get studyNobody => 'Mitte keegi'; + + @override + String get studyOnlyMe => 'Ainult mina'; + + @override + String get studyContributors => 'Panustajad'; + + @override + String get studyMembers => 'Liikmed'; + + @override + String get studyEveryone => 'Kõik'; + + @override + String get studyEnableSync => 'Luba sünkroneerimine'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jah: hoia kõik samal positsioonil'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ei: lase inimestel sirvida vabalt'; + + @override + String get studyPinnedStudyComment => 'Kinnitatud uuringu kommentaar'; + @override String get studyStart => 'Alusta'; + + @override + String get studySave => 'Salvesta'; + + @override + String get studyClearChat => 'Clear chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Kas soovite kustutada uuringu vestluse ajaloo? Seda otsust ei saa tagasi võtta!'; + + @override + String get studyDeleteStudy => 'Kustuta uuring'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Kas soovite kustutada terve uuringu? Seda otsust ei saa tagasi võtta! Kirjutage uuringu nimi otsuse kinnitamiseks: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kus te seda lauda soovite uurida?'; + + @override + String get studyGoodMove => 'Hea käik'; + + @override + String get studyMistake => 'Viga'; + + @override + String get studyBrilliantMove => 'Suurepärane käik'; + + @override + String get studyBlunder => 'Tõsine viga'; + + @override + String get studyInterestingMove => 'Huvitav käik'; + + @override + String get studyDubiousMove => 'Kahtlane käik'; + + @override + String get studyOnlyMove => 'Ainus käik'; + + @override + String get studyZugzwang => 'Sundkäik'; + + @override + String get studyEqualPosition => 'Võrdne positsioon'; + + @override + String get studyUnclearPosition => 'Ebaselge positsioon'; + + @override + String get studyWhiteIsSlightlyBetter => 'Valgel on kerge eelis'; + + @override + String get studyBlackIsSlightlyBetter => 'Mustal on kerge eelis'; + + @override + String get studyWhiteIsBetter => 'Valgel on eelis'; + + @override + String get studyBlackIsBetter => 'Mustal on eelis'; + + @override + String get studyWhiteIsWinning => 'Valge on võitmas'; + + @override + String get studyBlackIsWinning => 'Must on võitmas'; + + @override + String get studyNovelty => 'Uudsus'; + + @override + String get studyDevelopment => 'Arendus'; + + @override + String get studyInitiative => 'Algatus'; + + @override + String get studyAttack => 'Rünnak'; + + @override + String get studyCounterplay => 'Vastumäng'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Järgmine peatükk'; + + @override + String get studyPrevChapter => 'Eelmine peatükk'; + + @override + String get studyStudyActions => 'Uuringu toimingud'; + + @override + String get studyTopics => 'Teemad'; + + @override + String get studyMyTopics => 'Minu teemad'; + + @override + String get studyPopularTopics => 'Populaarsed teemad'; + + @override + String get studyManageTopics => 'Halda teemasid'; + + @override + String get studyBack => 'Tagasi'; + + @override + String get studyPlayAgain => 'Mängi uuesti'; + + @override + String get studyWhatWouldYouPlay => 'Mis sa mängiksid selles positsioonis?'; + + @override + String get studyYouCompletedThisLesson => 'Palju õnne! Oled läbinud selle õppetunni.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count peatükki', + one: '$count peatükk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mängu', + one: '$count mäng', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count liiget', + one: '$count liige', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Aseta oma PGN tekst siia, kuni $count mängu', + one: 'Aseta oma PGN tekst siia, kuni $count mäng', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'äsja'; + + @override + String get timeagoRightNow => 'praegu'; + + @override + String get timeagoCompleted => 'lõppenud'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sekundi pärast', + one: '$count sekundi pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti pärast', + one: '$count minuti pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tunni pärast', + one: '$count tunni pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päeva pärast', + one: '$count päeva pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nädala pärast', + one: '$count nädala pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuu pärast', + one: '$count kuu pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count aasta pärast', + one: '$count aasta pärast', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutit tagasi', + one: '$count minut tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tundi tagasi', + one: '$count tund tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päeva tagasi', + one: '$count päev tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nädalat tagasi', + one: '$count nädal tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuud tagasi', + one: '$count kuu tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count aastat tagasi', + one: '$count aasta tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutit jäänud', + one: '$count minut jäänud', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tundi jäänud', + one: '$count tund jäänud', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index f3e070aab1..5e05fe1573 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsEu extends AppLocalizations { AppLocalizationsEu([String locale = 'eu']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Partida guztiak'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Ziur zaude?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Bertan behera utzi atzera-egite eskaera'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Garbitu'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Garbitu gordetako jokaldia'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Sartu partida baten'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Iritzia'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Kaixo $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Kaixo'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Ezkutatu aukera'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Hasiera'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Zuzeneko streamerrak'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'Sartu egin behar zara orri hau ikusteko.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'Emaitzarik ez'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'Ez zaude erabiltzailerik jarraitzen.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'Ados'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return '\"$param\" duten jokalariak'; } @override - String get mobileNoSearchResults => 'No results'; - - @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePrefMagnifyDraggedPiece => 'Handitu arrastatutako pieza'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormConfirmEndRun => 'Saiakera hau amaitu nahi duzu?'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ez dago erakusteko ezer, aldatu filtroak'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormNothingToShow => 'Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Ebatzi ahalik eta ariketa gehien 3 minututan.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Ariketak'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Azken bilaketak'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsHapticFeedback => 'Ukipen-erantzuna'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Murgiltze modua'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Ezarpenak'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGamePGN => 'Partekatu PGNa'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Partekatu partidaren URLa'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Partekatu posizioa FEN gisa'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Partekatu ariketa hau'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Erakutsi iruzkinak'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Erakutsi emaitza'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Erakutsi aukerak'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Zerbait gaizki joan da.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Sistemaren koloreak'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Itxura'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tresnak'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Aurkaria sartzeko zain...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Ikusi'; @override String get activityActivity => 'Jarduera'; @@ -246,6 +243,17 @@ class AppLocalizationsEu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$param2 posta bidezko $count partida osatuta', + one: '$param2 posta bidezko partida $count osatuta', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsEu extends AppLocalizations { @override String get broadcastBroadcasts => 'Emanaldiak'; + @override + String get broadcastMyBroadcasts => 'Nire zuzenekoak'; + @override String get broadcastLiveBroadcasts => 'Txapelketen zuzeneko emanaldiak'; + @override + String get broadcastBroadcastCalendar => 'Emanaldien egutegia'; + + @override + String get broadcastNewBroadcast => 'Zuzeneko emanaldi berria'; + + @override + String get broadcastSubscribedBroadcasts => 'Harpidetutako emanaldiak'; + + @override + String get broadcastAboutBroadcasts => 'Zuzeneko emanaldiei buruz'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Nola erabili Lichessen Zuzenekoak.'; + + @override + String get broadcastTheNewRoundHelp => 'Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu.'; + + @override + String get broadcastAddRound => 'Gehitu txanda bat'; + + @override + String get broadcastOngoing => 'Orain martxan'; + + @override + String get broadcastUpcoming => 'Hurrengo emanaldiak'; + + @override + String get broadcastCompleted => 'Amaitutako emanaldiak'; + + @override + String get broadcastCompletedHelp => 'Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago.'; + + @override + String get broadcastRoundName => 'Txandaren izena'; + + @override + String get broadcastRoundNumber => 'Txanda zenbaki'; + + @override + String get broadcastTournamentName => 'Txapelketaren izena'; + + @override + String get broadcastTournamentDescription => 'Txapelketaren deskribapen laburra'; + + @override + String get broadcastFullDescription => 'Ekitaldiaren deskribapen osoa'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Emanaldiaren azalpen luzea, hautazkoa da. $param1 badago. Luzera $param2 karaktere edo laburragoa izan behar da.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGNaren jatorrizko URLa'; + + @override + String get broadcastSourceUrlHelp => 'Lichessek PGNaren eguneraketak jasoko dituen URLa. Interneteko helbide bat izan behar da.'; + + @override + String get broadcastSourceGameIds => 'Gehienez ere Lichesseko 64 partidren idak, espazioekin banatuta.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Txapelketaren hasiera ordua ordu-zona lokalean: $param'; + } + + @override + String get broadcastStartDateHelp => 'Hautazkoa, ekitaldia noiz hasten den baldin badakizu'; + + @override + String get broadcastCurrentGameUrl => 'Uneko partidaren URL helbidea'; + + @override + String get broadcastDownloadAllRounds => 'Deskargatu txanda guztiak'; + + @override + String get broadcastResetRound => 'Berrezarri txanda hau'; + + @override + String get broadcastDeleteRound => 'Ezabatu txanda hau'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Betiko ezabatu txanda eta bere partida guztiak.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Ezabatu txanda honetako partida guztiak. Jatorria aktibo egon behar da berriz sortzeko.'; + + @override + String get broadcastEditRoundStudy => 'Editatu txandako azterlana'; + + @override + String get broadcastDeleteTournament => 'Ezabatu txapelketa hau'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Txapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne.'; + + @override + String get broadcastShowScores => 'Erakutsi jokalarien puntuazioak partiden emaitzen arabera'; + + @override + String get broadcastReplacePlayerTags => 'Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak'; + + @override + String get broadcastFideFederations => 'FIDE federazioak'; + + @override + String get broadcastTop10Rating => '10 onenak'; + + @override + String get broadcastFidePlayers => 'FIDE jokalariak'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE jokalaria ez da aurkitu'; + + @override + String get broadcastFideProfile => 'FIDE profila'; + + @override + String get broadcastFederation => 'Federazioa'; + + @override + String get broadcastAgeThisYear => 'Adina'; + + @override + String get broadcastUnrated => 'Ez du sailkapenik'; + + @override + String get broadcastRecentTournaments => 'Azken txapelketak'; + + @override + String get broadcastOpenLichess => 'Ireki Lichessen'; + + @override + String get broadcastTeams => 'Taldeak'; + + @override + String get broadcastBoards => 'Taulak'; + + @override + String get broadcastOverview => 'Laburpena'; + + @override + String get broadcastSubscribeTitle => 'Harpidetu txanda bakoitza hastean jakinarazpena jasotzeko. Kanpaia edo push erako notifikazioak zure kontuaren hobespenetan aktibatu ditzakezu.'; + + @override + String get broadcastUploadImage => 'Kargatu txapelketaren irudia'; + + @override + String get broadcastNoBoardsYet => 'Taularik ez oraindik. Partidak igotzean agertuko dira.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Taulak iturburu batekin edo ${param}ren bidez kargatu daitezke'; + } + + @override + String broadcastStartsAfter(String param) { + return '${param}ren ondoren hasiko da'; + } + + @override + String get broadcastStartVerySoon => 'Zuzenekoa berehala hasiko da.'; + + @override + String get broadcastNotYetStarted => 'Zuzenekoa ez da oraindik hasi.'; + + @override + String get broadcastOfficialWebsite => 'Webgune ofiziala'; + + @override + String get broadcastStandings => 'Sailkapena'; + + @override + String get broadcastOfficialStandings => 'Sailkapen ofiziala'; + + @override + String broadcastIframeHelp(String param) { + return 'Aukera gehiago ${param}ean'; + } + + @override + String get broadcastWebmastersPage => 'webmasterraren webgune'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Txanda honen zuzeneko PGN iturburua. $param ere eskaintzen dugu sinkronizazio zehatzagoa nahi baduzu.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Txertatu zuzeneko hau zure webgunean'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Txertatu $param zure webgunean'; + } + + @override + String get broadcastRatingDiff => 'Elo diferentzia'; + + @override + String get broadcastGamesThisTournament => 'Txapelketa honetako partidak'; + + @override + String get broadcastScore => 'Emaitza'; + + @override + String get broadcastAllTeams => 'Talde guztiak'; + + @override + String get broadcastTournamentFormat => 'Txapelketaren formatua'; + + @override + String get broadcastTournamentLocation => 'Txapelketaren kokalekua'; + + @override + String get broadcastTopPlayers => 'Jokalari onenak'; + + @override + String get broadcastTimezone => 'Ordu-zona'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating kategoria'; + + @override + String get broadcastOptionalDetails => 'Hautazko xehetasunak'; + + @override + String get broadcastPastBroadcasts => 'Pasatutako zuzenekoak'; + + @override + String get broadcastAllBroadcastsByMonth => 'Ikusi zuzeneko guztiak hilabeteka'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count zuzeneko', + one: 'Zuzeneko $count', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Erronkak: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get preferencesInGameOnly => 'Partidan zehar bakarrik'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Xake-erlojua'; @@ -750,6 +1008,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Kanpaiaren jakinarazpen soinua'; + @override + String get preferencesBlindfold => 'Itsuka'; + @override String get puzzlePuzzles => 'Ariketak'; @@ -1390,10 +1651,10 @@ class AppLocalizationsEu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Aurkariak jokaldi mugatuak ditu eta jokaldi guztien bere posizioa okertu egiten dute.'; @override - String get puzzleThemeHealthyMix => 'Denetik pixkat'; + String get puzzleThemeMix => 'Denetik pixkat'; @override - String get puzzleThemeHealthyMixDescription => 'Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.'; + String get puzzleThemeMixDescription => 'Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.'; @override String get puzzleThemePlayerGames => 'Jokalarien partidak'; @@ -1636,10 +1897,10 @@ class AppLocalizationsEu extends AppLocalizations { String get deleteFromHere => 'Ezabatu hemendik aurrera'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Ezkutatu aldaerak'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Erakutsi aldaerak'; @override String get forceVariation => 'Aldaera derrigortu'; @@ -1767,9 +2028,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Ikerketa ireki'; - @override String get enable => 'Aktibatu'; @@ -1797,9 +2055,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get removesTheDepthLimit => 'Sakonera muga ezabatzendu eta zure ordenagailua epel mantentzen du'; - @override - String get engineManager => 'Motore kudeatzailea'; - @override String get blunder => 'Hanka-sartzea'; @@ -1878,7 +2133,7 @@ class AppLocalizationsEu extends AppLocalizations { String get friends => 'Lagunak'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'beste jokalariak'; @override String get discussions => 'Eztabaidak'; @@ -2063,6 +2318,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get gamesPlayed => 'Partida jokaturik'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ezeztatu'; @@ -2121,7 +2379,7 @@ class AppLocalizationsEu extends AppLocalizations { String get standard => 'Ohikoa'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Posizio pertsonalizatua'; @override String get unlimited => 'Mugagabea'; @@ -2437,9 +2695,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get unblock => 'Desblokeatu'; - @override - String get followsYou => 'Zu jarraitzen'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 jarraitzen hasi da'; @@ -2631,7 +2886,7 @@ class AppLocalizationsEu extends AppLocalizations { String get editProfile => 'Nire profila editatu'; @override - String get realName => 'Real name'; + String get realName => 'Benetako izena'; @override String get setFlair => 'Ezarri zure iruditxoa'; @@ -2691,7 +2946,7 @@ class AppLocalizationsEu extends AppLocalizations { String get puzzles => 'Ariketak'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Online dauden botak'; @override String get name => 'Izena'; @@ -2712,10 +2967,10 @@ class AppLocalizationsEu extends AppLocalizations { String get yes => 'Bai'; @override - String get website => 'Website'; + String get website => 'Webgunea'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobila'; @override String get help => 'Laguntza:'; @@ -2772,7 +3027,13 @@ class AppLocalizationsEu extends AppLocalizations { String get other => 'Bestelakoak'; @override - String get reportDescriptionHelp => 'Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak. Zure mezua azkarrago begiratuko dugu ingelesez idazten baduzu.'; + String get reportCheatBoostHelp => 'Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak.'; + + @override + String get reportUsernameHelp => 'Azaldu erabiltzaile-izen honek zer duen iraingarria. Ez esan \"iraingarria da\" soilik, eman arrazoiak, batez ere iraina ezkutatuta badago, ez bada ingelesezko hitz bat edo errefererantzia historiko edo kulturala bada.'; + + @override + String get reportProcessedFasterInEnglish => 'Zure mezua azkarrago kudeatuko dugu ingelesez idazten baduzu.'; @override String get error_provideOneCheatedGameLink => 'Iruzurra izandako partida baten lotura bidali gutxienez.'; @@ -2875,7 +3136,7 @@ class AppLocalizationsEu extends AppLocalizations { String get outsideTheBoard => 'Taulatik kanpo'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Taulako lauki guztiak'; @override String get onSlowGames => 'Partida moteletan'; @@ -3471,22 +3732,22 @@ class AppLocalizationsEu extends AppLocalizations { String get backgroundImageUrl => 'Atzeko-planoko irudia:'; @override - String get board => 'Board'; + String get board => 'Taula'; @override - String get size => 'Size'; + String get size => 'Tamaina'; @override - String get opacity => 'Opacity'; + String get opacity => 'Gardentasuna'; @override - String get brightness => 'Brightness'; + String get brightness => 'Argitasuna'; @override - String get hue => 'Hue'; + String get hue => 'Ñabardura'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Berrezarri koloreak defektuzkoetara'; @override String get pieceSet => 'Pieza formatua'; @@ -4075,7 +4336,10 @@ class AppLocalizationsEu extends AppLocalizations { String get lichessPatronInfo => 'Lichess software librea da.\nGarapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Hemen ez dago ezer zuretzat.'; + + @override + String get stats => 'Estatistikak'; @override String opponentLeftCounter(int count) { @@ -4724,8 +4988,692 @@ class AppLocalizationsEu extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streamerrak'; @override - String get studyShareAndExport => 'Partekatu & esportatu'; + String get studyPrivate => 'Pribatua'; @override - String get studyStart => 'Hasi'; + String get studyMyStudies => 'Nire azterlanak'; + + @override + String get studyStudiesIContributeTo => 'Nik parte hartzen dudan azterlanak'; + + @override + String get studyMyPublicStudies => 'Nire azterlan publikoak'; + + @override + String get studyMyPrivateStudies => 'Nire azterlan pribatuak'; + + @override + String get studyMyFavoriteStudies => 'Nire azterlan gogokoenak'; + + @override + String get studyWhatAreStudies => 'Zer dira azterlanak?'; + + @override + String get studyAllStudies => 'Azterlan guztiak'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param erabiltzaileak sortutako azterlanak'; + } + + @override + String get studyNoneYet => 'Bat ere ez.'; + + @override + String get studyHot => 'Nabarmendutakoak'; + + @override + String get studyDateAddedNewest => 'Sorrera-data (berriena)'; + + @override + String get studyDateAddedOldest => 'Sorrera-data (zaharrena)'; + + @override + String get studyRecentlyUpdated => 'Eguneratutako azkenak'; + + @override + String get studyMostPopular => 'Arrakasta gehien duena'; + + @override + String get studyAlphabetical => 'Alfabetikoa'; + + @override + String get studyAddNewChapter => 'Kapitulu berria gehitu'; + + @override + String get studyAddMembers => 'Kideak gehitu'; + + @override + String get studyInviteToTheStudy => 'Azterlanera gonbidatu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ezagutzen duzun eta benetan azterlanean interesa duen jendea gonbidatu bakarrik.'; + + @override + String get studySearchByUsername => 'Erabiltzaile izenaren arabera bilatu'; + + @override + String get studySpectator => 'Ikuslea'; + + @override + String get studyContributor => 'Laguntzailea'; + + @override + String get studyKick => 'Kanporatu'; + + @override + String get studyLeaveTheStudy => 'Azterlana utzi'; + + @override + String get studyYouAreNowAContributor => 'Laguntzailea zara orain'; + + @override + String get studyYouAreNowASpectator => 'Ikuslea zara orain'; + + @override + String get studyPgnTags => 'PGN etiketak'; + + @override + String get studyLike => 'Datsegit'; + + @override + String get studyUnlike => 'Ez dut atsegin'; + + @override + String get studyNewTag => 'Etiketa berria'; + + @override + String get studyCommentThisPosition => 'Posizio hau komentatu'; + + @override + String get studyCommentThisMove => 'Jokaldi hau komentatu'; + + @override + String get studyAnnotateWithGlyphs => 'Ikonoekin komentatu'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Komentatzeko laburregia da kapitulua.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Azterlanaren laguntzaileek bakarrik eskatu dezakete ordenagailu bidezko analisia.'; + + @override + String get studyGetAFullComputerAnalysis => 'Linea nagusiaren ordenagailu bidezko analisia lortu.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ziurtatu kapitulua guztiz osatu duzula. Analisia behin bakarrik eskatu dezakezu.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Kide sinkronizatu guztiak posizio berean jarraitzen dute'; + + @override + String get studyShareChanges => 'Aldaketak ikusleekin partekatu eta zerbitzarian gorde'; + + @override + String get studyPlaying => 'Jokatzen'; + + @override + String get studyShowEvalBar => 'Ebaluazio barrak'; + + @override + String get studyFirst => 'Lehenengoa'; + + @override + String get studyPrevious => 'Aurrekoa'; + + @override + String get studyNext => 'Hurrengoa'; + + @override + String get studyLast => 'Azkena'; + + @override + String get studyShareAndExport => 'Partekatu & esportatu'; + + @override + String get studyCloneStudy => 'Klonatu'; + + @override + String get studyStudyPgn => 'Azterlanaren PGNa'; + + @override + String get studyDownloadAllGames => 'Partida guztiak deskargatu'; + + @override + String get studyChapterPgn => 'Kapituluaren PGNa'; + + @override + String get studyCopyChapterPgn => 'Kopiatu PGNa'; + + @override + String get studyDownloadGame => 'Partida deskargatu'; + + @override + String get studyStudyUrl => 'Azterlanaren helbidea'; + + @override + String get studyCurrentChapterUrl => 'Uneko kapituluaren helbidea'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Hau foroan itsatsi dezakezu'; + + @override + String get studyStartAtInitialPosition => 'Hasierako posizioan hasi'; + + @override + String studyStartAtX(String param) { + return 'Hemen asi $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Zure webgunean itsatsi'; + + @override + String get studyReadMoreAboutEmbedding => 'Itsasteari buruz gehiago irakurri'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Azterlan publikoak bakarrik txertatu daitezke beste webguneetan!'; + + @override + String get studyOpen => 'Ireki'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 azterlana $param2 erabiltzaileak prestatu du'; + } + + @override + String get studyStudyNotFound => 'Azterlana ez da aurkitu'; + + @override + String get studyEditChapter => 'Kapitulua aldatu'; + + @override + String get studyNewChapter => 'Kapitulu berria'; + + @override + String studyImportFromChapterX(String param) { + return 'Inportatu $param kapitulotik'; + } + + @override + String get studyOrientation => 'Kokapena'; + + @override + String get studyAnalysisMode => 'Analisi modua'; + + @override + String get studyPinnedChapterComment => 'Kapituluaren iltzatutako iruzkina'; + + @override + String get studySaveChapter => 'Kapitulua gorde'; + + @override + String get studyClearAnnotations => 'Iruzkinak garbitu'; + + @override + String get studyClearVariations => 'Garbitu aldaerak'; + + @override + String get studyDeleteChapter => 'Kapitulua ezabatu'; + + @override + String get studyDeleteThisChapter => 'Kapitulu hau ezabatu egin nahi duzu? Ez dago atzera egiterik!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Kapitulu honetako iruzkin guztiak ezabatu?'; + + @override + String get studyRightUnderTheBoard => 'Xake-taularen azpian'; + + @override + String get studyNoPinnedComment => 'Ez erakutsi'; + + @override + String get studyNormalAnalysis => 'Analisi arrunta'; + + @override + String get studyHideNextMoves => 'Hurrengo jokaldiak ezkutatu'; + + @override + String get studyInteractiveLesson => 'Ikasgai interaktiboa'; + + @override + String studyChapterX(String param) { + return '$param kapitulua'; + } + + @override + String get studyEmpty => 'Hutsa'; + + @override + String get studyStartFromInitialPosition => 'Hasierako posiziotik hasi'; + + @override + String get studyEditor => 'Editorea'; + + @override + String get studyStartFromCustomPosition => 'Pertsonalizatutako posiziotik hasi'; + + @override + String get studyLoadAGameByUrl => 'Partida interneteko helbide batetik kargatu'; + + @override + String get studyLoadAPositionFromFen => 'Posizioa FEN batetik kargatu'; + + @override + String get studyLoadAGameFromPgn => 'Partida PGN batetik kargatu'; + + @override + String get studyAutomatic => 'Automatikoa'; + + @override + String get studyUrlOfTheGame => 'Partidaren URLa'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Hemendik kargatu partida bat: $param1 edo $param2'; + } + + @override + String get studyCreateChapter => 'Kapitulua sortu'; + + @override + String get studyCreateStudy => 'Azterlana sortu'; + + @override + String get studyEditStudy => 'Azterlana aldatu'; + + @override + String get studyVisibility => 'Ikusgaitasuna'; + + @override + String get studyPublic => 'Publikoa'; + + @override + String get studyUnlisted => 'Ez zerrendatu'; + + @override + String get studyInviteOnly => 'Gonbidatuentzat bakarrik'; + + @override + String get studyAllowCloning => 'Kopiatzea utzi'; + + @override + String get studyNobody => 'Inor ere ez'; + + @override + String get studyOnlyMe => 'Ni bakarrik'; + + @override + String get studyContributors => 'Laguntzaileak'; + + @override + String get studyMembers => 'Kideak'; + + @override + String get studyEveryone => 'Guztiak'; + + @override + String get studyEnableSync => 'Sinkronizazioa aktibatu'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Bai: guztiak posizio berean mantendu'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ez: erabiltzaileei nahi dutena egiten utzi'; + + @override + String get studyPinnedStudyComment => 'Azterlanaren iltzatutako iruzkina'; + + @override + String get studyStart => 'Hasi'; + + @override + String get studySave => 'Gorde'; + + @override + String get studyClearChat => 'Txata garbitu'; + + @override + String get studyDeleteTheStudyChatHistory => 'Azterlaneko txata ezabatu? Ez dago atzera egiterik!'; + + @override + String get studyDeleteStudy => 'Azterlana ezabatu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Azterlan osoa ezabatu? Ez dago atzera egiterik! Idatzi azterlanaren izena baieztapena emateko: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Non nahi duzu hori aztertu?'; + + @override + String get studyGoodMove => 'Jokaldi ona'; + + @override + String get studyMistake => 'Akatsa'; + + @override + String get studyBrilliantMove => 'Jokaldi bikaina'; + + @override + String get studyBlunder => 'Akats larria'; + + @override + String get studyInterestingMove => 'Jokaldi interesgarria'; + + @override + String get studyDubiousMove => 'Zalantzazko jokaldia'; + + @override + String get studyOnlyMove => 'Jokaldi bakarra'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Berdindutako posizioa'; + + @override + String get studyUnclearPosition => 'Posizioa ez da argia'; + + @override + String get studyWhiteIsSlightlyBetter => 'Zuria hobetoxeago'; + + @override + String get studyBlackIsSlightlyBetter => 'Beltza hobetoxeago'; + + @override + String get studyWhiteIsBetter => 'Zuria hobeto'; + + @override + String get studyBlackIsBetter => 'Beltza hobeto'; + + @override + String get studyWhiteIsWinning => 'Zuria irabazten ari da'; + + @override + String get studyBlackIsWinning => 'Beltza irabazten ari da'; + + @override + String get studyNovelty => 'Berritasuna'; + + @override + String get studyDevelopment => 'Garapena'; + + @override + String get studyInitiative => 'Iniziatiba'; + + @override + String get studyAttack => 'Erasoa'; + + @override + String get studyCounterplay => 'Kontraerasoa'; + + @override + String get studyTimeTrouble => 'Denbora-arazoak'; + + @override + String get studyWithCompensation => 'Konepntsazioarekin'; + + @override + String get studyWithTheIdea => 'Ideiarekin'; + + @override + String get studyNextChapter => 'Hurrengo kapitulua'; + + @override + String get studyPrevChapter => 'Aurreko kapitulua'; + + @override + String get studyStudyActions => 'Azterlanen akzioak'; + + @override + String get studyTopics => 'Gaiak'; + + @override + String get studyMyTopics => 'Nire gaiak'; + + @override + String get studyPopularTopics => 'Gai arrakastatsuak'; + + @override + String get studyManageTopics => 'Kudeatu gaiak'; + + @override + String get studyBack => 'Atzera joan'; + + @override + String get studyPlayAgain => 'Jokatu berriz'; + + @override + String get studyWhatWouldYouPlay => 'Zer jokatuko zenuke posizio honetan?'; + + @override + String get studyYouCompletedThisLesson => 'Zorionak! Ikasgai hau osatu duzu.'; + + @override + String studyPerPage(String param) { + return '$param orrialde bakoitzean'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitulu', + one: 'Kapitulu $count', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partida', + one: 'Partida $count', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kide', + one: 'Kide $count', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Itsatsi hemen zure PGNa, gehienez $count partida', + one: 'Itsatsi hemen zure PGNa, gehienez partida $count', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'orain'; + + @override + String get timeagoRightNow => 'orain'; + + @override + String get timeagoCompleted => 'amaituta'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundotan', + one: 'segundo ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minututan', + one: 'minutu ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ordutan', + one: 'ordu ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count egunetan', + one: 'egun ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count egunetan', + one: 'aste ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hilabetetan', + one: 'hilabete ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count urtetan', + one: 'urte ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count minutu', + one: 'orain dela minutu $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count ordu', + one: 'orain dela ordu $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count egun', + one: 'orain dela egun $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count aste', + one: 'orain dela aste $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count hilabete', + one: 'orain dela hilabete $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count urte', + one: 'orain dela urte $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutu falta dira', + one: 'Minutu $count falta da', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ordu falta dira', + one: 'Ordu $count falta da', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index f250133286..585153cf80 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsFa extends AppLocalizations { AppLocalizationsFa([String locale = 'fa']) : super(locale); @override - String get mobileHomeTab => 'خانه'; + String get mobileAllGames => 'همه بازی‌ها'; @override - String get mobilePuzzlesTab => 'معماها'; + String get mobileAreYouSure => 'مطمئنید؟'; @override - String get mobileToolsTab => 'ابزارها'; + String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; @override - String get mobileWatchTab => 'تماشا'; + String get mobileClearButton => 'پاکسازی'; @override - String get mobileSettingsTab => 'تنظیمات'; + String get mobileCorrespondenceClearSavedMove => 'پاک کردن حرکت ذخیره شده'; @override - String get mobileMustBeLoggedIn => 'برای دیدن این صفحه باید وارد حساب‌تان شده باشید.'; + String get mobileCustomGameJoinAGame => 'به بازی بپیوندید'; @override - String get mobileSystemColors => 'رنگ‌های دستگاه'; + String get mobileFeedbackButton => 'بازخورد'; @override - String get mobileFeedbackButton => 'بازخوراند'; + String mobileGreeting(String param) { + return 'درود، $param'; + } @override - String get mobileOkButton => 'باشه'; + String get mobileGreetingWithoutName => 'درود'; @override - String get mobileSettingsHapticFeedback => 'بازخوراند لمسی'; + String get mobileHideVariation => 'پنهانیدن وَرتِش'; @override - String get mobileSettingsImmersiveMode => 'حالت غوطه‌ور'; + String get mobileHomeTab => 'خانه'; @override - String get mobileSettingsImmersiveModeSubtitle => 'هنگام بازی، میانای کاربری دستگاه را پنهان کنید. اگر حرکت‌های ناوبری دستگاه در لبه‌های پرده آزارتان می‌دهد، از این استفاده کنید. برای پرده‌های بازی و معماباران (Puzzle Storm) کاربرد دارد.'; + String get mobileLiveStreamers => 'بَرخَط-محتواسازان زنده'; @override - String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را نمی‌دنبالید.'; + String get mobileMustBeLoggedIn => 'برای دیدن این برگه باید وارد شده باشید.'; @override - String get mobileAllGames => 'همه بازی‌ها'; + String get mobileNoSearchResults => 'بدون پیامد'; @override - String get mobileRecentSearches => 'جستجوهای اخیر'; + String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را نمی‌دنبالید.'; @override - String get mobileClearButton => 'پاکسازی'; + String get mobileOkButton => 'باشه'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'بازیکنانِ «$param»'; + return 'کاربران با پیوند «$param»'; } @override - String get mobileNoSearchResults => 'بدون نتیجه'; + String get mobilePrefMagnifyDraggedPiece => 'بزرگ‌نمودن مهره‌ی کشیده'; @override - String get mobileAreYouSure => 'مطمئنید؟'; + String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را به پایان برسانید؟'; @override - String get mobilePuzzleStreakAbortWarning => 'شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.'; + String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.'; @override String get mobilePuzzleStormNothingToShow => 'چیزی برای نمایش نیست، چند دور معماباران بازی کنید.'; @override - String get mobileSharePuzzle => 'همرسانی این معما'; + String get mobilePuzzleStormSubtitle => 'هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.'; @override - String get mobileShareGameURL => 'همرسانی وب‌نشانی بازی'; + String get mobilePuzzleStreakAbortWarning => 'شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.'; @override - String get mobileShareGamePGN => 'همرسانی PGN'; + String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.'; @override - String get mobileSharePositionAsFEN => 'همرسانی وضعیت، به شکل FEN'; + String get mobilePuzzlesTab => 'معماها'; @override - String get mobileShowVariations => 'باز کردن شاخه‌ها'; + String get mobileRecentSearches => 'واپسین جستجوها'; @override - String get mobileHideVariation => 'بستن شاخه‌ها'; + String get mobileSettingsHapticFeedback => 'بازخورد لمسی'; @override - String get mobileShowComments => 'نمایش نظرها'; + String get mobileSettingsImmersiveMode => 'حالت فراگیر'; @override - String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را پایان دهید؟'; + String get mobileSettingsImmersiveModeSubtitle => 'رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، لطفا پالاب‌گرها را تغییر دهید'; + String get mobileSettingsTab => 'تنظیمات'; @override - String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; + String get mobileShareGamePGN => 'همرسانی PGN'; @override - String get mobileCancelDrawOffer => 'رد پیشنهاد تساوی'; + String get mobileShareGameURL => 'همرسانی وب‌نشانی بازی'; @override - String get mobileWaitingForOpponentToJoin => 'در انتظار آمدن حریف...'; + String get mobileSharePositionAsFEN => 'همرسانی وضعیت، به شکل FEN'; @override - String get mobileBlindfoldMode => 'چشم‌بسته'; + String get mobileSharePuzzle => 'همرسانی این معما'; @override - String get mobileLiveStreamers => 'بَرخَط-محتواسازان زنده'; + String get mobileShowComments => 'نمایش دیدگاه‌ها'; @override - String get mobileCustomGameJoinAGame => 'به بازی بپیوندید'; + String get mobileShowResult => 'نمایش پیامد'; @override - String get mobileCorrespondenceClearSavedMove => 'پاکیدن حرکت ذخیره‌شده'; + String get mobileShowVariations => 'نمایش وَرتِش'; @override String get mobileSomethingWentWrong => 'مشکلی پیش آمد.'; @override - String get mobileShowResult => 'نمایش نتیجه'; - - @override - String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا موضوعی را برگزینید.'; + String get mobileSystemColors => 'رنگ‌های دستگاه'; @override - String get mobilePuzzleStormSubtitle => 'در ۳ دقیقه، هر چندتا معما که می‌توانید، حل کنید.'; + String get mobileTheme => 'پوسته'; @override - String mobileGreeting(String param) { - return 'درود، $param'; - } + String get mobileToolsTab => 'ابزارها'; @override - String get mobileGreetingWithoutName => 'سلام'; + String get mobileWaitingForOpponentToJoin => 'در انتظار آمدن حریف...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'تماشا'; @override String get activityActivity => 'فعالیت'; @@ -156,7 +153,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get activitySignedUp => 'در لیچس ثبت نام کرد'; + String get activitySignedUp => 'در lichess.org نام‌نوشت'; @override String activitySupportedNbMonths(int count, String param2) { @@ -196,8 +193,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی $param2 را انجام داد', - one: '$count بازی $param2 را انجام داد', + other: '$count بازی $param2 کرد', + one: '$count بازی $param2 کرد', ); return '$_temp0'; } @@ -207,8 +204,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count پیام را در $param2 ارسال کرد', - one: '$count پیام را در $param2 ارسال کرد', + other: '$count پیام در $param2 فرستاد', + one: '$count پیام در $param2 فرستاد', ); return '$_temp0'; } @@ -246,13 +243,24 @@ class AppLocalizationsFa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تکمیل $count بازی مکاتبه‌ای $param2', + one: 'تکمیل $count بازی مکاتبه‌ای $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازیکن را دنبال کرد', - one: '$count بازیکن را دنبال کرد', + other: 'شروع به دنبالیدن $count بازیکن کرد', + one: 'شروع به دنبالیدن $count بازیکن کرد', ); return '$_temp0'; } @@ -262,8 +270,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال کننده جدید به دست آورد', - one: '$count دنبال کننده جدید به دست آورد', + other: '$count دنبال‌گر جدید به‌دست آورد', + one: '$count دنبال‌گر جدید به‌دست آورد', ); return '$_temp0'; } @@ -306,8 +314,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'در $count مسابقه آرنا رقابت کرد', - one: 'در $count مسابقه آرنا رقابت کرد', + other: 'در $count مسابقهٔ راوان رقابت کرد', + one: 'در $count مسابقهٔ راوان رقابت کرد', ); return '$_temp0'; } @@ -348,9 +356,256 @@ class AppLocalizationsFa extends AppLocalizations { @override String get broadcastBroadcasts => 'پخش همگانی'; + @override + String get broadcastMyBroadcasts => 'پخش همگانی من'; + @override String get broadcastLiveBroadcasts => 'پخش زنده مسابقات'; + @override + String get broadcastBroadcastCalendar => 'تقویم پخش'; + + @override + String get broadcastNewBroadcast => 'پخش زنده جدید'; + + @override + String get broadcastSubscribedBroadcasts => 'پخش‌های دنبالیده'; + + @override + String get broadcastAboutBroadcasts => 'درباره پخش‌های همگانی'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'نحوه استفاده از پخش همگانی Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'دور جدید، همان اعضا و مشارکت‌کنندگان دور قبلی را خواهد داشت.'; + + @override + String get broadcastAddRound => 'اضافه کردن یک دور'; + + @override + String get broadcastOngoing => 'ادامه‌دار'; + + @override + String get broadcastUpcoming => 'آینده'; + + @override + String get broadcastCompleted => 'کامل‌شده'; + + @override + String get broadcastCompletedHelp => 'Lichess تکمیل دور را شناسایی می‌کند، اما می‌تواند آن را اشتباه بگیرد. از این کلید برای تنظیم دستی بهرایید.'; + + @override + String get broadcastRoundName => 'نام دور'; + + @override + String get broadcastRoundNumber => 'شماره دور'; + + @override + String get broadcastTournamentName => 'نام مسابقات'; + + @override + String get broadcastTournamentDescription => 'توضیحات کوتاه مسابقات'; + + @override + String get broadcastFullDescription => 'توضیحات کامل مسابقات'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'توضیحات بلند و اختیاری پخش همگانی. $param1 قابل‌استفاده است. طول متن باید کمتر از $param2 نویسه باشد.'; + } + + @override + String get broadcastSourceSingleUrl => 'وب‌نشانیِ PGN'; + + @override + String get broadcastSourceUrlHelp => 'وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.'; + + @override + String get broadcastSourceGameIds => 'تا ۶۴ شناسهٔ بازی Lichess، جداشده با فاصله.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'تاریخ آغاز در زمان-یانه محلی مسابقات: $param'; + } + + @override + String get broadcastStartDateHelp => 'اختیاری است، اگر می‌دانید چه زمانی رویداد شروع می‌شود'; + + @override + String get broadcastCurrentGameUrl => 'نشانی بازی کنونی'; + + @override + String get broadcastDownloadAllRounds => 'بارگیری همه دورها'; + + @override + String get broadcastResetRound => 'ازنوکردن این دور'; + + @override + String get broadcastDeleteRound => 'حذف این دور'; + + @override + String get broadcastDefinitivelyDeleteRound => 'این دور و همه بازی‌هایش را به طور کامل حذف کن.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'همه بازی‌های این دور را حذف کن. منبع باید فعال باشد تا بتوان آنها را بازساخت.'; + + @override + String get broadcastEditRoundStudy => 'ویرایش مطالعه دور'; + + @override + String get broadcastDeleteTournament => 'حذف این مسابقات'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'کل مسابقات، شامل همه دورها و بازی‌هایش را به طور کامل حذف کن.'; + + @override + String get broadcastShowScores => 'نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها'; + + @override + String get broadcastReplacePlayerTags => 'اختیاری: عوض کردن نام، درجه‌بندی و عنوان بازیکنان'; + + @override + String get broadcastFideFederations => 'کشورگان‌های فیده'; + + @override + String get broadcastTop10Rating => 'ده درجه‌بندی برتر'; + + @override + String get broadcastFidePlayers => 'بازیکنان فیده'; + + @override + String get broadcastFidePlayerNotFound => 'بازیکن فیده پیدا نشد'; + + @override + String get broadcastFideProfile => 'رُخ‌نمای فیده'; + + @override + String get broadcastFederation => 'کشورگان'; + + @override + String get broadcastAgeThisYear => 'سنِ امسال'; + + @override + String get broadcastUnrated => 'بی‌درجه‌بندی'; + + @override + String get broadcastRecentTournaments => 'مسابقاتِ اخیر'; + + @override + String get broadcastOpenLichess => 'آزاد در Lichess'; + + @override + String get broadcastTeams => 'یَران‌ها'; + + @override + String get broadcastBoards => 'میز‌ها'; + + @override + String get broadcastOverview => 'نمای کلی'; + + @override + String get broadcastSubscribeTitle => 'مشترک شوید تا از آغاز هر دور باخبر شوید. می‌توانید اعلان‌های زنگی یا رانشی برای پخش‌های زنده را در تنظیمات حساب‌تان تغییر دهید.'; + + @override + String get broadcastUploadImage => 'بارگذاری تصویر مسابقات'; + + @override + String get broadcastNoBoardsYet => 'تاکنون هیچی. وقتی بازی‌ها بارگذاری شدند، میزها پدیدار خواهند شد.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'میزها را می‌توان از یک منبع یا از راه $param بارگذاری کرد'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'پخش زنده به زودی آغاز خواهد شد.'; + + @override + String get broadcastNotYetStarted => 'پخش زنده هنوز نیاغازیده است.'; + + @override + String get broadcastOfficialWebsite => 'وبگاه رسمی'; + + @override + String get broadcastStandings => 'رده‌بندی'; + + @override + String get broadcastOfficialStandings => 'رده‌بندی رسمی'; + + @override + String broadcastIframeHelp(String param) { + return 'گزینه‌های بیشتر در $param'; + } + + @override + String get broadcastWebmastersPage => 'صفحهٔ وبداران'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'یک منبع عمومی و بی‌درنگ PGN برای این دور. ما همچنین $param را برای همگامِش تندتر و کارآمدتر پیشنهاد می‌دهیم.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'جاسازی این پخش زنده در وبگاه‌تان'; + + @override + String broadcastEmbedThisRound(String param) { + return 'جاسازی $param در وبگاه‌تان'; + } + + @override + String get broadcastRatingDiff => 'اختلاف درجه‌بندی'; + + @override + String get broadcastGamesThisTournament => 'بازی‌های این مسابقات'; + + @override + String get broadcastScore => 'امتیاز'; + + @override + String get broadcastAllTeams => 'همهٔ یَران‌ها'; + + @override + String get broadcastTournamentFormat => 'ساختار مسابقات'; + + @override + String get broadcastTournamentLocation => 'مکان مسابقات'; + + @override + String get broadcastTopPlayers => 'بازیکنان برتر'; + + @override + String get broadcastTimezone => 'زمان-یانه'; + + @override + String get broadcastFideRatingCategory => 'رسته‌بندی درجه‌بندی فیده'; + + @override + String get broadcastOptionalDetails => 'جزئیات اختیاری'; + + @override + String get broadcastPastBroadcasts => 'پخش‌های گذشته'; + + @override + String get broadcastAllBroadcastsByMonth => 'دیدن پخش‌های هر ماه'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count پخش همگانی', + one: '$count پخش همگانی', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'پیشنهاد بازی: $param1'; @@ -388,7 +643,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'به‌خاطر داشتن درجه‌بندی $param موقت، نمی‌توانید پیشنهاد بازی دهید.'; + return 'به‌خاطر درجه‌بندی $param موقت، نمی‌توانید پیشنهاد بازی دهید.'; } @override @@ -418,10 +673,10 @@ class AppLocalizationsFa extends AppLocalizations { String get challengeDeclineCasual => 'لطفا به جایش، پیشنهاد بازی نارسمی بده.'; @override - String get challengeDeclineStandard => 'الان پیشنهاد بازی‌های شطرنج‌گونه را نمی‌پذیرم.'; + String get challengeDeclineStandard => 'اکنون پیشنهاد بازی‌های وَرتا را نمی‌پذیرم.'; @override - String get challengeDeclineVariant => 'الان مایل نیستم این شطرنج‌گونه را بازی کنم.'; + String get challengeDeclineVariant => 'اکنون مایل نیستم این وَرتا را بازی کنم.'; @override String get challengeDeclineNoBot => 'من پیشنهاد بازی از ربات‌ها را نمی‌پذیرم.'; @@ -446,11 +701,11 @@ class AppLocalizationsFa extends AppLocalizations { @override String perfStatPerfStats(String param) { - return 'وضعیت $param'; + return 'آمار $param'; } @override - String get perfStatViewTheGames => 'بازی ها را تماشا کنید'; + String get perfStatViewTheGames => 'دیدن بازی‌ها'; @override String get perfStatProvisional => 'موقت'; @@ -470,7 +725,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'مقدار کمتر به این معنی است که درجه‌بندی پایدارتر است. بالاتر از $param1، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرار گرفتن در درجه‌بندی‌ها، این مقدار باید کم‌تر از $param2 (در شطرنج استاندارد) یا $param3 (در شطرنج‌گونه‌ها) باشد.'; + return 'مقدار کمتر به معنای درجه‌بندی پایدارتر است. بالاتر از $param1، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرارگیری در درجه‌بندی‌ها، این مقدار باید کم‌تر از $param2 (در شطرنج استاندارد) یا $param3 (در وَرتاها) باشد.'; } @override @@ -559,7 +814,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesPrivacy => 'امنیت و حریم شخصی'; @override - String get preferencesNotifications => 'اعلانات'; + String get preferencesNotifications => 'اعلان'; @override String get preferencesPieceAnimation => 'حرکت مهره ها'; @@ -577,7 +832,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesBoardCoordinates => 'مختصات صفحه(A-H، 1-8)'; @override - String get preferencesMoveListWhilePlaying => 'لیست حرکات هنگام بازی کردن'; + String get preferencesMoveListWhilePlaying => 'فهرست حرکت هنگام بازی کردن'; @override String get preferencesPgnPieceNotation => 'نشانه‌گذاری حرکات'; @@ -609,6 +864,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get preferencesInGameOnly => 'تنها در بازی'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'ساعت شطرنج'; @@ -643,10 +901,10 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesBothClicksAndDrag => 'هر دو'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'پیش حرکت (بازی در نوبت حریف)'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'پیش‌حرکت (بازی در نوبت حریف)'; @override - String get preferencesTakebacksWithOpponentApproval => 'پس گرفتن حرکت (با تایید حریف)'; + String get preferencesTakebacksWithOpponentApproval => 'برگردان (با تایید حریف)'; @override String get preferencesInCasualGamesOnly => 'فقط در بازی‌های نارسمی'; @@ -706,55 +964,58 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesYourPreferencesHaveBeenSaved => 'تغییرات شما ذخیره شده است'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'اسکرول کردن روی صفحه برای مشاهده مجدد حرکت‌ها'; + String get preferencesScrollOnTheBoardToReplayMoves => 'برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید'; @override - String get preferencesCorrespondenceEmailNotification => 'ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند'; + String get preferencesCorrespondenceEmailNotification => 'فهرست رایانامهٔ روزانه از بازی‌های مکاتبه‌ای‌تان'; @override - String get preferencesNotifyStreamStart => 'استریمر شروع به فعالیت کرد'; + String get preferencesNotifyStreamStart => 'بَرخَط-محتواساز روی پخش است'; @override String get preferencesNotifyInboxMsg => 'پیام جدید'; @override - String get preferencesNotifyForumMention => 'در کامنتی نام شما ذکر شده است'; + String get preferencesNotifyForumMention => 'در انجمن از شما نام‌بُرده‌اند'; @override String get preferencesNotifyInvitedStudy => 'دعوت به مطالعه'; @override - String get preferencesNotifyGameEvent => 'اعلان به روزرسانی بازی'; + String get preferencesNotifyGameEvent => 'به‌روزرسانی‌های بازی مکاتبه‌ای'; @override String get preferencesNotifyChallenge => 'پیشنهاد بازی'; @override - String get preferencesNotifyTournamentSoon => 'تورنمت به زودی آغاز می شود'; + String get preferencesNotifyTournamentSoon => 'مسابقات به‌زودی می‌آغازد'; @override String get preferencesNotifyTimeAlarm => 'هشدار تنگی زمان'; @override - String get preferencesNotifyBell => 'زنگوله اعلانات لیچس'; + String get preferencesNotifyBell => 'اعلان زنگی در Lichess'; @override - String get preferencesNotifyPush => 'اعلانات برای زمانی که شما در لیچس نیستید'; + String get preferencesNotifyPush => 'اعلان اَفزاره، هنگامی که در Lichess نیستید'; @override String get preferencesNotifyWeb => 'مرورگر'; @override - String get preferencesNotifyDevice => 'دستگاه'; + String get preferencesNotifyDevice => 'اَفزاره'; + + @override + String get preferencesBellNotificationSound => 'صدای اعلان زنگی'; @override - String get preferencesBellNotificationSound => 'زنگ اعلان'; + String get preferencesBlindfold => 'چشم‌بسته'; @override String get puzzlePuzzles => 'معماها'; @override - String get puzzlePuzzleThemes => 'موضوع معماها'; + String get puzzlePuzzleThemes => 'موضوع معما'; @override String get puzzleRecommended => 'توصیه شده'; @@ -855,7 +1116,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleNotTheMove => 'این حرکت نیست!'; @override - String get puzzleTrySomethingElse => 'چیز دیگری پیدا کنید'; + String get puzzleTrySomethingElse => 'چیز دیگری بیابید.'; @override String puzzleRatingX(String param) { @@ -904,7 +1165,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleJumpToNextPuzzleImmediately => 'فوراً به معمای بعدی بروید'; @override - String get puzzlePuzzleDashboard => 'پیشخوان معماها'; + String get puzzlePuzzleDashboard => 'پیشخوان معما'; @override String get puzzleImprovementAreas => 'نقاط ضعف'; @@ -913,7 +1174,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleStrengths => 'نقاط قوت'; @override - String get puzzleHistory => 'پیشینه معماها'; + String get puzzleHistory => 'پیشینهٔ معما'; @override String get puzzleSolved => 'حل شده'; @@ -950,10 +1211,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get puzzleSearchPuzzles => 'جستجوی معماها'; + String get puzzleSearchPuzzles => 'جستجوی معما'; @override - String get puzzleFromMyGamesNone => 'شما هیچ معمایی در پایگاه‌داده ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بَختِتان را برای افزودن معمایی از خودتان بیفزایید!'; + String get puzzleFromMyGamesNone => 'شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و فکری را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!'; @override String puzzleFromXGamesFound(String param1, String param2) { @@ -961,7 +1222,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get puzzlePuzzleDashboardDescription => 'تمرین کن، تحلیل کن، پیشرفت کن'; + String get puzzlePuzzleDashboardDescription => 'آموزش، واکاوی، بهبود'; @override String puzzlePercentSolved(String param) { @@ -972,7 +1233,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleNoPuzzlesToShow => 'چیزی برای نمایش نیست، نخست بروید و چند معما حل کنید!'; @override - String get puzzleImprovementAreasDescription => 'این‌ها را تمرین کنید تا روند پیشرفت خود را بهبود ببخشید!'; + String get puzzleImprovementAreasDescription => 'برای بهینیدن پیشرفت‌تان، این‌ها را بیاموزید!'; @override String get puzzleStrengthDescription => 'شما در این زمینه‌ها بهترین عملکرد را دارید'; @@ -1120,7 +1381,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeEquality => 'برابری'; @override - String get puzzleThemeEqualityDescription => 'از وضعیت باخت در‌آیید و به وضعیت تساوی یا ترازمند برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)'; + String get puzzleThemeEqualityDescription => 'از وضعیت باخت در‌آیید و به وضعیت تساوی یا تعادل برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)'; @override String get puzzleThemeKingsideAttack => 'حمله به جناح شاه'; @@ -1366,7 +1627,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeTrappedPieceDescription => 'یک مهره قادر به فرار کردن از زده شدن نیست چون حرکات محدودی دارد.'; @override - String get puzzleThemeUnderPromotion => 'فرو-ارتقا'; + String get puzzleThemeUnderPromotion => 'کم‌ارتقا'; @override String get puzzleThemeUnderPromotionDescription => 'ارتقا به اسب، فیل یا رخ.'; @@ -1375,7 +1636,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeVeryLong => 'معمای خیلی طولانی'; @override - String get puzzleThemeVeryLongDescription => 'چهار حرکت یا بیشتر برای برنده شدن.'; + String get puzzleThemeVeryLongDescription => 'بُردن با چهار حرکت یا بیشتر.'; @override String get puzzleThemeXRayAttack => 'حمله پیکانی'; @@ -1390,10 +1651,10 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'حریف در حرکت‌هایش محدود است و همه‌شان وضعیتش را بدتر می‌کند.'; @override - String get puzzleThemeHealthyMix => 'ترکیب سالم'; + String get puzzleThemeMix => 'آمیزهٔ همگن'; @override - String get puzzleThemeHealthyMixDescription => 'یک ذره از همه چیز. شما نمی دانید چه چیزی پیش روی شماست، بنابراین شما باید برای هر چیزی آماده باشید! دقیقا مثل بازی های واقعی.'; + String get puzzleThemeMixDescription => 'کمی از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.'; @override String get puzzleThemePlayerGames => 'بازی‌های بازیکن'; @@ -1416,7 +1677,7 @@ class AppLocalizationsFa extends AppLocalizations { String get settingsCloseAccount => 'بستن حساب'; @override - String get settingsManagedAccountCannotBeClosed => 'اکانت شما مدیریت شده است و نمی تواند بسته شود.'; + String get settingsManagedAccountCannotBeClosed => 'حساب‌تان مدیریت می‌شود و نمی‌توان آن را بست.'; @override String get settingsClosingIsDefinitive => 'بعد از بستن حسابتان دیگر نمی توانید به آن دسترسی پیدا کنید. آیا مطمئن هستید؟'; @@ -1425,7 +1686,7 @@ class AppLocalizationsFa extends AppLocalizations { String get settingsCantOpenSimilarAccount => 'شما نمی توانید حساب جدیدی با این نام کاربری باز کنید، حتی اگر با دستگاه دیگری وارد شوید.'; @override - String get settingsChangedMindDoNotCloseAccount => 'نظرم را عوض کردم اکانتم را نمی بندم'; + String get settingsChangedMindDoNotCloseAccount => 'نظرم عوض شد، حسابم را نبند'; @override String get settingsCloseAccountExplanation => 'آیا مطمئنید که می خواهید حساب خود را ببندید؟ بستن حساب یک تصمیم دائمی است. شما هرگز نمی توانید دوباره وارد حساب خود شوید.'; @@ -1449,7 +1710,7 @@ class AppLocalizationsFa extends AppLocalizations { String get waitingForOpponent => 'انتطار برای حریف'; @override - String get orLetYourOpponentScanQrCode => 'یا اجازه دهید حریف شما این QR کد را پویش کند'; + String get orLetYourOpponentScanQrCode => 'یا اجازه دهید حریف‌تان این کد QR را بِروبینَد'; @override String get waiting => 'در حال انتظار'; @@ -1523,13 +1784,13 @@ class AppLocalizationsFa extends AppLocalizations { String get kingInTheCenter => 'شاه روی تپه'; @override - String get threeChecks => 'سه کیش'; + String get threeChecks => 'سه‌کیش'; @override String get raceFinished => 'مسابقه تمام شد'; @override - String get variantEnding => 'پایان شطرنج‌گونه'; + String get variantEnding => 'پایان وَرتا'; @override String get newOpponent => 'حریف جدید'; @@ -1574,10 +1835,10 @@ class AppLocalizationsFa extends AppLocalizations { String get blackLeftTheGame => 'سیاه بازی را ترک کرد'; @override - String get whiteDidntMove => 'سفید تکان نخورد'; + String get whiteDidntMove => 'سفید بازی نکرد'; @override - String get blackDidntMove => 'مشکی تکان نخورد'; + String get blackDidntMove => 'سیاه بازی نکرد'; @override String get requestAComputerAnalysis => 'درخواست تحلیل رایانه‌ای'; @@ -1603,13 +1864,13 @@ class AppLocalizationsFa extends AppLocalizations { String get usingServerAnalysis => 'با استفاده از کارسازِ تحلیل'; @override - String get loadingEngine => 'پردازشگر بارمی‌گذارد...'; + String get loadingEngine => 'موتور بارمی‌گذارد...'; @override - String get calculatingMoves => 'در حال محاسبه حرکات...'; + String get calculatingMoves => 'محاسبهٔ حرکت‌ها...'; @override - String get engineFailed => 'خطا در بارگذاری پردازشگر شطرنج'; + String get engineFailed => 'خطا در بارگذاری موتور'; @override String get cloudAnalysis => 'تحلیل ابری'; @@ -1627,7 +1888,7 @@ class AppLocalizationsFa extends AppLocalizations { String get toggleLocalEvaluation => 'کلید ارزیابی محلی'; @override - String get promoteVariation => 'افزایش عمق شاخه اصلی'; + String get promoteVariation => 'ارتقای وَرتِش'; @override String get makeMainLine => 'خط کنونی را به خط اصلی تبدیل کنید'; @@ -1636,25 +1897,25 @@ class AppLocalizationsFa extends AppLocalizations { String get deleteFromHere => 'از اینجا به بعد را پاک کنید'; @override - String get collapseVariations => 'بستن شاخه‌ها'; + String get collapseVariations => 'بستن وَرتِش‌ها'; @override - String get expandVariations => 'باز کردن شاخه‌ها'; + String get expandVariations => 'گستردنِ وَرتِش‌ها'; @override - String get forceVariation => 'نتیجه تحلیل را به عنوان یکی از تنوعهای بازی انتخاب نمایید'; + String get forceVariation => 'وَرتِشِ زوری'; @override - String get copyVariationPgn => 'کپی PGN این شاخه'; + String get copyVariationPgn => 'رونوشت‌گیری PGN ِ وَرتِش'; @override String get move => 'حرکت'; @override - String get variantLoss => 'حرکت بازنده'; + String get variantLoss => 'باختِ وَرتا'; @override - String get variantWin => 'حرکت برنده'; + String get variantWin => 'بُردِ وَرتا'; @override String get insufficientMaterial => 'مُهره ناکافی برای مات'; @@ -1669,7 +1930,7 @@ class AppLocalizationsFa extends AppLocalizations { String get close => 'بستن'; @override - String get winning => 'حرکت پیروزی‌بخش'; + String get winning => 'حرکت برنده'; @override String get losing => 'حرکت بازنده'; @@ -1712,7 +1973,7 @@ class AppLocalizationsFa extends AppLocalizations { String get maxDepthReached => 'عمق به حداکثر رسیده!'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'ممکنه بازی‌های بیشتری با توجه به گزینگان ترجیح‌ها، وجود داشته باشد؟'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'شاید بازی‌های بیشتری با توجه به نام‌چین تنظیمات، وجود داشته باشد.'; @override String get openings => 'گشایش‌ها'; @@ -1725,14 +1986,14 @@ class AppLocalizationsFa extends AppLocalizations { @override String xOpeningExplorer(String param) { - return 'جستجوگر گشایش $param'; + return 'پویشگر گشایش $param'; } @override String get playFirstOpeningEndgameExplorerMove => 'نخستین حرکت گشایش/آخربازی پویشگر را برو'; @override - String get winPreventedBy50MoveRule => 'قانون پنجاه حرکت از پیروزی جلوگیری کرد'; + String get winPreventedBy50MoveRule => 'قانون پنجاه حرکت جلوی پیروزی را گرفت'; @override String get lossSavedBy50MoveRule => 'قانون ۵۰ حرکت از شکست جلوگیری کرد'; @@ -1765,19 +2026,16 @@ class AppLocalizationsFa extends AppLocalizations { String get realtimeReplay => 'مشابه بازی'; @override - String get byCPL => 'درنگ حین اشتباهات'; - - @override - String get openStudy => 'گشودن مطالعه'; + String get byCPL => 'درنگ هنگام اشتباه'; @override String get enable => 'فعال سازی'; @override - String get bestMoveArrow => 'فلش نشان دهنده بهترین حرکت'; + String get bestMoveArrow => 'پیکانِ بهترین حرکت'; @override - String get showVariationArrows => 'نمایش پیکان‌های شاخه اصلی'; + String get showVariationArrows => 'نمایش پیکان‌های وَرتِش'; @override String get evaluationGauge => 'میله ارزیابی'; @@ -1792,14 +2050,11 @@ class AppLocalizationsFa extends AppLocalizations { String get memory => 'حافظه'; @override - String get infiniteAnalysis => 'آنالیز بی پایان'; + String get infiniteAnalysis => 'تحلیل بی‌کران'; @override String get removesTheDepthLimit => 'محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند'; - @override - String get engineManager => 'مدیر موتور شطرنج'; - @override String get blunder => 'اشتباه فاحش'; @@ -1816,7 +2071,7 @@ class AppLocalizationsFa extends AppLocalizations { String get flipBoard => 'چرخاندن صفحه'; @override - String get threefoldRepetition => 'تکرار سه گانه'; + String get threefoldRepetition => 'تکرار سه‌گانه'; @override String get claimADraw => 'ادعای تساوی'; @@ -1834,7 +2089,7 @@ class AppLocalizationsFa extends AppLocalizations { String get fiftyMovesWithoutProgress => 'قانون ۵۰ حرکت'; @override - String get currentGames => 'بازی های در جریان'; + String get currentGames => 'بازی‌های جاری'; @override String get viewInFullSize => 'نمایش در اندازه کامل'; @@ -1849,13 +2104,13 @@ class AppLocalizationsFa extends AppLocalizations { String get rememberMe => 'مرا به خاطر بسپار'; @override - String get youNeedAnAccountToDoThat => 'شما برای انجام این کار به یک حساب کاربری نیاز دارید.'; + String get youNeedAnAccountToDoThat => 'برای انجام آن به یک حساب نیازمندید'; @override String get signUp => 'نام نویسی'; @override - String get computersAreNotAllowedToPlay => 'كامپيوتر و بازيكناني كه از كامپيوتر كمك مي گيرند،مجاز به بازی نیستند.لطفا از انجین شطرنج يا دیتابیس شطرنج و يا بازيكنان ديگر كمک نگيريد. همچنین توجه کنید که داشتن چند حساب کاربری به شدت نهی شده است. استفاده فزاینده از چند حساب منجر به مسدود شدن حساب شما خواهد شد.'; + String get computersAreNotAllowedToPlay => 'رایانه ها و بازیکنان رایانه-یاریده، مجاز به بازی نیستند. لطفا هنگام بازی از موتورهای شطرنج، دادگان‌ها یا دیگر بازیکنان کمک نگیرید. همچنین توجه کنید که ساخت چندین حساب به شدت ممنوع است و چند حسابی افزاینده، منجر به بستن‌تان می‌شود.'; @override String get games => 'بازی ها'; @@ -1865,11 +2120,11 @@ class AppLocalizationsFa extends AppLocalizations { @override String xPostedInForumY(String param1, String param2) { - return '$param1 در انجمن،موضوع $param2 را پست کرد.'; + return '$param1 در موضوع $param2، پیامی نوشت'; } @override - String get latestForumPosts => 'آخرین پست های انجمن'; + String get latestForumPosts => 'آخرین فرسته‌های انجمن'; @override String get players => 'بازیکنان'; @@ -1890,13 +2145,13 @@ class AppLocalizationsFa extends AppLocalizations { String get yesterday => 'دیروز'; @override - String get minutesPerSide => 'زمان برای هر بازیکن(به دقیقه)'; + String get minutesPerSide => 'هر بازیکن چند دقیقه'; @override - String get variant => 'گونه'; + String get variant => 'وَرتا'; @override - String get variants => 'گونه‌ها'; + String get variants => 'وَرتا'; @override String get timeControl => 'زمان'; @@ -1926,7 +2181,7 @@ class AppLocalizationsFa extends AppLocalizations { String get username => 'نام کاربری'; @override - String get usernameOrEmail => 'نام کاربری یا ایمیل'; + String get usernameOrEmail => 'نام کاربری یا رایانامه'; @override String get changeUsername => 'تغییر نام کاربری'; @@ -1941,13 +2196,13 @@ class AppLocalizationsFa extends AppLocalizations { String get signupUsernameHint => 'مطمئن شوید که یک نام کاربری مناسب انتخاب میکنید. بعداً نمی توانید آن را تغییر دهید و هر حسابی با نام کاربری نامناسب بسته می شود!'; @override - String get signupEmailHint => 'ما فقط از آن برای تنظیم مجدد رمز عبور استفاده خواهیم کرد.'; + String get signupEmailHint => 'ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.'; @override - String get password => 'رمز عبور'; + String get password => 'گذرواژه'; @override - String get changePassword => 'تغییر کلمه عبور'; + String get changePassword => 'تغییر گذرواژه'; @override String get changeEmail => 'تغییر ایمیل'; @@ -1956,25 +2211,25 @@ class AppLocalizationsFa extends AppLocalizations { String get email => 'ایمیل'; @override - String get passwordReset => 'بازیابی کلمه عبور'; + String get passwordReset => 'بازنشانی گذرواژه'; @override - String get forgotPassword => 'آیا کلمه عبور را فراموش کرده اید؟'; + String get forgotPassword => 'گذرواژه را فراموش کرده‌اید؟'; @override - String get error_weakPassword => 'این رمز به شدت معمول و قابل حدس است.'; + String get error_weakPassword => 'این گذرواژه بسیار رایج و آسان‌حدس است.'; @override - String get error_namePassword => 'لطفا رمز خود را متفاوت از نام کاربری خود انتخاب کنید.'; + String get error_namePassword => 'خواهشانه از نام کاربری‌تان برای گذرواژه‌تان استفاده نکنید.'; @override - String get blankedPassword => 'شما از همین رمز عبور در سایت دیگری استفاده کرده اید و آن سایت در معرض خطر قرار گرفته است. برای اطمینان از ایمنی حساب لیچس خود، لازم است که شما یک رمز عبور جدید ایجاد کنید. ممنون از همکاری شما.'; + String get blankedPassword => 'شما از گذرواژهٔ یکسانی در وبگاه دیگری بهراییده‌اید و آن وبگاه به خطر افتاده است. برای اطمینان از ایمنی حساب Lichessتان، به شما نیاز داریم تا گذرواژهٔ نویی را تعیین کنید. از درک‌تان سپاسگزاریم.'; @override String get youAreLeavingLichess => 'در حال ترک lichess هستید'; @override - String get neverTypeYourPassword => 'هرگز رمز خود را در سایت دیگر وارد نکنید!'; + String get neverTypeYourPassword => 'هرگز گذرواژهٔ Lichessتان را در وبگاه دیگری ننویسید!'; @override String proceedToX(String param) { @@ -1991,7 +2246,7 @@ class AppLocalizationsFa extends AppLocalizations { String get emailConfirmHelp => 'کمک با تائید ایمیل'; @override - String get emailConfirmNotReceived => 'آیا ایمیل تائید بعد از ثبت نام را دریافت نکرده اید؟'; + String get emailConfirmNotReceived => 'آیا رایانامهٔ تاییدتان را پس از نام‌نویسی دریافت نکردید؟'; @override String get whatSignupUsername => 'از چه نام کاربری برای ثبت نام استفاده کردید؟'; @@ -2061,7 +2316,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get gamesPlayed => 'تعداد بازی های انجام شده'; + String get gamesPlayed => 'بازی‌های انجامیده'; + + @override + String get ok => 'بسیار خب'; @override String get cancel => 'لغو'; @@ -2103,10 +2361,10 @@ class AppLocalizationsFa extends AppLocalizations { String get decline => 'رد کردن'; @override - String get playingRightNow => 'بازی در حال انجام'; + String get playingRightNow => 'هم‌اکنون بازی می‌کنند'; @override - String get eventInProgress => 'بازی در حال انجام'; + String get eventInProgress => 'اکنون بازی می‌کنند'; @override String get finished => 'تمام شده'; @@ -2184,7 +2442,7 @@ class AppLocalizationsFa extends AppLocalizations { String get youHaveBeenTimedOut => 'زمان شما به پایان رسید.'; @override - String get spectatorRoom => 'اتاق تماشاچیان'; + String get spectatorRoom => 'اتاق تماشاگران'; @override String get composeMessage => 'نوشتن پیام'; @@ -2214,28 +2472,28 @@ class AppLocalizationsFa extends AppLocalizations { String get openingExplorerAndTablebase => 'پویشگر گشایش و آخربازی'; @override - String get takeback => 'پس گرفتن حرکت'; + String get takeback => 'برگردان'; @override - String get proposeATakeback => 'پیشنهاد پس گرفتن حرکت'; + String get proposeATakeback => 'پیشنهاد برگردان'; @override - String get takebackPropositionSent => 'پیشنهاد پس گرفتن حرکت فرستاده شد'; + String get takebackPropositionSent => 'برگردان فرستاده شد'; @override - String get takebackPropositionDeclined => 'پیشنهاد پس گرفتن حرکت رد شد'; + String get takebackPropositionDeclined => 'برگردان رد شد'; @override - String get takebackPropositionAccepted => 'پیشنهاد پس گرفتن حرکت پذیرفته شد'; + String get takebackPropositionAccepted => 'برگردان پذیرفته شد'; @override - String get takebackPropositionCanceled => 'پیشنهاد پس گرفتن حرکت لغو شد'; + String get takebackPropositionCanceled => 'برگردان لغو شد'; @override - String get yourOpponentProposesATakeback => 'حریف پیشنهاد پس گرفتن حرکت می دهد'; + String get yourOpponentProposesATakeback => 'حریف‌تان پیشنهاد «برگرداندن» می‌دهد'; @override - String get bookmarkThisGame => 'نشان گذاری بازی'; + String get bookmarkThisGame => 'نشانک‌گذاری'; @override String get tournament => 'مسابقه'; @@ -2247,10 +2505,10 @@ class AppLocalizationsFa extends AppLocalizations { String get tournamentPoints => 'مجموع امتیازات مسابقات'; @override - String get viewTournament => 'مشاهده مسابقات'; + String get viewTournament => 'دیدن مسابقات'; @override - String get backToTournament => 'برگشت به مسابقه'; + String get backToTournament => 'بازگشت به مسابقات'; @override String get noDrawBeforeSwissLimit => 'شما نمی‌توانید در مسابقات سوییس تا قبل از حرکت ۳۰ام بازی را مساوی کنید.'; @@ -2302,7 +2560,7 @@ class AppLocalizationsFa extends AppLocalizations { String get backToGame => 'بازگشت به بازی'; @override - String get siteDescription => 'کارساز برخط و رایگان شطرنج. با میانایی روان شطرنج بازی کنید. بدون ثبت‌نام، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.'; + String get siteDescription => 'کارساز برخط و رایگان شطرنج. با میانایی روان، شطرنج بازی کنید. بدون نام‌نویسی، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.'; @override String xJoinedTeamY(String param1, String param2) { @@ -2315,11 +2573,11 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get startedStreaming => 'پخش را آغازید'; + String get startedStreaming => 'جریان‌سازی را آغازید'; @override String xStartedStreaming(String param) { - return '$param پخش را آغازید'; + return '$param جریان‌سازی را آغازید'; } @override @@ -2350,7 +2608,7 @@ class AppLocalizationsFa extends AppLocalizations { String get gameAsGIF => 'بارگیری GIF بازی'; @override - String get pasteTheFenStringHere => 'پوزیشن دلخواه(FEN) را در این قسمت وارد کنید'; + String get pasteTheFenStringHere => 'رشته FEN را در این قسمت قرار دهید'; @override String get pasteThePgnStringHere => 'متن PGN را در این قسمت وارد کنید'; @@ -2374,7 +2632,7 @@ class AppLocalizationsFa extends AppLocalizations { String get importGameExplanation => 'برای دریافت بازپخش مرورپذیر، واکاوی رایانه‌ای، گپ‌های بازی، و وب‌نشانی همگانی همرسانی‌پذیر، PGN یک بازی را جای‌گذاری کنید.'; @override - String get importGameCaveat => 'تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید.'; + String get importGameCaveat => 'ورتش‌ها پاک خواهند شد. برای حفظشان، PGN را از طریق مطالعه درون‌بَرید.'; @override String get importGameDataPrivacyWarning => 'این PGN برای عموم در دسترس است، برای وارد کردن یک بازی خصوصی، از *مطالعه* استفاده کنید.'; @@ -2401,7 +2659,7 @@ class AppLocalizationsFa extends AppLocalizations { String get retry => 'تلاش دوباره'; @override - String get reconnecting => 'در حال اتصال دوباره'; + String get reconnecting => 'بازاتصال...'; @override String get noNetwork => 'بُرون‌خط'; @@ -2410,22 +2668,22 @@ class AppLocalizationsFa extends AppLocalizations { String get favoriteOpponents => 'رقبای مورد علاقه'; @override - String get follow => 'دنبال کردن'; + String get follow => 'دنبالیدن'; @override - String get following => 'افرادی که دنبال می کنید'; + String get following => 'دنبال‌شدگان'; @override - String get unfollow => 'لغو دنبال کردن'; + String get unfollow => 'وادنبالیدن'; @override String followX(String param) { - return 'دنبال کردن $param'; + return 'دنبالیدن $param'; } @override String unfollowX(String param) { - return 'لغو دنبال کردن $param'; + return 'وادنبالیدن $param'; } @override @@ -2437,12 +2695,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get unblock => 'لغو انسداد'; - @override - String get followsYou => 'افرادی که شما را دنبال می کنند'; - @override String xStartedFollowingY(String param1, String param2) { - return '$param1 $param2 را فالو کرد'; + return '$param1 دنبالیدن $param2 را آغازید'; } @override @@ -2478,13 +2733,13 @@ class AppLocalizationsFa extends AppLocalizations { String get winner => 'برنده'; @override - String get standing => 'رتبه بندی'; + String get standing => 'رده‌بندی'; @override - String get createANewTournament => 'درست کردن یک مسابقه ی جدید'; + String get createANewTournament => 'ایجاد یک مسابقهٔ نو'; @override - String get tournamentCalendar => 'برنامه ی مسابقات'; + String get tournamentCalendar => 'گاهشمار مسابقات'; @override String get conditionOfEntry => 'شرایط ورود:'; @@ -2499,7 +2754,7 @@ class AppLocalizationsFa extends AppLocalizations { String get inappropriateNameWarning => 'هرچیز حتی کمی نامناسب ممکن است باعث بسته شدن حساب کاربری شما بشود.'; @override - String get emptyTournamentName => 'این مکان را خالی بگذارید تا به صورت تصادفی اسم یک استاد بزرگ برای مسابقات انتخاب شود.'; + String get emptyTournamentName => 'برای نامیدن مسابقات به نام یک شطرنج‌باز برجسته، خالی بگذارید.'; @override String get makePrivateTournament => 'تورنومنت را به حالت خصوصی در بیاورید و دسترسی را محدود به داشتن پسورد کنید'; @@ -2540,13 +2795,13 @@ class AppLocalizationsFa extends AppLocalizations { String get resume => 'ادامه دادن'; @override - String get youArePlaying => 'شما بازی میکنید!'; + String get youArePlaying => 'شما بازی می‌کنید!'; @override String get winRate => 'درصد برد'; @override - String get berserkRate => 'میزان جنون'; + String get berserkRate => 'میزان دیوانگی'; @override String get performance => 'عملکرد'; @@ -2561,7 +2816,7 @@ class AppLocalizationsFa extends AppLocalizations { String get whiteWins => 'پیروزی با مهره سفید'; @override - String get blackWins => 'سیاه برنده شد'; + String get blackWins => 'سیاه می‌برد'; @override String get drawRate => 'نرخ تساوی'; @@ -2581,7 +2836,7 @@ class AppLocalizationsFa extends AppLocalizations { String get boardEditor => 'مُهره‌چینی'; @override - String get setTheBoard => 'مهره‌ها را بچینید'; + String get setTheBoard => 'میز را بچینید'; @override String get popularOpenings => 'گشایش‌های محبوب'; @@ -2682,13 +2937,13 @@ class AppLocalizationsFa extends AppLocalizations { String get success => 'موفق شدید'; @override - String get automaticallyProceedToNextGameAfterMoving => 'حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن'; + String get automaticallyProceedToNextGameAfterMoving => 'پس از حرکت، خودکار به بازی بعدی روید'; @override String get autoSwitch => 'تعویض خودکار'; @override - String get puzzles => 'معماها'; + String get puzzles => 'معما'; @override String get onlineBots => 'ربات‌های بَرخط'; @@ -2703,7 +2958,7 @@ class AppLocalizationsFa extends AppLocalizations { String get descPrivate => 'توضیحات خصوصی'; @override - String get descPrivateHelp => 'متنی که فقط اعضای تیم مشاهده خواهند کرد. در صورت تعیین، جایگزین توضیحات عمومی برای اعضای تیم خواهد شد.'; + String get descPrivateHelp => 'متنی که فقط هم‌تیمی‌ها خواهند دید. در صورت تعیین، جایگزین وصف همگانی برای هم‌تیمی‌ها می‌شود خواهد شد.'; @override String get no => 'نه'; @@ -2727,10 +2982,10 @@ class AppLocalizationsFa extends AppLocalizations { String get topics => 'مباحث'; @override - String get posts => 'پست ها'; + String get posts => 'فرسته‌ها'; @override - String get lastPost => 'آخرین ارسال'; + String get lastPost => 'آخرین فرسته'; @override String get views => 'نمایش ها'; @@ -2769,17 +3024,23 @@ class AppLocalizationsFa extends AppLocalizations { String get troll => 'وِزُل'; @override - String get other => 'موضوعات دیگر'; + String get other => 'دیگر'; @override - String get reportDescriptionHelp => 'لینک بازی های این کاربر را قرار دهید و توضیع دهید خطای رفتار این بازیکن چه بوده است'; + String get reportCheatBoostHelp => 'پیوند بازی(ها) را جای‌گذارید و بشرحید که چه رفتاری از این کاربر مشکل دارد. فقط نگویید «آنها تقلب‌کارند»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید.'; + + @override + String get reportUsernameHelp => 'بشرحید چه چیز این نام‌کاربری آزارنده است. فقط نگویید «آزارنده/نامناسب است»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید، به‌ویژه اگر توهین: گنگ است، انگلیسی نیست، کوچه‌بازاری است، یا یک ارجاع تاریخی/فرهنگی است.'; + + @override + String get reportProcessedFasterInEnglish => 'اگر انگلیسی بنویسید، زودتر به گزارش‌تان رسیدگی خواهد شد.'; @override String get error_provideOneCheatedGameLink => 'لطفآ حداقل یک نمونه تقلب در بازی را مطرح کنید.'; @override String by(String param) { - return 'توسط $param'; + return 'به‌دستِ $param'; } @override @@ -2791,46 +3052,46 @@ class AppLocalizationsFa extends AppLocalizations { String get thisTopicIsNowClosed => 'این موضوع بسته شده است'; @override - String get blog => 'بلاگ'; + String get blog => 'وبنوشت'; @override - String get notes => 'یادداشت ها'; + String get notes => 'یادداشت‌ها'; @override - String get typePrivateNotesHere => 'یادداشت خصوصی را اینجا وارد کنید'; + String get typePrivateNotesHere => 'یادداشت‌های خصوصی را اینجا بنویسید'; @override String get writeAPrivateNoteAboutThisUser => 'یک یادداشت خصوصی درباره این کاربر بنویسید'; @override - String get noNoteYet => 'تا الان، بدون یادداشت'; + String get noNoteYet => 'تاکنون، بدون یادداشت'; @override - String get invalidUsernameOrPassword => 'نام کاربری یا رمز عبور نادرست است'; + String get invalidUsernameOrPassword => 'نام کاربری یا گذرواژهٔ نامعتبر'; @override - String get incorrectPassword => 'رمزعبور اشتباه'; + String get incorrectPassword => 'گذرواژهٔ نادرست'; @override - String get invalidAuthenticationCode => 'کد اصالت سنجی نامعتبر'; + String get invalidAuthenticationCode => 'کد راستین‌آزمایی نامعتبر'; @override String get emailMeALink => 'یک لینک به من ایمیل کنید'; @override - String get currentPassword => 'رمز جاری'; + String get currentPassword => 'گذرواژهٔ جاری'; @override - String get newPassword => 'رمز جدید'; + String get newPassword => 'گذرواژهٔ نو'; @override - String get newPasswordAgain => '(رمز جدید(برای دومین بار'; + String get newPasswordAgain => 'گذرواژهٔ نو (دوباره)'; @override - String get newPasswordsDontMatch => 'کلمه‌های عبور وارد شده مطابقت ندارند'; + String get newPasswordsDontMatch => 'گذرواژه‌های نو هم‌جور نیستند'; @override - String get newPasswordStrength => 'استحکام کلمه عبور'; + String get newPasswordStrength => 'نیرومندی گذرواژه'; @override String get clockInitialTime => 'مقدار زمان اولیه'; @@ -2845,7 +3106,7 @@ class AppLocalizationsFa extends AppLocalizations { String get privacyPolicy => 'سیاست حریم شخصی'; @override - String get letOtherPlayersFollowYou => 'بقیه بازیکنان شما را دنبال کنند'; + String get letOtherPlayersFollowYou => 'اجازه دهید دیگر بازیکنان شما را بدنبالند'; @override String get letOtherPlayersChallengeYou => 'اجازه دهید بازیکنان دیگر به شما پیشنهاد بازی دهند'; @@ -2916,7 +3177,7 @@ class AppLocalizationsFa extends AppLocalizations { String get timeline => 'جدول زمانی'; @override - String get starting => 'شروع'; + String get starting => 'آغاز:'; @override String get allInformationIsPublicAndOptional => 'تمامی اطلاعات عمومی و اختیاری است.'; @@ -2946,13 +3207,13 @@ class AppLocalizationsFa extends AppLocalizations { String get learnMenu => 'یادگیری'; @override - String get studyMenu => 'مطالعه‌ها'; + String get studyMenu => 'مطالعه'; @override - String get practice => 'تمرین کردن'; + String get practice => 'تمرین'; @override - String get community => 'انجمن'; + String get community => 'همدارگان'; @override String get tools => 'ابزارها'; @@ -2985,7 +3246,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String error_maxLength(String param) { - return 'باید حداقل دارای $param حرف باشد'; + return 'باید حداکثر $param نویسه داشته باشد'; } @override @@ -3013,7 +3274,7 @@ class AppLocalizationsFa extends AppLocalizations { String get onlyFriends => 'فقط دوستان'; @override - String get menu => 'فهرست'; + String get menu => 'نام‌چین'; @override String get castling => 'قلعه‌روی'; @@ -3030,21 +3291,21 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get watchGames => 'تماشای بازی ها'; + String get watchGames => 'تماشای بازی‌ها'; @override String tpTimeSpentOnTV(String param) { - return 'مدت گذرانده در تلویزیون: $param'; + return 'مدت آرنگیده در تلویزیون: $param'; } @override - String get watch => 'نگاه کردن'; + String get watch => 'تماشا'; @override - String get videoLibrary => 'فیلم ها'; + String get videoLibrary => 'فیلم‌ها'; @override - String get streamersMenu => 'بَرخَط-محتواسازها'; + String get streamersMenu => 'بَرخَط-محتواسازان'; @override String get mobileApp => 'گوشی‌افزار'; @@ -3075,7 +3336,7 @@ class AppLocalizationsFa extends AppLocalizations { String get termsOfService => 'قوانین'; @override - String get sourceCode => 'منبع کد لایچس'; + String get sourceCode => 'کد منبع'; @override String get simultaneousExhibitions => 'نمایش هم زمان'; @@ -3122,7 +3383,7 @@ class AppLocalizationsFa extends AppLocalizations { String get aboutSimulRules => 'وقتی نمایش همزمان شروع شود، هر بازیکن یک بازی را با میزبان که با مهره سفید بازی میکند آغاز میکند. نمایش وقتی تمام می شود که تمام بازی ها تمام شده باشند.'; @override - String get aboutSimulSettings => 'نمایش های همزمان همیشه غیر رسمی هستند. بازی دوباره، پس گرفتن حرکت و اضافه کردن زمان غیرفعال شده اند.'; + String get aboutSimulSettings => 'نمایشگاه همزمان همیشه نارسمی است. بازرویارویی، برگرداندن و زمان افزاینده نافعال شده‌اند.'; @override String get create => 'ساختن'; @@ -3131,7 +3392,7 @@ class AppLocalizationsFa extends AppLocalizations { String get whenCreateSimul => 'وقتی یک نمایش همزمان ایجاد میکنید باید با چند نفر همزمان بازی کنید.'; @override - String get simulVariantsHint => 'اگر چندین گونه را انتخاب کنید، هر بازیکن می‌تواند انتخاب کند که کدام یک را بازی کند.'; + String get simulVariantsHint => 'اگر چندین وَرتا را برگزینید، هر بازیکن می‌تواند انتخاب کند که کدام‌یک را بازی کند.'; @override String get simulClockHint => 'تنظیم ساعت فیشر. هرچه از بازیکنان بیشتری برنده شوید، زمان بیشتری نیاز دارید'; @@ -3149,10 +3410,10 @@ class AppLocalizationsFa extends AppLocalizations { String get simulHostExtraTimePerPlayer => 'زمان اضافه میزبان به ازای بازیکن'; @override - String get lichessTournaments => 'مسابقات لی چس'; + String get lichessTournaments => 'مسابقات Lichess'; @override - String get tournamentFAQ => 'سوالات متداول مسابقات'; + String get tournamentFAQ => 'پرسش‌های پربسامد مسابقات راوان'; @override String get timeBeforeTournamentStarts => 'زمان باقی مانده به شروع مسابقه'; @@ -3164,7 +3425,7 @@ class AppLocalizationsFa extends AppLocalizations { String get accuracy => 'دقت'; @override - String get keyboardShortcuts => 'میانبر های صفحه کلید'; + String get keyboardShortcuts => 'میانبرهای صفحه‌کلید'; @override String get keyMoveBackwardOrForward => 'حرکت به عقب/جلو'; @@ -3173,19 +3434,19 @@ class AppLocalizationsFa extends AppLocalizations { String get keyGoToStartOrEnd => 'رفتن به آغاز/پایان'; @override - String get keyCycleSelectedVariation => 'چرخه شاخه اصلی انتخاب‌شده'; + String get keyCycleSelectedVariation => 'چرخاندن وَرتِش گزیده'; @override - String get keyShowOrHideComments => 'نمایش/ پنهان کردن نظرات'; + String get keyShowOrHideComments => 'نمایش/پنهان کردن نظرها'; @override - String get keyEnterOrExitVariation => 'ورود / خروج به شاخه'; + String get keyEnterOrExitVariation => 'ورود/خروج به وَرتِش'; @override String get keyRequestComputerAnalysis => 'درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید'; @override - String get keyNextLearnFromYourMistakes => 'بعدی (از اشتباهات خود درس بگیرید)'; + String get keyNextLearnFromYourMistakes => 'بعدی (از اشتباه‌های‌تان بیاموزید)'; @override String get keyNextBlunder => 'اشتباه فاحش بعدی'; @@ -3203,10 +3464,10 @@ class AppLocalizationsFa extends AppLocalizations { String get keyNextBranch => 'شاخه بعدی'; @override - String get toggleVariationArrows => 'کلید پیکان‌های شاخه اصلی'; + String get toggleVariationArrows => 'کلید پیکان‌های وَرتِش'; @override - String get cyclePreviousOrNextVariation => 'چرخه پیشین/پسین شاخه اصلی'; + String get cyclePreviousOrNextVariation => 'چرخاندن پیشین/پسین وَرتِش'; @override String get toggleGlyphAnnotations => 'کلید علائم حرکت‌نویسی'; @@ -3215,7 +3476,7 @@ class AppLocalizationsFa extends AppLocalizations { String get togglePositionAnnotations => 'تغییر حرکت‌نویسی وضعیت'; @override - String get variationArrowsInfo => 'پیکان های شاخه اصلی به شما امکان می‌دهد بدون استفاده از فهرست حرکت، پیمایش کنید.'; + String get variationArrowsInfo => 'پیکان های وَرتِش به شما امکان ناوِش بدون استفاده از فهرستِ حرکت را می‌دهد.'; @override String get playSelectedMove => 'حرکت انتخاب شده را بازی کن'; @@ -3224,10 +3485,10 @@ class AppLocalizationsFa extends AppLocalizations { String get newTournament => 'مسابقه جدید'; @override - String get tournamentHomeTitle => 'مسابقات شطرنج با گونه‌ها و زمان‌بندی‌های مختلف'; + String get tournamentHomeTitle => 'مسابقات شطرنج با وَرتاها و زمان‌بندی‌های گوناگون'; @override - String get tournamentHomeDescription => 'هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، مرسوم، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.'; + String get tournamentHomeDescription => 'هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، فکری، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.'; @override String get tournamentNotFound => 'مسابقات یافت نشد'; @@ -3258,12 +3519,12 @@ class AppLocalizationsFa extends AppLocalizations { @override String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 بهتر از $param2 از بازیکنان $param3 میباشد.'; + return '$param1 بهتر از $param2 بازیکنان $param3 است.'; } @override String betterThanPercentPlayers(String param1, String param2) { - return 'بهتر از $param1 بازیکنان در $param2'; + return 'بهتر از $param1 بازیکنان $param2'; } @override @@ -3291,7 +3552,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String weHaveSentYouAnEmailTo(String param) { - return 'ایمیل ارسال شد.بر روی لینک داخل ایمیل کلیک کنید تا پسورد شما ریست شود $param به آدرس'; + return 'ما یک رایانامه به $param فرستاده‌ایم. برای بازنشانی گذرواژه‌تان، روی پیوند موجود در رایانامه بزنید.'; } @override @@ -3305,7 +3566,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get networkLagBetweenYouAndLichess => 'تاخیر شبکه بین شما و Lichess'; + String get networkLagBetweenYouAndLichess => 'تاخیر شبکه میان شما و Lichess'; @override String get timeToProcessAMoveOnLichessServer => 'زمان سپری شده برای پردازش یک حرکت'; @@ -3323,19 +3584,19 @@ class AppLocalizationsFa extends AppLocalizations { String get crosstable => 'رودررو'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'شما می توانید برای حرکت در بازی از صفحه استفاده کنید'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'برای حرکت، روی صفحه بازی بِنَوَردید.'; @override - String get scrollOverComputerVariationsToPreviewThem => 'برای مشاهده آن ها اسکرول کنید.'; + String get scrollOverComputerVariationsToPreviewThem => 'برای پیش‌نمایش آن‌ها، روی وَرتِش‌های رایانه‌ای بِغَرالید.'; @override - String get analysisShapesHowTo => 'و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift'; + String get analysisShapesHowTo => 'برای رسم دایره و پیکان روی تخته، shift+click یا راست-تِلیک را بفشارید.'; @override String get letOtherPlayersMessageYou => 'ارسال پیام توسط بقیه به شما'; @override - String get receiveForumNotifications => 'دریافت اعلان در هنگام ذکر شدن در انجمن'; + String get receiveForumNotifications => 'دریافت اعلان هنگام نام‌بَری در انجمن'; @override String get shareYourInsightsData => 'اشتراک گذاشتن داده های شما'; @@ -3350,33 +3611,33 @@ class AppLocalizationsFa extends AppLocalizations { String get withEverybody => 'با همه'; @override - String get kidMode => 'حالت کودکان'; + String get kidMode => 'حالت کودک'; @override String get kidModeIsEnabled => 'حالت کودک فعال است.'; @override - String get kidModeExplanation => 'این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید.'; + String get kidModeExplanation => 'این دربارهٔ ایمنی است. در حالت کودک، همهٔ ارتباط‌های وبگاه نافعال است. این را برای فرزندان و شطرنج‌آموزان مدرسه خود فعال کنید تا از آنها در برابر دیگر کاربران اینترنت حفاظت کنید.'; @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'در حالت کودکانه،به نماد لیچس،یک $param اضافه می شود تا شما از فعال بودن آن مطلع شوید.'; + return 'در حالت کودک، نماد Lichess نقشک $param را می‌گیرد، بنابراین می‌دانید کودکان‌تان در امانند.'; } @override - String get askYourChessTeacherAboutLiftingKidMode => 'اکانت شما مدیریت شده است. برای برداشتن حالت کودک از معلم شطرنج خود درخواست کنید.'; + String get askYourChessTeacherAboutLiftingKidMode => 'حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.'; @override - String get enableKidMode => 'فعال کردن حالت کودکانه'; + String get enableKidMode => 'فعال‌سازی حالت کودک'; @override - String get disableKidMode => 'غیر فعال کردن حالت کودکانه'; + String get disableKidMode => 'ازکاراندازی حالت کودک'; @override String get security => 'امنیت'; @override - String get sessions => 'جلسات'; + String get sessions => 'جلسه'; @override String get revokeAllSessions => 'باطل کردن تمامی موارد'; @@ -3400,10 +3661,10 @@ class AppLocalizationsFa extends AppLocalizations { String get fullFeatured => 'با تمامی امکانات'; @override - String get phoneAndTablet => 'موبایل و تبلت'; + String get phoneAndTablet => 'گوشی و رایانک'; @override - String get bulletBlitzClassical => 'گلوله‌ای، برق‌آسا، مرسوم'; + String get bulletBlitzClassical => 'گلوله‌ای، برق‌آسا، فکری'; @override String get correspondenceChess => 'شطرنج مکاتبه ای'; @@ -3415,24 +3676,24 @@ class AppLocalizationsFa extends AppLocalizations { String get viewTheSolution => 'دیدن راه‌حل'; @override - String get followAndChallengeFriends => 'دنبال کردن و پیشنهاد بازی دادن به دوستان'; + String get followAndChallengeFriends => 'دنبالیدن و پیشنهاد بازی دادن به دوستان'; @override String get gameAnalysis => 'تجزیه و تحلیلِ بازی'; @override String xHostsY(String param1, String param2) { - return '$param1 میزبان ها $param2'; + return '$param1 میزبان $param2 است'; } @override String xJoinsY(String param1, String param2) { - return '$param1 وارد می شود $param2'; + return '$param1 به $param2 می‌پیوندد'; } @override String xLikesY(String param1, String param2) { - return '$param1 می پسندد $param2'; + return '$param1، $param2 را می‌پسندد'; } @override @@ -3465,7 +3726,7 @@ class AppLocalizationsFa extends AppLocalizations { String get transparent => 'شفاف'; @override - String get deviceTheme => 'طرح زمینه دستگاه'; + String get deviceTheme => 'پوستهٔ اَفزاره'; @override String get backgroundImageUrl => 'وب‌نشانی تصویر پس‌زمینه:'; @@ -3507,19 +3768,19 @@ class AppLocalizationsFa extends AppLocalizations { String get usernameCharsInvalid => 'نام کاربری فقط می تواند شامل حروف،اعداد،خط فاصله یا زیر خط(under line) باشد.'; @override - String get usernameUnacceptable => 'این نام کاربری قابل قبول نیست.'; + String get usernameUnacceptable => 'این نام کاربری پذیرفتنی نیست.'; @override - String get playChessInStyle => 'استایل شطرنج باز داشته باشید!'; + String get playChessInStyle => 'شطرنج‌بازیِ نوگارانه'; @override - String get chessBasics => 'اصول شطرنج'; + String get chessBasics => 'پایه‌های شطرنج'; @override - String get coaches => 'مربی ها'; + String get coaches => 'مربیان'; @override - String get invalidPgn => 'فایل PGN نامعتبر است'; + String get invalidPgn => 'PGN ِ نامعتبر'; @override String get invalidFen => 'وضعیت نامعتبر'; @@ -3528,11 +3789,11 @@ class AppLocalizationsFa extends AppLocalizations { String get custom => 'دلخواه'; @override - String get notifications => 'گزارش'; + String get notifications => 'اعلان'; @override String notificationsX(String param1) { - return 'هشدار: $param1'; + return 'اعلان: $param1'; } @override @@ -3554,10 +3815,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get youBrowsedAway => 'پوزیشن را به هم زدید!'; + String get youBrowsedAway => 'دور شُدید'; @override - String get resumePractice => 'ادامه تمرین'; + String get resumePractice => 'از سرگیری تمرین'; @override String get drawByFiftyMoves => 'بازی با قانون پنجاه حرکت مساوی شده است.'; @@ -3566,10 +3827,10 @@ class AppLocalizationsFa extends AppLocalizations { String get theGameIsADraw => 'بازی مساوی است.'; @override - String get computerThinking => 'محاسبه رایانه‌ای ...'; + String get computerThinking => 'محاسبهٔ رایانه‌ای...'; @override - String get seeBestMove => 'مشاهده بهترین حرکت'; + String get seeBestMove => 'دیدن بهترین حرکت'; @override String get hideBestMove => 'پنهان کردن بهترین حرکت'; @@ -3578,16 +3839,16 @@ class AppLocalizationsFa extends AppLocalizations { String get getAHint => 'راهنمایی'; @override - String get evaluatingYourMove => 'در حال بررسی حرکت شما...'; + String get evaluatingYourMove => 'حرکت‌تان را می‌ارزیابد...'; @override - String get whiteWinsGame => 'سفید برنده شد'; + String get whiteWinsGame => 'سفید می‌برد'; @override - String get blackWinsGame => 'سیاه برنده شد'; + String get blackWinsGame => 'سیاه می‌برد'; @override - String get learnFromYourMistakes => 'از اشتباهات خود درس بگیرید'; + String get learnFromYourMistakes => 'از اشتباه‌های‌تان بیاموزید'; @override String get learnFromThisMistake => 'از این اشتباه درس بگیرید'; @@ -3616,22 +3877,22 @@ class AppLocalizationsFa extends AppLocalizations { String get youCanDoBetter => 'می‌توانید بهتر انجامش دهید'; @override - String get tryAnotherMoveForWhite => 'برای سفید،حرکت دیگری را امتحان کنید'; + String get tryAnotherMoveForWhite => 'حرکت دیگری را برای سفید بیابید'; @override - String get tryAnotherMoveForBlack => 'برای سیاه،حرکت دیگری را امتحان کنید'; + String get tryAnotherMoveForBlack => 'حرکت دیگری را برای سیاه بیابید'; @override String get solution => 'راه‌حل'; @override - String get waitingForAnalysis => 'در انتظار برای آنالیز'; + String get waitingForAnalysis => 'در انتظار تحلیل'; @override - String get noMistakesFoundForWhite => 'هیچ اشتباهی برای سفید مشاهده نشد'; + String get noMistakesFoundForWhite => 'هیچی اشتباهی از سفید یافت نشد'; @override - String get noMistakesFoundForBlack => 'هیچ اشتباهی برای سیاه مشاهده نشد'; + String get noMistakesFoundForBlack => 'هیچی اشتباهی از سیاه یافت نشد'; @override String get doneReviewingWhiteMistakes => 'اشتباهات سفید بررسی شد'; @@ -3664,10 +3925,10 @@ class AppLocalizationsFa extends AppLocalizations { String get conditionalPremoves => 'پیش‌حرکت‌های شرطی'; @override - String get addCurrentVariation => 'اضافه کردن این نوع حرکات'; + String get addCurrentVariation => 'افزودن وَرتِش جاری'; @override - String get playVariationToCreateConditionalPremoves => 'یک نوع حرکات را بازی کنید تا پیش حرکت های شرطی را بسازید'; + String get playVariationToCreateConditionalPremoves => 'بازی کردن یک وَرتِش، برای ایجاد پیش‌حرکت‌های شرطی'; @override String get noConditionalPremoves => 'بدون پیش‌حرکت‌های شرطی'; @@ -3702,7 +3963,7 @@ class AppLocalizationsFa extends AppLocalizations { String get potentialProblem => 'زمانی که مشکلی احتمالی شناسایی شد ، این پیام را نمایش می دهیم.'; @override - String get howToAvoidThis => 'چگونه از این امر جلوگیری کنیم؟'; + String get howToAvoidThis => 'چگونه از آن بپرهیزیم؟'; @override String get playEveryGame => 'هر بازی‌ای که آغازیدید را، بازی کنید.'; @@ -3729,18 +3990,18 @@ class AppLocalizationsFa extends AppLocalizations { String get currentMatchScore => 'امتیاز بازی فعلی'; @override - String get agreementAssistance => 'من تضمین میکنم که در حین بازی ها کمک نگیرم ( از انجین ، کتاب ، پایگاه داده یا شخصی دیگر)'; + String get agreementAssistance => 'من موافقم که در طول بازی‌هایم هیچگاه کمکی نخواهم گرفت (از یک رایانه شطرنج، کتاب، دادگان یا شخص دیگری).'; @override - String get agreementNice => 'من تضمین میکنم که همیشه به بازیکن های دیگر احترام بگذارم.'; + String get agreementNice => 'می‌پذیرم که همواره به بازیکنان دیگر احترام گزارم.'; @override String agreementMultipleAccounts(String param) { - return 'من موافقت می‌کنم که چندین اکانت برای خودم ایجاد نکنم(به جز دلایلی که در $param اشاره شده).'; + return 'موافقم که چندین حساب نخواهم ساخت (جز به دلیل‌های ذکر شده در $param).'; } @override - String get agreementPolicy => 'من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم .'; + String get agreementPolicy => 'با پیروی از همهٔ خط‌مشی‌های Lichess، موافقم.'; @override String get searchOrStartNewDiscussion => 'جستجو یا شروع کردن مکالمه جدید'; @@ -3758,7 +4019,7 @@ class AppLocalizationsFa extends AppLocalizations { String get rapid => 'سریع'; @override - String get classical => 'کلاسیک'; + String get classical => 'فکری'; @override String get ultraBulletDesc => 'بازی‌های سرعتی دیوانه‌وار: کمتر از ۳۰ ثانیه'; @@ -3773,7 +4034,7 @@ class AppLocalizationsFa extends AppLocalizations { String get rapidDesc => 'بازی های سریع: ۸ تا ۲۵ دقیقه'; @override - String get classicalDesc => 'بازی های کلاسیک : 25 دقیقه یا بیشتر'; + String get classicalDesc => 'بازی های فکری: ۲۵ دقیقه یا بیشتر'; @override String get correspondenceDesc => 'بازی های مکاتبه ای : یک یا چند روز برای هر حرکت'; @@ -3790,7 +4051,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get inTheFAQ => 'در سوالات متداول باشد.'; + String get inTheFAQ => 'در پرسش‌های پُربسامد'; @override String toReportSomeoneForCheatingOrBadBehavior(String param1) { @@ -3821,7 +4082,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String joinTheTeamXToPost(String param1) { - return 'به $param1 ملحق شوید تا در این انجمن پست بگذارید'; + return 'برای فرسته گذاشتن در این انجمن، به $param1 بپیوندید'; } @override @@ -3830,7 +4091,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get youCannotPostYetPlaySomeGames => 'شما هنوز قادر به پست گذاشتن در انجمن نیستید. چند بازی انجام دهید!'; + String get youCannotPostYetPlaySomeGames => 'هنوز نمی‌توانید در انجمن‌ها فرسته گذارید. چند بازی کنید!'; @override String get subscribe => 'مشترک شدن'; @@ -3840,12 +4101,12 @@ class AppLocalizationsFa extends AppLocalizations { @override String mentionedYouInX(String param1) { - return 'از شما در $param1 نام برده شد.'; + return 'در «$param1» از شما نام‌برده شد.'; } @override String xMentionedYouInY(String param1, String param2) { - return '$param1 اسم شما را در \"$param2\" ذکر کرده است.'; + return '$param1 از شما در \"$param2\" نام برد.'; } @override @@ -3863,14 +4124,14 @@ class AppLocalizationsFa extends AppLocalizations { @override String youHaveJoinedTeamX(String param1) { - return 'شما به \"$param1\" پیوستید.'; + return 'شما به «$param1» پیوسته‌اید.'; } @override String get someoneYouReportedWasBanned => 'شخصی که گزارش کردید مسدود شد'; @override - String get congratsYouWon => 'تبریک، شما برنده شدید!'; + String get congratsYouWon => 'شادباش، شما بُردید!'; @override String gameVsX(String param1) { @@ -3883,7 +4144,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get lostAgainstTOSViolator => 'شما در برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید'; + String get lostAgainstTOSViolator => 'شما برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید'; @override String refundXpointsTimeControlY(String param1, String param2) { @@ -3891,10 +4152,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get timeAlmostUp => 'زمان تقریباً تمام شده است!'; + String get timeAlmostUp => 'زمان نزدیک به پایان است!'; @override - String get clickToRevealEmailAddress => '[برای آشکارسازی نشانی رایانامه بتلیکید]'; + String get clickToRevealEmailAddress => '[برای آشکارسازی نشانیِ رایانامه بتِلیکید]'; @override String get download => 'بارگیری'; @@ -3903,7 +4164,7 @@ class AppLocalizationsFa extends AppLocalizations { String get coachManager => 'تنظیمات مربی'; @override - String get streamerManager => 'مدیریت پخش'; + String get streamerManager => 'مدیریت جریان‌سازی'; @override String get cancelTournament => 'لغو مسابقه'; @@ -3934,7 +4195,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String positionInputHelp(String param) { - return 'برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با شطرنج‌گونه‌ها.\nمی‌توانید از $param برای آزانیدن وضعیت FEN استفاده کنید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.'; + return 'برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با وَرتاها.\nمی‌توانید از $param برای آزانیدن وضعیت FEN بهرایید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.'; } @override @@ -3948,7 +4209,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String simulFeatured(String param) { - return 'نمایش در $param'; + return 'آرنگیدن در $param'; } @override @@ -3977,7 +4238,7 @@ class AppLocalizationsFa extends AppLocalizations { String get tournChat => 'چت مسابقه'; @override - String get noChat => 'بدون چت'; + String get noChat => 'بدون گپ'; @override String get onlyTeamLeaders => 'تنها مسئولان تیم'; @@ -4077,6 +4338,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get nothingToSeeHere => 'فعلا هیچی اینجا نیست.'; + @override + String get stats => 'آمار'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4126,8 +4390,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count غیردقیق', - one: '$count غیردقیق', + other: '$count نادقیق', + one: '$count نادقیق', ); return '$_temp0'; } @@ -4159,7 +4423,7 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count ریتینگ در $param2 بازی', + other: 'درجه‌بندی $count در $param2 بازی', one: 'درجه‌بندی $count در $param2 بازی', ); return '$_temp0'; @@ -4170,8 +4434,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی مورد علاقه', - one: '$count بازی مورد علاقه', + other: '$count نشانک', + one: '$count نشانک', ); return '$_temp0'; } @@ -4291,8 +4555,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی در حال انجام', - one: '$count بازی در حال انجام', + other: '$count بازیِ اکنونی', + one: '$count بازیِ اکنونی', ); return '$_temp0'; } @@ -4368,8 +4632,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'شما باید$count بازی رسمی$param2 انجام دهید.', - one: 'شما باید$count بازی رسمی$param2 انجام دهید.', + other: 'شما باید $count بازی رسمی $param2 دیگر کنید', + one: 'شما باید $count بازی رسمی $param2 دیگر کنید', ); return '$_temp0'; } @@ -4412,8 +4676,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال کننده‌', - one: '$count دنبال کننده‌', + other: '$count دنبال‌گر', + one: '$count دنبال‌گر', ); return '$_temp0'; } @@ -4423,8 +4687,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال میکند', - one: '$count دنبال می کند', + other: '$count دنبال‌شده', + one: '$count دنبالیده', ); return '$_temp0'; } @@ -4478,8 +4742,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count وبنوشته در انجمن', - one: '$count وبنوشته در انجمن', + other: '$count فرسته در انجمن', + one: '$count فرسته در انجمن', ); return '$_temp0'; } @@ -4489,8 +4753,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازیکن $param2 این هفته فعالیت داشته‌اند.', - one: '$count بازیکن $param2 این هفته فعالیت داشته‌ است.', + other: 'این هفته، $count بازیکن $param2.', + one: 'این هفته، $count بازیکن $param2.', ); return '$_temp0'; } @@ -4610,7 +4874,7 @@ class AppLocalizationsFa extends AppLocalizations { String get stormHighscores => 'بالاترین امتیازها'; @override - String get stormViewBestRuns => 'مشاهده بهترین دورها'; + String get stormViewBestRuns => 'دیدن بهترین دورها'; @override String get stormBestRunOfDay => 'بهترین دور روز'; @@ -4622,7 +4886,7 @@ class AppLocalizationsFa extends AppLocalizations { String get stormGetReady => 'آماده شوید!'; @override - String get stormWaitingForMorePlayers => 'در حال انتظار برای پیوستن بازیکنان بیشتر...'; + String get stormWaitingForMorePlayers => 'در انتظارِ پیوستن بازیکنان بیشتر...'; @override String get stormRaceComplete => 'مسابقه تمام شد!'; @@ -4724,8 +4988,692 @@ class AppLocalizationsFa extends AppLocalizations { String get streamerLichessStreamers => 'بَرخَط-محتواسازان Lichess'; @override - String get studyShareAndExport => 'همرسانی و برون‏بُرد'; + String get studyPrivate => 'خصوصی'; @override - String get studyStart => 'آغاز'; + String get studyMyStudies => 'مطالعه‌های من'; + + @override + String get studyStudiesIContributeTo => 'مطالعه‌هایی که در آن شرکت دارم'; + + @override + String get studyMyPublicStudies => 'مطالعه‌های همگانی من'; + + @override + String get studyMyPrivateStudies => 'مطالعه‌های خصوصی من'; + + @override + String get studyMyFavoriteStudies => 'مطالعه‌های دلخواه من'; + + @override + String get studyWhatAreStudies => 'مطالعه‌ها چه هستند؟'; + + @override + String get studyAllStudies => 'همه مطالعه‌ها'; + + @override + String studyStudiesCreatedByX(String param) { + return 'مطالعه‌هایی که $param ساخته است'; + } + + @override + String get studyNoneYet => 'هنوز، هیچ.'; + + @override + String get studyHot => 'رواجیده'; + + @override + String get studyDateAddedNewest => 'تاریخ افزوده شدن (نوترین)'; + + @override + String get studyDateAddedOldest => 'تاریخ افزوده شدن (کهنه‌ترین)'; + + @override + String get studyRecentlyUpdated => 'تازگی به‌روزشده'; + + @override + String get studyMostPopular => 'محبوب‌ترین‌'; + + @override + String get studyAlphabetical => 'براساس حروف الفبا'; + + @override + String get studyAddNewChapter => 'افزودن بخش جدید'; + + @override + String get studyAddMembers => 'افزودن اعضا'; + + @override + String get studyInviteToTheStudy => 'دعوت به این مطالعه'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'لطفا تنها کسانی را دعوت کنید که شما را می‌شناسند و کنشگرانه می‌خواهند به این مطالعه بپیوندند.'; + + @override + String get studySearchByUsername => 'جستجو بر اساس نام کاربری'; + + @override + String get studySpectator => 'تماشاگر'; + + @override + String get studyContributor => 'مشارکت کننده'; + + @override + String get studyKick => 'اخراج'; + + @override + String get studyLeaveTheStudy => 'ترک مطالعه'; + + @override + String get studyYouAreNowAContributor => 'شما یک مشارکت کننده جدید هستید'; + + @override + String get studyYouAreNowASpectator => 'شما اکنون یک تماشاگرید'; + + @override + String get studyPgnTags => 'نشان های PGN'; + + @override + String get studyLike => 'پسندیدن'; + + @override + String get studyUnlike => 'نمی‌پسندم'; + + @override + String get studyNewTag => 'برچسب جدید'; + + @override + String get studyCommentThisPosition => 'یادداشت‌نویسی برای این وضعیت'; + + @override + String get studyCommentThisMove => 'یادداشت‌نویسی برای این حرکت'; + + @override + String get studyAnnotateWithGlyphs => 'حرکت‌نویسی به‌همراه علامت‌ها'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'این بخش برای تحلیل، بسیار کوتاه است.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'تنها مشارکت‌گران این مطالعه، می‌توانند درخواست تحلیل رایانه‌ای دهند.'; + + @override + String get studyGetAFullComputerAnalysis => 'یک تحلیل کامل رایانه‌ای کارساز-سو از شاخه اصلی بگیرید.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'مطمئن شوید که بخش کامل است. شما فقط یک بار می‌توانید درخواست تحلیل دهید.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'همه‌ی عضوهای همگام در وضعیت یکسانی باقی می‌مانند'; + + @override + String get studyShareChanges => 'تغییرها را در کارساز ذخیره کنید و با تماشاگران به اشتراک گذارید'; + + @override + String get studyPlaying => 'در حال انجام'; + + @override + String get studyShowEvalBar => 'نوار ارزیابی'; + + @override + String get studyFirst => 'اولین'; + + @override + String get studyPrevious => 'پیشین'; + + @override + String get studyNext => 'بعدی'; + + @override + String get studyLast => 'آخرین'; + + @override + String get studyShareAndExport => 'همرسانی و برون‏بُرد'; + + @override + String get studyCloneStudy => 'همسانیدن'; + + @override + String get studyStudyPgn => 'PGN مطالعه'; + + @override + String get studyDownloadAllGames => 'بارگیری تمام بازی ها'; + + @override + String get studyChapterPgn => 'PGN ِ بخش'; + + @override + String get studyCopyChapterPgn => 'رونوشت‌گیری PGN'; + + @override + String get studyDownloadGame => 'بارگیری بازی'; + + @override + String get studyStudyUrl => 'وب‌نشانی مطالعه'; + + @override + String get studyCurrentChapterUrl => 'وب‌نشانی بخش جاری'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'می‌توانید این را در انجمن یا وبنوشت Lichessتان برای جاسازی قرار دهید'; + + @override + String get studyStartAtInitialPosition => 'در وضعیت نخستین بیاغازید'; + + @override + String studyStartAtX(String param) { + return 'آغاز از $param'; + } + + @override + String get studyEmbedInYourWebsite => 'در وبگاهتان قرار دهید'; + + @override + String get studyReadMoreAboutEmbedding => 'درباره قرار دادن (در سایت) بیشتر بخوانید'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'فقط مطالعه‌های همگانی می‌توانند جایگذاری شوند!'; + + @override + String get studyOpen => 'بگشایید'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1، به دست $param2 برای شما آورده شده است'; + } + + @override + String get studyStudyNotFound => 'مطالعه یافت نشد'; + + @override + String get studyEditChapter => 'ویرایش بخش'; + + @override + String get studyNewChapter => 'بخش نو'; + + @override + String studyImportFromChapterX(String param) { + return 'درونبُرد از $param'; + } + + @override + String get studyOrientation => 'جهت'; + + @override + String get studyAnalysisMode => 'حالت تجزیه تحلیل'; + + @override + String get studyPinnedChapterComment => 'یادداشت سنجاقیده‌ٔ بخش'; + + @override + String get studySaveChapter => 'ذخیره بخش'; + + @override + String get studyClearAnnotations => 'پاک کردن حرکت‌نویسی'; + + @override + String get studyClearVariations => 'پاکیدن وَرتِش‌ها'; + + @override + String get studyDeleteChapter => 'حذف بخش'; + + @override + String get studyDeleteThisChapter => 'حذف این بخش. بازگشت وجود ندارد!'; + + @override + String get studyClearAllCommentsInThisChapter => 'همه دیدگاه‌ها، نمادها و شکل‌های ترسیم شده در این بخش، پاک شوند'; + + @override + String get studyRightUnderTheBoard => 'درست زیر صفحهٔ بازی'; + + @override + String get studyNoPinnedComment => 'هیچ'; + + @override + String get studyNormalAnalysis => 'تحلیل ساده'; + + @override + String get studyHideNextMoves => 'پنهان کردن حرکت بعدی'; + + @override + String get studyInteractiveLesson => 'درس میان‌کنشی'; + + @override + String studyChapterX(String param) { + return 'بخش $param'; + } + + @override + String get studyEmpty => 'خالی'; + + @override + String get studyStartFromInitialPosition => 'از وضعیت نخستین بیاغازید'; + + @override + String get studyEditor => 'ویرایشگر'; + + @override + String get studyStartFromCustomPosition => 'از وضعیت دلخواه بیاغازید'; + + @override + String get studyLoadAGameByUrl => 'بارگذاری بازی از وب‌نشانی‌ها'; + + @override + String get studyLoadAPositionFromFen => 'بار کردن وضعیت از FEN'; + + @override + String get studyLoadAGameFromPgn => 'باگذاری بازی با استفاده از فایل PGN'; + + @override + String get studyAutomatic => 'خودکار'; + + @override + String get studyUrlOfTheGame => 'وب‌نشانی بازی‌ها، یکی در هر خط'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'بازی‌ها را از $param1 یا $param2 بارگذاری نمایید'; + } + + @override + String get studyCreateChapter => 'ساخت بخش'; + + @override + String get studyCreateStudy => 'ساخت مطالعه'; + + @override + String get studyEditStudy => 'ویرایش مطالعه'; + + @override + String get studyVisibility => 'دیدگی'; + + @override + String get studyPublic => 'همگانی'; + + @override + String get studyUnlisted => 'فهرست‌نشده'; + + @override + String get studyInviteOnly => 'فقط توسط دعوتنامه'; + + @override + String get studyAllowCloning => 'اجازه همسانِش'; + + @override + String get studyNobody => 'هیچ کس'; + + @override + String get studyOnlyMe => 'تنها من'; + + @override + String get studyContributors => 'مشارکت‌کنندگان'; + + @override + String get studyMembers => 'اعضا'; + + @override + String get studyEveryone => 'همه'; + + @override + String get studyEnableSync => 'فعال کردن همگام سازی'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'بله: همه را در وضعیت یکسانی نگه دار'; + + @override + String get studyNoLetPeopleBrowseFreely => 'خیر: به مردم اجازه جستجوی آزادانه بده'; + + @override + String get studyPinnedStudyComment => 'یادداشت سنجاقیده به مطالعه'; + + @override + String get studyStart => 'آغاز'; + + @override + String get studySave => 'ذخیره'; + + @override + String get studyClearChat => 'پاک کردن گفتگو'; + + @override + String get studyDeleteTheStudyChatHistory => 'پیشینه گپِ مطالعه پاک شود؟ بازگشت وجود ندارد!'; + + @override + String get studyDeleteStudy => 'پاکیدن مطالعه'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'کل مطالعه پاک شود؟ بازگشت وجود ندارد! برای تایید، نام مطالعه را بنویسید: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'کجا می‌خواهید آنرا مطالعه کنید؟'; + + @override + String get studyGoodMove => 'حرکت خوب'; + + @override + String get studyMistake => 'اشتباه'; + + @override + String get studyBrilliantMove => 'حرکت درخشان'; + + @override + String get studyBlunder => 'اشتباه فاحش'; + + @override + String get studyInterestingMove => 'حرکت جالب'; + + @override + String get studyDubiousMove => 'حرکت مشکوک'; + + @override + String get studyOnlyMove => 'تک‌حرکت'; + + @override + String get studyZugzwang => 'اکراهی'; + + @override + String get studyEqualPosition => 'وضعیت برابر'; + + @override + String get studyUnclearPosition => 'وضعیت ناروشن'; + + @override + String get studyWhiteIsSlightlyBetter => 'سفید کمی بهتر است'; + + @override + String get studyBlackIsSlightlyBetter => 'سیاه کمی بهتر است'; + + @override + String get studyWhiteIsBetter => 'سفید بهتر است'; + + @override + String get studyBlackIsBetter => 'سیاه بهتر است'; + + @override + String get studyWhiteIsWinning => 'سفید می‌برد'; + + @override + String get studyBlackIsWinning => 'سیاه می‌برد'; + + @override + String get studyNovelty => 'روش و ایده‌ای نو در شروع بازی'; + + @override + String get studyDevelopment => 'گسترش'; + + @override + String get studyInitiative => 'ابتکار عمل'; + + @override + String get studyAttack => 'حمله'; + + @override + String get studyCounterplay => 'بازی‌متقابل'; + + @override + String get studyTimeTrouble => 'تنگی زمان'; + + @override + String get studyWithCompensation => 'دارای مزیت و برتری'; + + @override + String get studyWithTheIdea => 'با طرح'; + + @override + String get studyNextChapter => 'بخش بعدی'; + + @override + String get studyPrevChapter => 'بخش پیشین'; + + @override + String get studyStudyActions => 'عملگرهای مطالعه'; + + @override + String get studyTopics => 'موضوع‌ها'; + + @override + String get studyMyTopics => 'موضوع‌های من'; + + @override + String get studyPopularTopics => 'موضوع‌های محبوب'; + + @override + String get studyManageTopics => 'مدیریت موضوع‌ها'; + + @override + String get studyBack => 'بازگشت'; + + @override + String get studyPlayAgain => 'دوباره بازی کنید'; + + @override + String get studyWhatWouldYouPlay => 'در این وضعیت چطور بازی می‌کنید؟'; + + @override + String get studyYouCompletedThisLesson => 'تبریک! شما این درس را کامل کردید.'; + + @override + String studyPerPage(String param) { + return '$param میز'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بخش', + one: '$count بخش', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بازی', + one: '$count بازی', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count عضو', + one: '$count عضو', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'متن PGN خود را در اینجا بچسبانید، تا $count بازی', + one: 'متن PGN خود را در اینجا بچسبانید، تا $count بازی', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'چند لحظه پیش'; + + @override + String get timeagoRightNow => 'هم‌اکنون'; + + @override + String get timeagoCompleted => 'کامل شده'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ثانیهٔ دیگر', + one: 'تا $count ثانیهٔ دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count دقیقه دیگر', + one: 'تا $count دقیقه دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ساعت دیگر', + one: 'تا $count ساعت دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count روز دیگر', + one: 'تا $count روز دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count هفته دیگر', + one: 'تا $count هفته دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ماه دیگر', + one: 'تا $count ماه دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count سال دیگر', + one: 'تا $count سال دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count دقیقه پیش', + one: '$count دقیقه پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ساعت پیش', + one: '$count ساعت پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count روز پیش', + one: '$count روز پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count هفته پیش', + one: '$count هفته پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ماه پیش', + one: '$count ماه پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count سال پیش', + one: '$count سال پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count دقیقه باقی مانده', + one: '$count دقیقه باقی مانده', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ساعت باقی مانده', + one: '$count ساعت باقی مانده', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index ff4644cfd5..76807dd677 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsFi extends AppLocalizations { AppLocalizationsFi([String locale = 'fi']) : super(locale); @override - String get mobileHomeTab => 'Etusivu'; + String get mobileAllGames => 'Kaikki pelit'; @override - String get mobilePuzzlesTab => 'Tehtävät'; + String get mobileAreYouSure => 'Oletko varma?'; @override - String get mobileToolsTab => 'Työkalut'; + String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; @override - String get mobileWatchTab => 'Seuraa'; + String get mobileClearButton => 'Tyhjennä'; @override - String get mobileSettingsTab => 'Asetukset'; + String get mobileCorrespondenceClearSavedMove => 'Poista tallennettu siirto'; @override - String get mobileMustBeLoggedIn => 'Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.'; + String get mobileCustomGameJoinAGame => 'Liity peliin'; @override - String get mobileSystemColors => 'Järjestelmän värit'; + String get mobileFeedbackButton => 'Palaute'; @override - String get mobileFeedbackButton => 'Palaute'; + String mobileGreeting(String param) { + return 'Hei $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsHapticFeedback => 'Kosketuspalaute'; + String get mobileHideVariation => 'Piilota muunnelma'; @override - String get mobileSettingsImmersiveMode => 'Kokoruututila'; + String get mobileHomeTab => 'Etusivu'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.'; + String get mobileLiveStreamers => 'Live-striimaajat'; @override - String get mobileNotFollowingAnyUser => 'Et seuraa yhtäkään käyttäjää.'; + String get mobileMustBeLoggedIn => 'Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.'; @override - String get mobileAllGames => 'Kaikki pelit'; + String get mobileNoSearchResults => 'Ei hakutuloksia'; @override - String get mobileRecentSearches => 'Viimeisimmät haut'; + String get mobileNotFollowingAnyUser => 'Et seuraa yhtäkään käyttäjää.'; @override - String get mobileClearButton => 'Tyhjennä'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Pelaajat, joiden tunnuksesta löytyy \"$param\"'; } @override - String get mobileNoSearchResults => 'Ei hakutuloksia'; + String get mobilePrefMagnifyDraggedPiece => 'Suurenna vedettävä nappula'; @override - String get mobileAreYouSure => 'Oletko varma?'; + String get mobilePuzzleStormConfirmEndRun => 'Haluatko lopettaa tämän sarjan?'; @override - String get mobilePuzzleStreakAbortWarning => 'Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ei näytettävää, muuta suodatusehtoja'; @override String get mobilePuzzleStormNothingToShow => 'Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.'; @override - String get mobileSharePuzzle => 'Jaa tämä tehtävä'; + String get mobilePuzzleStormSubtitle => 'Ratkaise mahdollisimman monta tehtävää 3 minuutissa.'; @override - String get mobileShareGameURL => 'Jaa pelin URL'; + String get mobilePuzzleStreakAbortWarning => 'Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.'; @override - String get mobileShareGamePGN => 'Jaa PGN'; + String get mobilePuzzleThemesSubtitle => 'Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.'; @override - String get mobileSharePositionAsFEN => 'Jaa asema FEN:nä'; + String get mobilePuzzlesTab => 'Tehtävät'; @override - String get mobileShowVariations => 'Näytä muunnelmat'; + String get mobileRecentSearches => 'Viimeisimmät haut'; @override - String get mobileHideVariation => 'Piilota muunnelma'; + String get mobileSettingsHapticFeedback => 'Kosketuspalaute'; @override - String get mobileShowComments => 'Näytä kommentit'; + String get mobileSettingsImmersiveMode => 'Kokoruututila'; @override - String get mobilePuzzleStormConfirmEndRun => 'Haluatko lopettaa tämän sarjan?'; + String get mobileSettingsImmersiveModeSubtitle => 'Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ei näytettävää, muuta suodatusehtoja'; + String get mobileSettingsTab => 'Asetukset'; @override - String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; + String get mobileShareGamePGN => 'Jaa PGN'; @override - String get mobileCancelDrawOffer => 'Peruuta tasapeliehdotus'; + String get mobileShareGameURL => 'Jaa pelin URL'; @override - String get mobileWaitingForOpponentToJoin => 'Odotetaan vastustajan löytymistä...'; + String get mobileSharePositionAsFEN => 'Jaa asema FEN:nä'; @override - String get mobileBlindfoldMode => 'Sokko'; + String get mobileSharePuzzle => 'Jaa tämä tehtävä'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Näytä kommentit'; @override - String get mobileCustomGameJoinAGame => 'Liity peliin'; + String get mobileShowResult => 'Näytä lopputulos'; @override - String get mobileCorrespondenceClearSavedMove => 'Poista tallennettu siirto'; + String get mobileShowVariations => 'Näytä muunnelmat'; @override String get mobileSomethingWentWrong => 'Jokin meni vikaan.'; @override - String get mobileShowResult => 'Näytä lopputulos'; - - @override - String get mobilePuzzleThemesSubtitle => 'Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.'; + String get mobileSystemColors => 'Järjestelmän värit'; @override - String get mobilePuzzleStormSubtitle => 'Ratkaise mahdollisimman monta tehtävää 3 minuutissa.'; + String get mobileTheme => 'Teema'; @override - String mobileGreeting(String param) { - return 'Hei $param'; - } + String get mobileToolsTab => 'Työkalut'; @override - String get mobileGreetingWithoutName => 'Hei'; + String get mobileWaitingForOpponentToJoin => 'Odotetaan vastustajan löytymistä...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Seuraa'; @override String get activityActivity => 'Toiminta'; @@ -246,6 +243,17 @@ class AppLocalizationsFi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pelasi $count $param2-kirjeshakkipeliä', + one: 'Pelasi $count $param2-kirjeshakkipelin', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsFi extends AppLocalizations { @override String get broadcastBroadcasts => 'Lähetykset'; + @override + String get broadcastMyBroadcasts => 'Omat lähetykset'; + @override String get broadcastLiveBroadcasts => 'Suorat lähetykset turnauksista'; + @override + String get broadcastBroadcastCalendar => 'Lähetyskalenteri'; + + @override + String get broadcastNewBroadcast => 'Uusi livelähetys'; + + @override + String get broadcastSubscribedBroadcasts => 'Tilatut lähetykset'; + + @override + String get broadcastAboutBroadcasts => 'Lähetyksistä'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kuinka Lichess-lähetyksiä käytetään.'; + + @override + String get broadcastTheNewRoundHelp => 'Uudella kierroksella on samat jäsenet ja osallistujat kuin edellisellä.'; + + @override + String get broadcastAddRound => 'Lisää kierros'; + + @override + String get broadcastOngoing => 'Käynnissä'; + + @override + String get broadcastUpcoming => 'Tulossa'; + + @override + String get broadcastCompleted => 'Päättyneet'; + + @override + String get broadcastCompletedHelp => 'Lichess tunnistaa lähteenä olevista peleistä, milloin kierros on viety päätökseen. Lähteen puuttuessa voit käyttää tätä asetusta.'; + + @override + String get broadcastRoundName => 'Kierroksen nimi'; + + @override + String get broadcastRoundNumber => 'Kierroksen numero'; + + @override + String get broadcastTournamentName => 'Turnauksen nimi'; + + @override + String get broadcastTournamentDescription => 'Turnauksen lyhyt kuvaus'; + + @override + String get broadcastFullDescription => 'Täysimittainen kuvaus tapahtumasta'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Ei-pakollinen pitkä kuvaus lähetyksestä. $param1-muotoiluja voi käyttää. Pituus voi olla enintään $param2 merkkiä.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN:n lähde-URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, josta Lichess hakee PGN-päivitykset. Sen täytyy olla julkisesti saatavilla internetissä.'; + + @override + String get broadcastSourceGameIds => 'Korkeintaan 64 Lichess-pelin tunnistenumeroa välilyönneillä eroteltuna.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Alkamisajankohta turnauksen paikallisella aikavyöhykkeellä: $param'; + } + + @override + String get broadcastStartDateHelp => 'Ei-pakollinen, laita jos tiedät milloin tapahtuma alkaa'; + + @override + String get broadcastCurrentGameUrl => 'Tämän pelin URL'; + + @override + String get broadcastDownloadAllRounds => 'Lataa kaikki kierrokset'; + + @override + String get broadcastResetRound => 'Nollaa tämä kierros'; + + @override + String get broadcastDeleteRound => 'Poista tämä kierros'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Poista kierros ja sen pelit lopullisesti.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Poista kaikki tämän kierroksen pelit. Lähteen on oltava aktiivinen, jotta pelit voidaan luoda uudelleen.'; + + @override + String get broadcastEditRoundStudy => 'Kierrostutkielman muokkaus'; + + @override + String get broadcastDeleteTournament => 'Poista tämä turnaus'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Poista lopullisesti koko turnaus, sen kaikki kierrokset ja kaikki pelit.'; + + @override + String get broadcastShowScores => 'Näytä pelaajien pisteet pelien tulosten pohjalta'; + + @override + String get broadcastReplacePlayerTags => 'Valinnainen: korvaa pelaajien nimet, vahvuusluvut ja arvonimet'; + + @override + String get broadcastFideFederations => 'FIDEn liitot'; + + @override + String get broadcastTop10Rating => 'Top 10 -vahvuuslukulista'; + + @override + String get broadcastFidePlayers => 'FIDE-pelaajat'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-pelaajaa ei löytynyt'; + + @override + String get broadcastFideProfile => 'FIDE-profiili'; + + @override + String get broadcastFederation => 'Kansallinen liitto'; + + @override + String get broadcastAgeThisYear => 'Ikä tänä vuonna'; + + @override + String get broadcastUnrated => 'Pisteyttämätön'; + + @override + String get broadcastRecentTournaments => 'Viimeisimmät turnaukset'; + + @override + String get broadcastOpenLichess => 'Avaa Lichessissä'; + + @override + String get broadcastTeams => 'Joukkueet'; + + @override + String get broadcastBoards => 'Laudat'; + + @override + String get broadcastOverview => 'Pääsivu'; + + @override + String get broadcastSubscribeTitle => 'Tilaa ilmoitukset kunkin kierroksen alkamisesta. Käyttäjätunnuksesi asetuksista voit kytkeä ääni- ja puskuilmoitukset päälle tai pois.'; + + @override + String get broadcastUploadImage => 'Lisää turnauksen kuva'; + + @override + String get broadcastNoBoardsYet => 'Pelilautoja ei vielä ole. Ne tulevat näkyviin sitä mukaa, kun pelit ladataan tänne.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Laudat voidaan ladata lähteen kautta tai $param kautta'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Alkaa $param:n jälkeen'; + } + + @override + String get broadcastStartVerySoon => 'Lähetys alkaa aivan pian.'; + + @override + String get broadcastNotYetStarted => 'Lähetys ei ole vielä alkanut.'; + + @override + String get broadcastOfficialWebsite => 'Virallinen verkkosivu'; + + @override + String get broadcastStandings => 'Tulostaulu'; + + @override + String get broadcastOfficialStandings => 'Virallinen tulostaulu'; + + @override + String broadcastIframeHelp(String param) { + return 'Lisäasetuksia löytyy $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasterin sivulta'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Tämän kierroksen julkinen ja reaaliaikainen PGN-tiedosto. Nopeampaan ja tehokkaampaan synkronisointiin on tarjolla myös $param.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Upota tämä lähetys sivustoosi'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Upota $param sivustoosi'; + } + + @override + String get broadcastRatingDiff => 'Vahvuuslukujen erotus'; + + @override + String get broadcastGamesThisTournament => 'Pelit tässä turnauksessa'; + + @override + String get broadcastScore => 'Pisteet'; + + @override + String get broadcastAllTeams => 'Kaikki joukkueet'; + + @override + String get broadcastTournamentFormat => 'Turnauksen laji'; + + @override + String get broadcastTournamentLocation => 'Turnauksen sijainti'; + + @override + String get broadcastTopPlayers => 'Parhaat pelaajat'; + + @override + String get broadcastTimezone => 'Aikavyöhyke'; + + @override + String get broadcastFideRatingCategory => 'Kategoria (FIDE-vahvuuslukujen mukaan)'; + + @override + String get broadcastOptionalDetails => 'Mahdolliset lisätiedot'; + + @override + String get broadcastPastBroadcasts => 'Menneet lähetykset'; + + @override + String get broadcastAllBroadcastsByMonth => 'Näytä kaikki lähetykset kuukausikohtaisesti'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lähetystä', + one: '$count lähetys', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Haasteet: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get preferencesInGameOnly => 'Vain pelin aikana'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Shakkikello'; @@ -750,6 +1008,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ilmoitusten kilahdusääni'; + @override + String get preferencesBlindfold => 'Sokko'; + @override String get puzzlePuzzles => 'Tehtävät'; @@ -1390,10 +1651,10 @@ class AppLocalizationsFi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Vastustajalla on rajoitettu määrä mahdollisia siirtoja, ja niistä kaikki heikentävät hänen asemaansa.'; @override - String get puzzleThemeHealthyMix => 'Terve sekoitus'; + String get puzzleThemeMix => 'Terve sekoitus'; @override - String get puzzleThemeHealthyMixDescription => 'Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.'; + String get puzzleThemeMixDescription => 'Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.'; @override String get puzzleThemePlayerGames => 'Pelaajan peleistä'; @@ -1767,9 +2028,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get byCPL => 'Virheet'; - @override - String get openStudy => 'Avaa tutkielma'; - @override String get enable => 'Käytössä'; @@ -1797,9 +2055,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get removesTheDepthLimit => 'Poistaa syvyysrajoituksen ja pitää koneesi lämpöisenä'; - @override - String get engineManager => 'Moottorin hallinta'; - @override String get blunder => 'Vakava virhe'; @@ -2063,6 +2318,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get gamesPlayed => 'Pelattuja pelejä'; + @override + String get ok => 'OK'; + @override String get cancel => 'Peruuta'; @@ -2437,9 +2695,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get unblock => 'Poista esto'; - @override - String get followsYou => 'Seuraa sinua'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 alkoi seurata $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsFi extends AppLocalizations { String get other => 'Muu'; @override - String get reportDescriptionHelp => 'Liitä linkki peliin/peleihin ja kerro, mikä on pielessä tämän käyttäjän käytöksessä. Älä vain sano että \"hän huijaa\", vaan kerro meille miksi ajattelet näin. Raporttisi käydään läpi nopeammin, jos se on kirjoitettu englanniksi.'; + String get reportCheatBoostHelp => 'Liitä linkki peliin/peleihin ja kerro, mikä tämän käyttäjän toiminnassa on pielessä. Älä vain sano hänen huijaavan, vaan kerro meille, miksi olet päätellyt niin.'; + + @override + String get reportUsernameHelp => 'Selitä, mikä tässä käyttäjätunnuksessa on loukkaavaa. Älä vain sano sen olevan loukkaava tai sopimaton, vaan kerro meille, mihin näkemyksesi perustuu, varsinkin jos loukkaus on epäsuora, muun kuin englanninkielinen, slangia, tai jos siinä viitataan kulttuuriin tai historiaan.'; + + @override + String get reportProcessedFasterInEnglish => 'Ilmoituksesi käsitellään nopeammin, jos se on kirjoitettu englanniksi.'; @override String get error_provideOneCheatedGameLink => 'Anna ainakin yksi linkki peliin, jossa epäilet huijaamista.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get nothingToSeeHere => 'Täällä ei ole tällä hetkellä mitään nähtävää.'; + @override + String get stats => 'Tilastot'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsFi extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-striimaajat'; + @override + String get studyPrivate => 'Yksityinen'; + + @override + String get studyMyStudies => 'Tutkielmani'; + + @override + String get studyStudiesIContributeTo => 'Tutkielmat joihin olen osallisena'; + + @override + String get studyMyPublicStudies => 'Julkiset tutkielmani'; + + @override + String get studyMyPrivateStudies => 'Yksityiset tutkielmani'; + + @override + String get studyMyFavoriteStudies => 'Suosikkitutkielmani'; + + @override + String get studyWhatAreStudies => 'Mitä ovat tutkielmat?'; + + @override + String get studyAllStudies => 'Kaikki tutkielmat'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param luomat tutkielmat'; + } + + @override + String get studyNoneYet => 'Ei mitään.'; + + @override + String get studyHot => 'Suositut juuri nyt'; + + @override + String get studyDateAddedNewest => 'Julkaisupäivä (uusimmat)'; + + @override + String get studyDateAddedOldest => 'Julkaisupäivä (vanhimmat)'; + + @override + String get studyRecentlyUpdated => 'Viimeksi päivitetyt'; + + @override + String get studyMostPopular => 'Suosituimmat'; + + @override + String get studyAlphabetical => 'Aakkosjärjestyksessä'; + + @override + String get studyAddNewChapter => 'Lisää uusi luku'; + + @override + String get studyAddMembers => 'Lisää jäseniä'; + + @override + String get studyInviteToTheStudy => 'Kutsu tutkielmaan'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Kutsu vain ihmisiä, jotka tunnet ja jotka haluavat osallistua aktiivisesti.'; + + @override + String get studySearchByUsername => 'Hae käyttäjätunnuksella'; + + @override + String get studySpectator => 'Katsoja'; + + @override + String get studyContributor => 'Osallistuja'; + + @override + String get studyKick => 'Poista'; + + @override + String get studyLeaveTheStudy => 'Jätä tutkielma'; + + @override + String get studyYouAreNowAContributor => 'Olet nyt osallistuja'; + + @override + String get studyYouAreNowASpectator => 'Olet nyt katsoja'; + + @override + String get studyPgnTags => 'PGN-tunnisteet'; + + @override + String get studyLike => 'Tykkää'; + + @override + String get studyUnlike => 'Poista tykkäys'; + + @override + String get studyNewTag => 'Uusi tunniste'; + + @override + String get studyCommentThisPosition => 'Kommentoi asemaa'; + + @override + String get studyCommentThisMove => 'Kommentoi siirtoa'; + + @override + String get studyAnnotateWithGlyphs => 'Arvioi symbolein'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Luku on liian lyhyt analysoitavaksi.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Vain tutkielman osallistujat voivat pyytää tietokoneanalyysin.'; + + @override + String get studyGetAFullComputerAnalysis => 'Hanki palvelimelta täysi tietokoneanalyysi päälinjasta.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Varmista, että luku on valmis. Voit pyytää analyysiä vain kerran.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Kaikki SYNC-jäsenet pysyvät samassa asemassa'; + + @override + String get studyShareChanges => 'Anna katsojien nähdä muutokset ja tallenna ne palvelimelle'; + + @override + String get studyPlaying => 'Meneillään'; + + @override + String get studyShowEvalBar => 'Arviopalkit'; + + @override + String get studyFirst => 'Alkuun'; + + @override + String get studyPrevious => 'Edellinen'; + + @override + String get studyNext => 'Seuraava'; + + @override + String get studyLast => 'Loppuun'; + @override String get studyShareAndExport => 'Jaa & vie'; + @override + String get studyCloneStudy => 'Kloonaa'; + + @override + String get studyStudyPgn => 'Tutkielman PGN'; + + @override + String get studyDownloadAllGames => 'Lataa kaikki pelit'; + + @override + String get studyChapterPgn => 'Luvun PGN'; + + @override + String get studyCopyChapterPgn => 'Kopioi PGN'; + + @override + String get studyDownloadGame => 'Lataa peli'; + + @override + String get studyStudyUrl => 'Tutkielman URL'; + + @override + String get studyCurrentChapterUrl => 'Tämän luvun URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Voit upottaa tämän foorumiin liittämällä'; + + @override + String get studyStartAtInitialPosition => 'Aloita alkuperäisestä asemasta'; + + @override + String studyStartAtX(String param) { + return 'Aloita siirrosta $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Upota sivustoosi tai blogiisi'; + + @override + String get studyReadMoreAboutEmbedding => 'Lue lisää upottamisesta'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vain julkiset tutkielmat voidaan upottaa!'; + + @override + String get studyOpen => 'Avaa'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, sivustolta $param2'; + } + + @override + String get studyStudyNotFound => 'Tutkielmaa ei löydy'; + + @override + String get studyEditChapter => 'Muokkaa lukua'; + + @override + String get studyNewChapter => 'Uusi luku'; + + @override + String studyImportFromChapterX(String param) { + return 'Tuo luvusta $param'; + } + + @override + String get studyOrientation => 'Suunta'; + + @override + String get studyAnalysisMode => 'Analyysitila'; + + @override + String get studyPinnedChapterComment => 'Kiinnitetty lukukommentti'; + + @override + String get studySaveChapter => 'Tallenna luku'; + + @override + String get studyClearAnnotations => 'Poista kommentit'; + + @override + String get studyClearVariations => 'Tyhjennä muunnelmat'; + + @override + String get studyDeleteChapter => 'Poista luku'; + + @override + String get studyDeleteThisChapter => 'Poistetaanko tämä luku? Et voi palauttaa sitä enää!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Poista kaikki kommentit, symbolit ja piirtokuviot tästä luvusta?'; + + @override + String get studyRightUnderTheBoard => 'Heti laudan alla'; + + @override + String get studyNoPinnedComment => 'Ei'; + + @override + String get studyNormalAnalysis => 'Tavallinen analyysi'; + + @override + String get studyHideNextMoves => 'Piilota tulevat siirrot'; + + @override + String get studyInteractiveLesson => 'Interaktiivinen oppitunti'; + + @override + String studyChapterX(String param) { + return 'Luku $param'; + } + + @override + String get studyEmpty => 'Tyhjä'; + + @override + String get studyStartFromInitialPosition => 'Aloita alkuasemasta'; + + @override + String get studyEditor => 'Editori'; + + @override + String get studyStartFromCustomPosition => 'Aloita haluamastasi asemasta'; + + @override + String get studyLoadAGameByUrl => 'Lataa peli URL:stä'; + + @override + String get studyLoadAPositionFromFen => 'Lataa asema FEN:istä'; + + @override + String get studyLoadAGameFromPgn => 'Ota peli PGN:stä'; + + @override + String get studyAutomatic => 'Automaattinen'; + + @override + String get studyUrlOfTheGame => 'URL peliin'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lataa peli lähteestä $param1 tai $param2'; + } + + @override + String get studyCreateChapter => 'Aloita luku'; + + @override + String get studyCreateStudy => 'Luo tutkielma'; + + @override + String get studyEditStudy => 'Muokkaa tutkielmaa'; + + @override + String get studyVisibility => 'Näkyvyys'; + + @override + String get studyPublic => 'Julkinen'; + + @override + String get studyUnlisted => 'Listaamaton'; + + @override + String get studyInviteOnly => 'Vain kutsutut'; + + @override + String get studyAllowCloning => 'Salli kloonaus'; + + @override + String get studyNobody => 'Ei kukaan'; + + @override + String get studyOnlyMe => 'Vain minä'; + + @override + String get studyContributors => 'Osallistujat'; + + @override + String get studyMembers => 'Jäsenet'; + + @override + String get studyEveryone => 'Kaikki'; + + @override + String get studyEnableSync => 'Synkronointi käyttöön'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Kyllä: pidä kaikki samassa asemassa'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ei: anna ihmisten selata vapaasti'; + + @override + String get studyPinnedStudyComment => 'Kiinnitetty tutkielmakommentti'; + @override String get studyStart => 'Aloita'; + + @override + String get studySave => 'Tallenna'; + + @override + String get studyClearChat => 'Tyhjennä keskustelu'; + + @override + String get studyDeleteTheStudyChatHistory => 'Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää!'; + + @override + String get studyDeleteStudy => 'Poista tutkielma'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Poistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Missä haluat tutkia tätä?'; + + @override + String get studyGoodMove => 'Hyvä siirto'; + + @override + String get studyMistake => 'Virhe'; + + @override + String get studyBrilliantMove => 'Loistava siirto'; + + @override + String get studyBlunder => 'Vakava virhe'; + + @override + String get studyInterestingMove => 'Mielenkiintoinen siirto'; + + @override + String get studyDubiousMove => 'Kyseenalainen siirto'; + + @override + String get studyOnlyMove => 'Ainoa siirto'; + + @override + String get studyZugzwang => 'Siirtopakko'; + + @override + String get studyEqualPosition => 'Tasainen asema'; + + @override + String get studyUnclearPosition => 'Epäselvä asema'; + + @override + String get studyWhiteIsSlightlyBetter => 'Valkealla on pieni etu'; + + @override + String get studyBlackIsSlightlyBetter => 'Mustalla on pieni etu'; + + @override + String get studyWhiteIsBetter => 'Valkealla on etu'; + + @override + String get studyBlackIsBetter => 'Mustalla on etu'; + + @override + String get studyWhiteIsWinning => 'Valkea on voitolla'; + + @override + String get studyBlackIsWinning => 'Musta on voitolla'; + + @override + String get studyNovelty => 'Uutuus'; + + @override + String get studyDevelopment => 'Kehitys'; + + @override + String get studyInitiative => 'Aloite'; + + @override + String get studyAttack => 'Hyökkäys'; + + @override + String get studyCounterplay => 'Vastapeli'; + + @override + String get studyTimeTrouble => 'Aikapula'; + + @override + String get studyWithCompensation => 'Kompensaatio'; + + @override + String get studyWithTheIdea => 'Ideana'; + + @override + String get studyNextChapter => 'Seuraava luku'; + + @override + String get studyPrevChapter => 'Edellinen luku'; + + @override + String get studyStudyActions => 'Tutkielmatoiminnot'; + + @override + String get studyTopics => 'Aiheet'; + + @override + String get studyMyTopics => 'Omat aiheeni'; + + @override + String get studyPopularTopics => 'Suositut aiheet'; + + @override + String get studyManageTopics => 'Aiheiden hallinta'; + + @override + String get studyBack => 'Takaisin'; + + @override + String get studyPlayAgain => 'Pelaa uudelleen'; + + @override + String get studyWhatWouldYouPlay => 'Mitä pelaisit tässä asemassa?'; + + @override + String get studyYouCompletedThisLesson => 'Onnittelut! Olet suorittanut tämän oppiaiheen.'; + + @override + String studyPerPage(String param) { + return '$param per sivu'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lukua', + one: '$count luku', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count peliä', + one: '$count peli', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jäsentä', + one: '$count jäsen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liitä PGN tähän, enintään $count peliä', + one: 'Liitä PGN tähän, enintään $count peli', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'juuri äsken'; + + @override + String get timeagoRightNow => 'juuri nyt'; + + @override + String get timeagoCompleted => 'suoritettu'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sekunnin kuluttua', + one: '$count sekunnin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuutin kuluttua', + one: '$count minuutin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tunnin kuluttua', + one: '$count tunnin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päivän kuluttua', + one: '$count päivän kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count viikon kuluttua', + one: '$count viikon kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuukauden kuluttua', + one: '$count kuukauden kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vuoden kuluttua', + one: '$count vuoden kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuuttia sitten', + one: '$count minuutti sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuntia sitten', + one: '$count tunti sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päivää sitten', + one: '$count päivä sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count viikkoa sitten', + one: '$count viikko sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuukautta sitten', + one: '$count kuukausi sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vuotta sitten', + one: '$count vuosi sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuuttia jäljellä', + one: '$count minuutti jäljellä', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuntia jäljellä', + one: '$count tunti jäljellä', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index cc6dc465d9..fea75bf8bf 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsFo extends AppLocalizations { AppLocalizationsFo([String locale = 'fo']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsFo extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Virkni'; @@ -246,6 +243,17 @@ class AppLocalizationsFo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsFo extends AppLocalizations { @override String get broadcastBroadcasts => 'Sendingar'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Beinleiðis sendingar frá kappingum'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nýggj beinleiðis sending'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Í gongd'; + + @override + String get broadcastUpcoming => 'Komandi'; + + @override + String get broadcastCompleted => 'Liðug sending'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Nummar á umfari'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Fullfíggjað lýsing av tiltaki'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfrí long lýsing av sending. $param1 er tøkt. Longdin má vera styttri enn $param2 bókstavir.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL-leinki, ið Lichess fer at kanna til tess at fáa PGN dagføringar. Leinkið nýtist at vera alment atkomiligt á alnetinum.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfrítt, um tú veitst, nær tiltakið byrjar'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Talvklokka'; @@ -750,6 +1008,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Uppgávur'; @@ -1388,10 +1649,10 @@ class AppLocalizationsFo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Mótleikarin hevur avmarkaðar møguleikar at flyta, og allir leikir gera støðu hansara verri.'; @override - String get puzzleThemeHealthyMix => 'Sunt bland'; + String get puzzleThemeMix => 'Sunt bland'; @override - String get puzzleThemeHealthyMixDescription => 'Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.'; + String get puzzleThemeMixDescription => 'Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1765,9 +2026,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get byCPL => 'Við CPL'; - @override - String get openStudy => 'Lat rannsókn upp'; - @override String get enable => 'Loyv'; @@ -1795,9 +2053,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get removesTheDepthLimit => 'Tekur dýpdaravmarkingar burtur, og heldur telduna hjá tær heita'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Bukkur'; @@ -2061,6 +2316,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get gamesPlayed => 'Talv telvað'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ógilda'; @@ -2435,9 +2693,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get unblock => 'Forða ikki'; - @override - String get followsYou => 'Fylgir tær'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 byrjaði at fylgja $param2'; @@ -2770,7 +3025,13 @@ class AppLocalizationsFo extends AppLocalizations { String get other => 'Annað'; @override - String get reportDescriptionHelp => 'Flyt leinkið til talvið ella talvini higar, og greið frá, hvat bagir atburðinum hjá brúkaranum. Skriva ikki bert \"hann snýtir\", men sig okkum, hvussu tú komst til hesa niðurstøðu. Fráboðan tín verður skjótari viðgjørd, um hon verður skrivað á enskum.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Útvega leinki til í minsta lagi eitt talv, har snýtt varð.'; @@ -4075,6 +4336,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4721,9 +4985,693 @@ class AppLocalizationsFo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess stroymarar'; + @override + String get studyPrivate => 'Egin (privat)'; + + @override + String get studyMyStudies => 'Mínar rannsóknir'; + + @override + String get studyStudiesIContributeTo => 'Rannsóknir, eg gevi mítt íkast til'; + + @override + String get studyMyPublicStudies => 'Mínar almennu rannsóknir'; + + @override + String get studyMyPrivateStudies => 'Mínar egnu rannsóknir'; + + @override + String get studyMyFavoriteStudies => 'Mínar yndisrannsóknir'; + + @override + String get studyWhatAreStudies => 'Hvat eru rannsóknir?'; + + @override + String get studyAllStudies => 'Allar rannsóknir'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param stovnaði hesar rannsóknir'; + } + + @override + String get studyNoneYet => 'Ongar enn.'; + + @override + String get studyHot => 'Heitar'; + + @override + String get studyDateAddedNewest => 'Eftir dagfesting (nýggjastu)'; + + @override + String get studyDateAddedOldest => 'Eftir dagfesting (eldstu)'; + + @override + String get studyRecentlyUpdated => 'Nýliga dagførdar'; + + @override + String get studyMostPopular => 'Best dámdu'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Skoyt nýggjan kapittul upp í'; + + @override + String get studyAddMembers => 'Legg limir aftrat'; + + @override + String get studyInviteToTheStudy => 'Bjóða uppí rannsóknina'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bjóða vinaliga bert fólki, tú kennir, og sum vilja taka virknan lut í rannsóknini.'; + + @override + String get studySearchByUsername => 'Leita eftir brúkaranavni'; + + @override + String get studySpectator => 'Áskoðari'; + + @override + String get studyContributor => 'Gevur íkast'; + + @override + String get studyKick => 'Koyr úr'; + + @override + String get studyLeaveTheStudy => 'Far úr rannsóknini'; + + @override + String get studyYouAreNowAContributor => 'Tú ert nú ein, ið leggur aftrat rannsóknini'; + + @override + String get studyYouAreNowASpectator => 'Tú ert nú áskoðari'; + + @override + String get studyPgnTags => 'PGN-frámerki'; + + @override + String get studyLike => 'Dáma'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Nýtt frámerki'; + + @override + String get studyCommentThisPosition => 'Viðmerk hesa støðuna'; + + @override + String get studyCommentThisMove => 'Viðmerk henda leikin'; + + @override + String get studyAnnotateWithGlyphs => 'Skriva við teknum'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittulin er ov stuttur til at verða greinaður.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Bert tey, ið geva sítt íkast til rannsóknina, kunnu biðja um eina teldugreining.'; + + @override + String get studyGetAFullComputerAnalysis => 'Fá eina fullfíggjaða teldugreining av høvuðsbrigdinum frá ambætaranum.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Tryggja tær, at kapittulin er fullfíggjaður. Tú kanst bert biðja um greining eina ferð.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Allir SYNC-limir verða verandi í somu støðu'; + + @override + String get studyShareChanges => 'Deil broytingar við áskoðarar, og goym tær á ambætaranum'; + + @override + String get studyPlaying => 'Í gongd'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Fyrsta'; + + @override + String get studyPrevious => 'Undanfarna'; + + @override + String get studyNext => 'Næsta'; + + @override + String get studyLast => 'Síðsta'; + @override String get studyShareAndExport => 'Deil & flyt út'; + @override + String get studyCloneStudy => 'Klona'; + + @override + String get studyStudyPgn => 'PGN rannsókn'; + + @override + String get studyDownloadAllGames => 'Tak øll talv niður'; + + @override + String get studyChapterPgn => 'PGN kapittul'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Tak talv niður'; + + @override + String get studyStudyUrl => 'URL rannsókn'; + + @override + String get studyCurrentChapterUrl => 'Núverandi URL partur'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Tú kanst seta hetta inn í torgið at sýna tað har'; + + @override + String get studyStartAtInitialPosition => 'Byrja við byrjanarstøðuni'; + + @override + String studyStartAtX(String param) { + return 'Byrja við $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Fell inn í heimasíðu tína ella blogg tín'; + + @override + String get studyReadMoreAboutEmbedding => 'Les meira um at fella inn í'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Bert almennar rannsóknir kunnu verða feldar inn í!'; + + @override + String get studyOpen => 'Lat upp'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 fekk tær $param1 til vegar'; + } + + @override + String get studyStudyNotFound => 'Rannsókn ikki funnin'; + + @override + String get studyEditChapter => 'Broyt kapittul'; + + @override + String get studyNewChapter => 'Nýggjur kapittul'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Helling'; + + @override + String get studyAnalysisMode => 'Greiningarstøða'; + + @override + String get studyPinnedChapterComment => 'Føst viðmerking til kapittulin'; + + @override + String get studySaveChapter => 'Goym kapittulin'; + + @override + String get studyClearAnnotations => 'Strika viðmerkingar'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Strika kapittul'; + + @override + String get studyDeleteThisChapter => 'Strika henda kapittulin? Til ber ikki at angra!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Skulu allar viðmerkingar, øll tekn og teknað skap strikast úr hesum kapitli?'; + + @override + String get studyRightUnderTheBoard => 'Beint undir talvborðinum'; + + @override + String get studyNoPinnedComment => 'Einki'; + + @override + String get studyNormalAnalysis => 'Vanlig greining'; + + @override + String get studyHideNextMoves => 'Fjal næstu leikirnar'; + + @override + String get studyInteractiveLesson => 'Samvirkin frálæra'; + + @override + String studyChapterX(String param) { + return 'Kapittul $param'; + } + + @override + String get studyEmpty => 'Tómur'; + + @override + String get studyStartFromInitialPosition => 'Byrja við byrjanarstøðuni'; + + @override + String get studyEditor => 'Ritstjóri'; + + @override + String get studyStartFromCustomPosition => 'Byrja við støðu, ið brúkari ger av'; + + @override + String get studyLoadAGameByUrl => 'Les inn talv frá URL'; + + @override + String get studyLoadAPositionFromFen => 'Les inn talvstøðu frá FEN'; + + @override + String get studyLoadAGameFromPgn => 'Les inn talv frá PGN'; + + @override + String get studyAutomatic => 'Sjálvvirkið'; + + @override + String get studyUrlOfTheGame => 'URL fyri talvini'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Les talv inn frá $param1 ella $param2'; + } + + @override + String get studyCreateChapter => 'Stovna kapittul'; + + @override + String get studyCreateStudy => 'Stovna rannsókn'; + + @override + String get studyEditStudy => 'Ritstjórna rannsókn'; + + @override + String get studyVisibility => 'Sýni'; + + @override + String get studyPublic => 'Almen'; + + @override + String get studyUnlisted => 'Ikki skrásett'; + + @override + String get studyInviteOnly => 'Bert innboðin'; + + @override + String get studyAllowCloning => 'Loyv kloning'; + + @override + String get studyNobody => 'Eingin'; + + @override + String get studyOnlyMe => 'Bert eg'; + + @override + String get studyContributors => 'Luttakarar'; + + @override + String get studyMembers => 'Limir'; + + @override + String get studyEveryone => 'Øll'; + + @override + String get studyEnableSync => 'Samstilling møgulig'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: varðveit øll í somu støðu'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: lat fólk kaga frítt'; + + @override + String get studyPinnedStudyComment => 'Føst rannsóknarviðmerking'; + @override String get studyStart => 'Byrja'; + + @override + String get studySave => 'Goym'; + + @override + String get studyClearChat => 'Rudda kjatt'; + + @override + String get studyDeleteTheStudyChatHistory => 'Skal kjattsøgan í rannsóknini strikast? Til ber ikki at angra!'; + + @override + String get studyDeleteStudy => 'Burturbein rannsókn'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvar vilt tú rannsaka hatta?'; + + @override + String get studyGoodMove => 'Góður leikur'; + + @override + String get studyMistake => 'Mistak'; + + @override + String get studyBrilliantMove => 'Framúrskarandi leikur'; + + @override + String get studyBlunder => 'Bukkur'; + + @override + String get studyInterestingMove => 'Áhugaverdur leikur'; + + @override + String get studyDubiousMove => 'Ivasamur leikur'; + + @override + String get studyOnlyMove => 'Einasti leikur'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'Hvítur stendur til at vinna'; + + @override + String get studyBlackIsWinning => 'Svartur stendur til at vinna'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitlar', + one: '$count kapittul', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count talv', + one: '$count talv', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count limir', + one: '$count limur', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Set PGN tekstin hjá tær inn her, upp til $count talv', + one: 'Set PGN tekstin hjá tær inn her, upp til $count talv', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'just now'; + + @override + String get timeagoRightNow => 'right now'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 30bfa85b63..5f495100be 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); @override - String get mobileHomeTab => 'Accueil'; + String get mobileAllGames => 'Toutes les parties'; @override - String get mobilePuzzlesTab => 'Problèmes'; + String get mobileAreYouSure => 'Êtes-vous sûr(e) ?'; @override - String get mobileToolsTab => 'Outils'; + String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; @override - String get mobileWatchTab => 'Regarder'; + String get mobileClearButton => 'Effacer'; @override - String get mobileSettingsTab => 'Paramètres'; + String get mobileCorrespondenceClearSavedMove => 'Effacer les coups enregistrés'; @override - String get mobileMustBeLoggedIn => 'Vous devez être connecté pour voir cette page.'; + String get mobileCustomGameJoinAGame => 'Joindre une partie'; @override - String get mobileSystemColors => 'Couleurs du système'; + String get mobileFeedbackButton => 'Commentaires'; @override - String get mobileFeedbackButton => 'Commentaires'; + String mobileGreeting(String param) { + return 'Bonjour $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Bonjour'; @override - String get mobileSettingsHapticFeedback => 'Mode vibration'; + String get mobileHideVariation => 'Masquer les variantes'; @override - String get mobileSettingsImmersiveMode => 'Mode plein écran'; + String get mobileHomeTab => 'Accueil'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Masquer l\'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l\'interface système sur les bords de l\'écran vous gênent. S\'applique aux écrans de la partie et des problèmes (Puzzle Storm).'; + String get mobileLiveStreamers => 'Diffuseurs en direct'; @override - String get mobileNotFollowingAnyUser => 'Vous ne suivez aucun utilisateur.'; + String get mobileMustBeLoggedIn => 'Vous devez être connecté pour voir cette page.'; @override - String get mobileAllGames => 'Toutes les parties'; + String get mobileNoSearchResults => 'Aucun résultat'; @override - String get mobileRecentSearches => 'Recherches récentes'; + String get mobileNotFollowingAnyUser => 'Vous ne suivez aucun utilisateur.'; @override - String get mobileClearButton => 'Effacer'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Aucun résultat'; + String get mobilePrefMagnifyDraggedPiece => 'Grossir la pièce déplacée'; @override - String get mobileAreYouSure => 'Êtes-vous sûr(e) ?'; + String get mobilePuzzleStormConfirmEndRun => 'Voulez-vous mettre fin à cette série?'; @override - String get mobilePuzzleStreakAbortWarning => 'Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.'; + String get mobilePuzzleStormFilterNothingToShow => 'Rien à afficher. Veuillez changer les filtres.'; @override String get mobilePuzzleStormNothingToShow => 'Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).'; @override - String get mobileSharePuzzle => 'Partager ce problème'; + String get mobilePuzzleStormSubtitle => 'Faites un maximum de problèmes en 3 minutes.'; @override - String get mobileShareGameURL => 'Partager l\'URL de la partie'; + String get mobilePuzzleStreakAbortWarning => 'Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.'; @override - String get mobileShareGamePGN => 'Partager le PGN'; + String get mobilePuzzleThemesSubtitle => 'Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.'; @override - String get mobileSharePositionAsFEN => 'Partager la position FEN'; + String get mobilePuzzlesTab => 'Problèmes'; @override - String get mobileShowVariations => 'Afficher les variantes'; + String get mobileRecentSearches => 'Recherches récentes'; @override - String get mobileHideVariation => 'Masquer les variantes'; + String get mobileSettingsHapticFeedback => 'Mode vibration'; @override - String get mobileShowComments => 'Afficher les commentaires'; + String get mobileSettingsImmersiveMode => 'Mode plein écran'; @override - String get mobilePuzzleStormConfirmEndRun => 'Voulez-vous mettre fin à cette série?'; + String get mobileSettingsImmersiveModeSubtitle => 'Masquer l\'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l\'interface système sur les bords de l\'écran vous gênent. S\'applique aux écrans de la partie et des problèmes (Puzzle Storm).'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Rien à afficher. Veuillez changer les filtres.'; + String get mobileSettingsTab => 'Paramètres'; @override - String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; + String get mobileShareGamePGN => 'Partager le PGN'; @override - String get mobileCancelDrawOffer => 'Annuler la proposition de nulle'; + String get mobileShareGameURL => 'Partager l\'URL de la partie'; @override - String get mobileWaitingForOpponentToJoin => 'En attente d\'un adversaire...'; + String get mobileSharePositionAsFEN => 'Partager la position FEN'; @override - String get mobileBlindfoldMode => 'Partie à l\'aveugle'; + String get mobileSharePuzzle => 'Partager ce problème'; @override - String get mobileLiveStreamers => 'Diffuseurs en direct'; + String get mobileShowComments => 'Afficher les commentaires'; @override - String get mobileCustomGameJoinAGame => 'Joindre une partie'; + String get mobileShowResult => 'Afficher le résultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Effacer les coups enregistrés'; + String get mobileShowVariations => 'Afficher les variantes'; @override String get mobileSomethingWentWrong => 'Une erreur s\'est produite.'; @override - String get mobileShowResult => 'Afficher le résultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.'; + String get mobileSystemColors => 'Couleurs du système'; @override - String get mobilePuzzleStormSubtitle => 'Faites un maximum de problèmes en 3 minutes.'; + String get mobileTheme => 'Thème'; @override - String mobileGreeting(String param) { - return 'Bonjour $param'; - } + String get mobileToolsTab => 'Outils'; @override - String get mobileGreetingWithoutName => 'Bonjour'; + String get mobileWaitingForOpponentToJoin => 'En attente d\'un adversaire...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Regarder'; @override String get activityActivity => 'Activité'; @@ -246,6 +243,17 @@ class AppLocalizationsFr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parties $param2 par correspondance terminées', + one: '$count partie $param2 par correspondance terminée', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsFr extends AppLocalizations { @override String get broadcastBroadcasts => 'Diffusions'; + @override + String get broadcastMyBroadcasts => 'Ma diffusion'; + @override String get broadcastLiveBroadcasts => 'Diffusions de tournois en direct'; + @override + String get broadcastBroadcastCalendar => 'Calendrier des diffusions'; + + @override + String get broadcastNewBroadcast => 'Nouvelle diffusion en direct'; + + @override + String get broadcastSubscribedBroadcasts => 'Diffusions suivies'; + + @override + String get broadcastAboutBroadcasts => 'À propos des diffusions'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Comment utiliser les diffusions dans Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nouvelle ronde aura les mêmes participants et contributeurs que la précédente.'; + + @override + String get broadcastAddRound => 'Ajouter une ronde'; + + @override + String get broadcastOngoing => 'En cours'; + + @override + String get broadcastUpcoming => 'À venir'; + + @override + String get broadcastCompleted => 'Terminé'; + + @override + String get broadcastCompletedHelp => 'Lichess détecte la fin des rondes en fonction des parties sources. Utilisez cette option s\'il n\'y a pas de source.'; + + @override + String get broadcastRoundName => 'Nom de la ronde'; + + @override + String get broadcastRoundNumber => 'Numéro de la ronde'; + + @override + String get broadcastTournamentName => 'Nom du tournoi'; + + @override + String get broadcastTournamentDescription => 'Brève description du tournoi'; + + @override + String get broadcastFullDescription => 'Description complète de l\'événement'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Description détaillée et optionnelle de la diffusion. $param1 est disponible. La longueur doit être inférieure à $param2 caractères.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL source de la partie en PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess interrogera pour obtenir les mises à jour du PGN. Elle doit être accessible publiquement depuis Internet.'; + + @override + String get broadcastSourceGameIds => 'Jusqu\'à 64 ID de partie Lichess séparés par des espaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Date de début du tournoi (fuseau horaire local) : $param'; + } + + @override + String get broadcastStartDateHelp => 'Facultatif, si vous savez quand l\'événement commence'; + + @override + String get broadcastCurrentGameUrl => 'URL de la partie en cours'; + + @override + String get broadcastDownloadAllRounds => 'Télécharger toutes les rondes'; + + @override + String get broadcastResetRound => 'Réinitialiser cette ronde'; + + @override + String get broadcastDeleteRound => 'Supprimer cette ronde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Supprimer définitivement la ronde et ses parties.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Supprimer toutes les parties de la ronde. La source doit être active pour recréer les parties.'; + + @override + String get broadcastEditRoundStudy => 'Modifier l\'étude de la ronde'; + + @override + String get broadcastDeleteTournament => 'Supprimer ce tournoi'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Supprimer définitivement le tournoi, toutes ses rondes et toutes ses parties.'; + + @override + String get broadcastShowScores => 'Afficher les résultats des joueurs en fonction des résultats des parties'; + + @override + String get broadcastReplacePlayerTags => 'Facultatif : remplacer les noms des joueurs, les classements et les titres'; + + @override + String get broadcastFideFederations => 'Fédérations FIDE'; + + @override + String get broadcastTop10Rating => '10 plus hauts classements'; + + @override + String get broadcastFidePlayers => 'Joueurs FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Joueur FIDE introuvable'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Fédération'; + + @override + String get broadcastAgeThisYear => 'Âge cette année'; + + @override + String get broadcastUnrated => 'Non classé'; + + @override + String get broadcastRecentTournaments => 'Tournois récents'; + + @override + String get broadcastOpenLichess => 'Ouvrir dans Lichess'; + + @override + String get broadcastTeams => 'Équipes'; + + @override + String get broadcastBoards => 'Échiquiers'; + + @override + String get broadcastOverview => 'Survol'; + + @override + String get broadcastSubscribeTitle => 'Abonnez-vous pour être averti du début de chaque ronde. Vous pouvez basculer entre une sonnerie ou une notification poussée pour les diffusions dans les préférences de votre compte.'; + + @override + String get broadcastUploadImage => 'Téléverser une image pour le tournoi'; + + @override + String get broadcastNoBoardsYet => 'Pas d\'échiquiers pour le moment. Ils s\'afficheront lorsque les parties seront téléversées.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Les échiquiers sont chargés à partir d\'une source ou de l\'$param.'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Commence après la $param'; + } + + @override + String get broadcastStartVerySoon => 'La diffusion commencera très bientôt.'; + + @override + String get broadcastNotYetStarted => 'La diffusion n\'a pas encore commencé.'; + + @override + String get broadcastOfficialWebsite => 'Site Web officiel'; + + @override + String get broadcastStandings => 'Classement'; + + @override + String get broadcastOfficialStandings => 'Résultats officiels'; + + @override + String broadcastIframeHelp(String param) { + return 'Plus d\'options sur la $param'; + } + + @override + String get broadcastWebmastersPage => 'page des webmestres'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Source PGN publique en temps réel pour cette ronde. Nous offrons également un $param pour permettre une synchronisation rapide et efficace.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Intégrer cette diffusion dans votre site Web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Intégrer la $param dans votre site Web'; + } + + @override + String get broadcastRatingDiff => 'Différence de cote'; + + @override + String get broadcastGamesThisTournament => 'Partie de ce tournoi'; + + @override + String get broadcastScore => 'Résultat'; + + @override + String get broadcastAllTeams => 'Toutes les équipes'; + + @override + String get broadcastTournamentFormat => 'Format du tournoi'; + + @override + String get broadcastTournamentLocation => 'Lieu du tournoi'; + + @override + String get broadcastTopPlayers => 'Meilleurs joueurs'; + + @override + String get broadcastTimezone => 'Fuseau horaire'; + + @override + String get broadcastFideRatingCategory => 'Catégorie FIDE'; + + @override + String get broadcastOptionalDetails => 'Informations facultatives'; + + @override + String get broadcastPastBroadcasts => 'Diffusions passées'; + + @override + String get broadcastAllBroadcastsByMonth => 'Voir les diffusions par mois'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count diffusions', + one: '$count diffusion', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Défis : $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get preferencesInGameOnly => 'Seulement durant la partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Pendule'; @@ -750,6 +1008,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Son de notification'; + @override + String get preferencesBlindfold => 'Partie à l\'aveugle'; + @override String get puzzlePuzzles => 'Problèmes'; @@ -1390,10 +1651,10 @@ class AppLocalizationsFr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'L\'adversaire est limité dans les mouvements qu\'il peut effectuer, et tous les coups aggravent sa position.'; @override - String get puzzleThemeHealthyMix => 'Divers'; + String get puzzleThemeMix => 'Problèmes variés'; @override - String get puzzleThemeHealthyMixDescription => 'Un peu de tout. Vous ne savez pas à quoi vous attendre ! Comme dans une vraie partie.'; + String get puzzleThemeMixDescription => 'Un peu de tout. Vous ne savez pas à quoi vous attendre! Comme dans une vraie partie.'; @override String get puzzleThemePlayerGames => 'Parties de joueurs'; @@ -1767,9 +2028,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get byCPL => 'Par erreurs'; - @override - String get openStudy => 'Ouvrir l\'analyse'; - @override String get enable => 'Activée'; @@ -1797,9 +2055,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get removesTheDepthLimit => 'Désactive la profondeur limitée et fait chauffer votre ordinateur'; - @override - String get engineManager => 'Gestionnaire de moteur d\'analyse'; - @override String get blunder => 'Gaffe'; @@ -2063,6 +2318,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get gamesPlayed => 'Parties jouées'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annuler'; @@ -2437,9 +2695,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get unblock => 'Débloquer'; - @override - String get followsYou => 'Vous suit'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 a suivi $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsFr extends AppLocalizations { String get other => 'Autre'; @override - String get reportDescriptionHelp => 'Copiez le(s) lien(s) vers les parties et expliquez en quoi le comportement de cet utilisateur est inapproprié. Ne dites pas juste \"il triche\", mais expliquez comment vous êtes arrivé à cette conclusion. Votre rapport sera traité plus vite s\'il est écrit en anglais.'; + String get reportCheatBoostHelp => 'Collez le lien vers la ou les parties et expliquez pourquoi le comportement de l\'utilisateur est inapproprié. Ne dites pas juste « il triche »; expliquez comment vous êtes arrivé à cette conclusion.'; + + @override + String get reportUsernameHelp => 'Expliquez pourquoi ce nom d\'utilisateur est offensant. Ne dites pas simplement qu\'il est choquant ou inapproprié; expliquez comment vous êtes arrivé à cette conclusion, surtout si l\'insulte n\'est pas claire, n\'est pas en anglais, est en argot ou a une connotation historique ou culturelle.'; + + @override + String get reportProcessedFasterInEnglish => 'Votre rapport sera traité plus rapidement s\'il est rédigé en anglais.'; @override String get error_provideOneCheatedGameLink => 'Merci de fournir au moins un lien vers une partie où il y a eu triche.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get nothingToSeeHere => 'Rien à voir ici pour le moment.'; + @override + String get stats => 'Statistiques'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsFr extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamers sur Lichess'; + @override + String get studyPrivate => 'Étude(s) privée(s)'; + + @override + String get studyMyStudies => 'Mes études'; + + @override + String get studyStudiesIContributeTo => 'Études auxquelles je participe'; + + @override + String get studyMyPublicStudies => 'Mes études publiques'; + + @override + String get studyMyPrivateStudies => 'Mes études privées'; + + @override + String get studyMyFavoriteStudies => 'Mes études favorites'; + + @override + String get studyWhatAreStudies => 'Qu\'est-ce qu\'une étude ?'; + + @override + String get studyAllStudies => 'Toutes les études'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Études créées par $param'; + } + + @override + String get studyNoneYet => 'Aucune étude.'; + + @override + String get studyHot => 'Populaire(s)'; + + @override + String get studyDateAddedNewest => 'Date d\'ajout (dernier ajout)'; + + @override + String get studyDateAddedOldest => 'Date d\'ajout (premier ajout)'; + + @override + String get studyRecentlyUpdated => 'Récemment mis à jour'; + + @override + String get studyMostPopular => 'Études les plus populaires'; + + @override + String get studyAlphabetical => 'Alphabétique'; + + @override + String get studyAddNewChapter => 'Ajouter un nouveau chapitre'; + + @override + String get studyAddMembers => 'Ajouter des membres'; + + @override + String get studyInviteToTheStudy => 'Inviter à l\'étude'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Veuillez n\'inviter que des personnes qui vous connaissent et qui souhaitent activement participer à cette étude.'; + + @override + String get studySearchByUsername => 'Rechercher par nom d\'utilisateur'; + + @override + String get studySpectator => 'Spectateur'; + + @override + String get studyContributor => 'Contributeur'; + + @override + String get studyKick => 'Éjecter'; + + @override + String get studyLeaveTheStudy => 'Quitter l\'étude'; + + @override + String get studyYouAreNowAContributor => 'Vous êtes maintenant un contributeur'; + + @override + String get studyYouAreNowASpectator => 'Vous êtes maintenant un spectateur'; + + @override + String get studyPgnTags => 'Étiquettes PGN'; + + @override + String get studyLike => 'Aimer'; + + @override + String get studyUnlike => 'Je n’aime pas'; + + @override + String get studyNewTag => 'Nouvelle étiquette'; + + @override + String get studyCommentThisPosition => 'Commenter la position'; + + @override + String get studyCommentThisMove => 'Commenter ce coup'; + + @override + String get studyAnnotateWithGlyphs => 'Annoter avec des symboles'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Le chapitre est trop court pour être analysé.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Seuls les contributeurs de l\'étude peuvent demander une analyse informatique.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtenez une analyse en ligne complète de la ligne principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Assurez-vous que le chapitre est terminé. Vous ne pouvez demander l\'analyse qu\'une seule fois.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tous les membres SYNC demeurent sur la même position'; + + @override + String get studyShareChanges => 'Partager les changements avec les spectateurs et les enregistrer sur le serveur'; + + @override + String get studyPlaying => 'En cours'; + + @override + String get studyShowEvalBar => 'Barre d’évaluation'; + + @override + String get studyFirst => 'Premier'; + + @override + String get studyPrevious => 'Précédent'; + + @override + String get studyNext => 'Suivant'; + + @override + String get studyLast => 'Dernier'; + @override String get studyShareAndExport => 'Partager & exporter'; + @override + String get studyCloneStudy => 'Dupliquer'; + + @override + String get studyStudyPgn => 'PGN de l\'étude'; + + @override + String get studyDownloadAllGames => 'Télécharger toutes les parties'; + + @override + String get studyChapterPgn => 'PGN du chapitre'; + + @override + String get studyCopyChapterPgn => 'Copier le fichier PGN'; + + @override + String get studyDownloadGame => 'Télécharger la partie'; + + @override + String get studyStudyUrl => 'URL de l\'étude'; + + @override + String get studyCurrentChapterUrl => 'URL du chapitre actuel'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vous pouvez collez ce lien dans le forum afin de l’insérer'; + + @override + String get studyStartAtInitialPosition => 'Commencer à partir du début'; + + @override + String studyStartAtX(String param) { + return 'Débuter à $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Intégrer dans votre site ou blog'; + + @override + String get studyReadMoreAboutEmbedding => 'En savoir plus sur l\'intégration'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Seules les études publiques peuvent être intégrées !'; + + @override + String get studyOpen => 'Ouvrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vous est apporté par $param2'; + } + + @override + String get studyStudyNotFound => 'Étude introuvable'; + + @override + String get studyEditChapter => 'Modifier le chapitre'; + + @override + String get studyNewChapter => 'Nouveau chapitre'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer depuis $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Mode analyse'; + + @override + String get studyPinnedChapterComment => 'Commentaire du chapitre épinglé'; + + @override + String get studySaveChapter => 'Enregistrer le chapitre'; + + @override + String get studyClearAnnotations => 'Effacer les annotations'; + + @override + String get studyClearVariations => 'Supprimer les variantes'; + + @override + String get studyDeleteChapter => 'Supprimer le chapitre'; + + @override + String get studyDeleteThisChapter => 'Supprimer ce chapitre ? Cette action est irréversible !'; + + @override + String get studyClearAllCommentsInThisChapter => 'Effacer tous les commentaires et annotations dans ce chapitre ?'; + + @override + String get studyRightUnderTheBoard => 'Juste sous l\'échiquier'; + + @override + String get studyNoPinnedComment => 'Aucun'; + + @override + String get studyNormalAnalysis => 'Analyse normale'; + + @override + String get studyHideNextMoves => 'Cacher les coups suivants'; + + @override + String get studyInteractiveLesson => 'Leçon interactive'; + + @override + String studyChapterX(String param) { + return 'Chapitre : $param'; + } + + @override + String get studyEmpty => 'Par défaut'; + + @override + String get studyStartFromInitialPosition => 'Commencer à partir du début'; + + @override + String get studyEditor => 'Editeur'; + + @override + String get studyStartFromCustomPosition => 'Commencer à partir d\'une position personnalisée'; + + @override + String get studyLoadAGameByUrl => 'Charger des parties à partir d\'une URL'; + + @override + String get studyLoadAPositionFromFen => 'Charger une position par FEN'; + + @override + String get studyLoadAGameFromPgn => 'Charger des parties par PGN'; + + @override + String get studyAutomatic => 'Automatique'; + + @override + String get studyUrlOfTheGame => 'URL des parties, une par ligne'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Charger des parties de $param1 ou $param2'; + } + + @override + String get studyCreateChapter => 'Créer un chapitre'; + + @override + String get studyCreateStudy => 'Créer une étude'; + + @override + String get studyEditStudy => 'Modifier l\'étude'; + + @override + String get studyVisibility => 'Visibilité'; + + @override + String get studyPublic => 'Publique'; + + @override + String get studyUnlisted => 'Non répertorié'; + + @override + String get studyInviteOnly => 'Sur invitation seulement'; + + @override + String get studyAllowCloning => 'Autoriser la duplication'; + + @override + String get studyNobody => 'Personne'; + + @override + String get studyOnlyMe => 'Seulement moi'; + + @override + String get studyContributors => 'Contributeurs'; + + @override + String get studyMembers => 'Membres'; + + @override + String get studyEveryone => 'Tout le monde'; + + @override + String get studyEnableSync => 'Activer la synchronisation'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Oui : garder tout le monde sur la même position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Non : laisser les gens naviguer librement'; + + @override + String get studyPinnedStudyComment => 'Commentaire d\'étude épinglé'; + @override String get studyStart => 'Commencer'; + + @override + String get studySave => 'Enregistrer'; + + @override + String get studyClearChat => 'Effacer le tchat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Supprimer l\'historique du tchat de l\'étude ? Cette action est irréversible !'; + + @override + String get studyDeleteStudy => 'Supprimer l\'étude'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Supprimer toute l’étude? Aucun retour en arrière possible! Taper le nom de l’étude pour confirmer : $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Où voulez-vous étudier cela ?'; + + @override + String get studyGoodMove => 'Bon coup'; + + @override + String get studyMistake => 'Erreur'; + + @override + String get studyBrilliantMove => 'Excellent coup'; + + @override + String get studyBlunder => 'Gaffe'; + + @override + String get studyInterestingMove => 'Coup intéressant'; + + @override + String get studyDubiousMove => 'Coup douteux'; + + @override + String get studyOnlyMove => 'Seul coup'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Position égale'; + + @override + String get studyUnclearPosition => 'Position incertaine'; + + @override + String get studyWhiteIsSlightlyBetter => 'Les Blancs sont un peu mieux'; + + @override + String get studyBlackIsSlightlyBetter => 'Les Noirs sont un peu mieux'; + + @override + String get studyWhiteIsBetter => 'Les Blancs sont mieux'; + + @override + String get studyBlackIsBetter => 'Les Noirs sont mieux'; + + @override + String get studyWhiteIsWinning => 'Les Blancs gagnent'; + + @override + String get studyBlackIsWinning => 'Les Noirs gagnent'; + + @override + String get studyNovelty => 'Nouveauté'; + + @override + String get studyDevelopment => 'Développement'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attaque'; + + @override + String get studyCounterplay => 'Contre-jeu'; + + @override + String get studyTimeTrouble => 'Pression de temps'; + + @override + String get studyWithCompensation => 'Avec compensation'; + + @override + String get studyWithTheIdea => 'Avec l\'idée'; + + @override + String get studyNextChapter => 'Chapitre suivant'; + + @override + String get studyPrevChapter => 'Chapitre précédent'; + + @override + String get studyStudyActions => 'Options pour les études'; + + @override + String get studyTopics => 'Thèmes'; + + @override + String get studyMyTopics => 'Mes thèmes'; + + @override + String get studyPopularTopics => 'Thèmes populaires'; + + @override + String get studyManageTopics => 'Gérer les thèmes'; + + @override + String get studyBack => 'Retour'; + + @override + String get studyPlayAgain => 'Jouer à nouveau'; + + @override + String get studyWhatWouldYouPlay => 'Que joueriez-vous dans cette position ?'; + + @override + String get studyYouCompletedThisLesson => 'Félicitations ! Vous avez terminé ce cours.'; + + @override + String studyPerPage(String param) { + return '$param par page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapitres', + one: '$count chapitre', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parties', + one: '$count partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membres', + one: '$count membre', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Collez votre texte PGN ici, jusqu\'à $count parties', + one: 'Collez votre texte PGN ici, jusqu\'à $count partie', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'Maintenant'; + + @override + String get timeagoRightNow => 'à l\'instant'; + + @override + String get timeagoCompleted => 'terminé'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count secondes', + one: 'dans $count seconde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count minutes', + one: 'dans $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count heures', + one: 'dans $count heure', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count jours', + one: 'dans $count jour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count semaines', + one: 'dans $count semaine', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count mois', + one: 'dans $count mois', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count ans', + one: 'dans $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count minutes', + one: 'il y a $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count heures', + one: 'il y a $count heure', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count jours', + one: 'il y a $count jour', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count semaines', + one: 'il y a $count semaine', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count mois', + one: 'il y a $count mois', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count ans', + one: 'il y a $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes restantes', + one: '$count minute restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count heures restantes', + one: '$count heure restante', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index 4aece38474..a00459924c 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsGa extends AppLocalizations { AppLocalizationsGa([String locale = 'ga']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsGa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Gníomhaíocht'; @@ -270,6 +267,17 @@ class AppLocalizationsGa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -399,9 +407,256 @@ class AppLocalizationsGa extends AppLocalizations { @override String get broadcastBroadcasts => 'Craoltaí'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Craoltaí beo comórtais'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Craoladh beo nua'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Cuir babhta leis'; + + @override + String get broadcastOngoing => 'Leanúnach'; + + @override + String get broadcastUpcoming => 'Le teacht'; + + @override + String get broadcastCompleted => 'Críochnaithe'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ainm babhta'; + + @override + String get broadcastRoundNumber => 'Uimhir bhabhta'; + + @override + String get broadcastTournamentName => 'Ainm comórtas'; + + @override + String get broadcastTournamentDescription => 'Cur síos gairid ar an gcomórtas'; + + @override + String get broadcastFullDescription => 'Cur síos iomlán ar an ócáid'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Cur síos fada roghnach ar an craoladh. Tá $param1 ar fáil. Caithfidh an fad a bheith níos lú ná $param2 carachtar.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL a seiceálfaidh Lichess chun PGN nuashonruithe a fháil. Caithfidh sé a bheith le féiceáil go poiblí ón Idirlíon.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Roghnach, má tá a fhios agat cathain a thosóidh an ócáid'; + + @override + String get broadcastCurrentGameUrl => 'URL cluiche reatha'; + + @override + String get broadcastDownloadAllRounds => 'Íoslódáil gach babhta'; + + @override + String get broadcastResetRound => 'Athshocraigh an babhta seo'; + + @override + String get broadcastDeleteRound => 'Scrios an babhta seo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Scrios go cinntitheach an babhta agus a chuid cluichí.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Scrios gach cluiche den bhabhta seo. Caithfidh an fhoinse a bheith gníomhach chun iad a athchruthú.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -660,6 +915,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Clog fichille'; @@ -801,6 +1059,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Fadhbanna'; @@ -1456,10 +1717,10 @@ class AppLocalizationsGa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ciallaíonn Zugzwang gur gá le himreoir a s(h) eans a \nthógáil cé nár mhaith leis nó léi toisc gur laige a bheith a s(h) uíomh cibé beart a dhéanfaidh sé/sí. Ba mhaith leis / léi \"háram\" a rá ach níl sé sin ceadaithe.'; @override - String get puzzleThemeHealthyMix => 'Meascán sláintiúil'; + String get puzzleThemeMix => 'Meascán sláintiúil'; @override - String get puzzleThemeHealthyMixDescription => 'Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.'; + String get puzzleThemeMixDescription => 'Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.'; @override String get puzzleThemePlayerGames => 'Cluichí imreoir'; @@ -1833,9 +2094,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get byCPL => 'De réir CPL'; - @override - String get openStudy => 'Oscail staidéar'; - @override String get enable => 'Cumasaigh'; @@ -1863,9 +2121,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get removesTheDepthLimit => 'Faigheann sé réidh leis an teorainn doimhneachta, agus coinníonn sé do ríomhaire te'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Meancóg'; @@ -2129,6 +2384,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get gamesPlayed => 'Cluichí imeartha'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cealaigh'; @@ -2503,9 +2761,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get unblock => 'Bain bac de'; - @override - String get followsYou => 'Do leanúint'; - @override String xStartedFollowingY(String param1, String param2) { return 'Thosaigh $param1 ag leanúint $param2'; @@ -2838,7 +3093,13 @@ class AppLocalizationsGa extends AppLocalizations { String get other => 'Eile'; @override - String get reportDescriptionHelp => 'Greamaigh an nasc chuig an gcluiche/na cluichí agus mínigh cad atá cearr le hiompar an úsáideora. Ná habair go díreach go mbíonn \"caimiléireacht\" ar bun acu, ach inis dúinn faoin dóigh a fuair tú amach faoi. Faraor, déanfar do thuairisc a phróiseáil níos tapúla más i mBéarla atá sé.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Cuir nasc ar fáil chuig cluiche amháin ar a laghad ar tharla caimiléireacht ann le do thoil.'; @@ -4143,6 +4404,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4915,9 +5179,744 @@ class AppLocalizationsGa extends AppLocalizations { @override String get streamerLichessStreamers => 'Sruthaithe Lichess'; + @override + String get studyPrivate => 'Cé na daoine! Tá an leathanach seo príobháideach, ní féidir leat é a rochtain'; + + @override + String get studyMyStudies => 'Mo chuid staidéir'; + + @override + String get studyStudiesIContributeTo => 'Staidéir atá á n-iarraidh agam'; + + @override + String get studyMyPublicStudies => 'Mo chuid staidéir phoiblí'; + + @override + String get studyMyPrivateStudies => 'Mo chuid staidéir phríobháideacha'; + + @override + String get studyMyFavoriteStudies => 'Na staidéir is fearr liom'; + + @override + String get studyWhatAreStudies => 'Cad is staidéir ann?'; + + @override + String get studyAllStudies => 'Gach staidéar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Staidéir a chruthaigh $param'; + } + + @override + String get studyNoneYet => 'Níl aon cheann fós.'; + + @override + String get studyHot => 'Te'; + + @override + String get studyDateAddedNewest => 'Dáta curtha leis (dáta is déanaí)'; + + @override + String get studyDateAddedOldest => 'Dáta curtha leis (dáta is sinne)'; + + @override + String get studyRecentlyUpdated => 'Faisnéis nuashonraithe le déanaí'; + + @override + String get studyMostPopular => 'Móréilimh'; + + @override + String get studyAlphabetical => 'Aibítre'; + + @override + String get studyAddNewChapter => 'Cuir caibidil nua leis'; + + @override + String get studyAddMembers => 'Cuir baill leis'; + + @override + String get studyInviteToTheStudy => 'Tabhair cuireadh don staidéar'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ná tabhair cuireadh ach do dhaoine a bhfuil aithne agat orthu, agus ar mian leo go gníomhach a bheith páirteach sa staidéar seo.'; + + @override + String get studySearchByUsername => 'Cuardaigh de réir ainm úsáideora'; + + @override + String get studySpectator => 'Breathnóir'; + + @override + String get studyContributor => 'Rannpháirtí'; + + @override + String get studyKick => 'Ciceáil'; + + @override + String get studyLeaveTheStudy => 'Fág an staidéar'; + + @override + String get studyYouAreNowAContributor => 'Is ranníocóir anois tú'; + + @override + String get studyYouAreNowASpectator => 'Is lucht féachana anois tú'; + + @override + String get studyPgnTags => 'Clibeanna PGN'; + + @override + String get studyLike => 'Is maith liom'; + + @override + String get studyUnlike => 'Díthogh'; + + @override + String get studyNewTag => 'Clib nua'; + + @override + String get studyCommentThisPosition => 'Déan trácht ar an suíomh seo'; + + @override + String get studyCommentThisMove => 'Déan trácht ar an mbeart seo'; + + @override + String get studyAnnotateWithGlyphs => 'Nodaireacht le glifeanna'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Tá an chaibidil ró-ghearr le hanailís a dhéanamh uirthi.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Ní féidir ach le rannpháirtithe an staidéir anailís ríomhaire a iarraidh.'; + + @override + String get studyGetAFullComputerAnalysis => 'Faigh anailís ríomhaire iomlán ón freastalaí ar an bpríomhlíne.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bí cinnte go bhfuil an chaibidil críochnaithe. Ní féidir leat iarr ar anailís ach uair amháin.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Fanann gach ball SYNC sa suíomh céanna'; + + @override + String get studyShareChanges => 'Roinn athruithe le lucht féachana agus sábháil iad ar an freastalaí'; + + @override + String get studyPlaying => 'Ag imirt'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Céad'; + + @override + String get studyPrevious => 'Roimhe'; + + @override + String get studyNext => 'Ar aghaidh'; + + @override + String get studyLast => 'Deiridh'; + @override String get studyShareAndExport => 'Comhroinn & easpórtáil'; + @override + String get studyCloneStudy => 'Déan cóip'; + + @override + String get studyStudyPgn => 'Déan staidéar ar PGN'; + + @override + String get studyDownloadAllGames => 'Íoslódáil gach cluiche'; + + @override + String get studyChapterPgn => 'PGN caibidle'; + + @override + String get studyCopyChapterPgn => 'Cóipeáil PGN'; + + @override + String get studyDownloadGame => 'Íoslódáil cluiche'; + + @override + String get studyStudyUrl => 'URL an staidéir'; + + @override + String get studyCurrentChapterUrl => 'URL caibidil reatha'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Is féidir é seo a ghreamú san fhóram chun leabú'; + + @override + String get studyStartAtInitialPosition => 'Tosaigh ag an suíomh tosaigh'; + + @override + String studyStartAtX(String param) { + return 'Tosú ag $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Leabaithe i do shuíomh Gréasáin nó i do bhlag'; + + @override + String get studyReadMoreAboutEmbedding => 'Léigh tuilleadh faoi leabú'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ní féidir ach staidéir phoiblí a leabú!'; + + @override + String get studyOpen => 'Oscailte'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, a thugann $param2 chugat'; + } + + @override + String get studyStudyNotFound => 'Níor aimsíodh staidéar'; + + @override + String get studyEditChapter => 'Cuir caibidil in eagar'; + + @override + String get studyNewChapter => 'Caibidil nua'; + + @override + String studyImportFromChapterX(String param) { + return 'Iompórtáil ó $param'; + } + + @override + String get studyOrientation => 'Treoshuíomh'; + + @override + String get studyAnalysisMode => 'Modh anailíse'; + + @override + String get studyPinnedChapterComment => 'Trácht caibidil greamaithe'; + + @override + String get studySaveChapter => 'Sábháil caibidil'; + + @override + String get studyClearAnnotations => 'Glan anótála'; + + @override + String get studyClearVariations => 'Glan éagsúlachtaí'; + + @override + String get studyDeleteChapter => 'Scrios caibidil'; + + @override + String get studyDeleteThisChapter => 'Scrios an chaibidil seo? Níl aon dul ar ais!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Glan gach trácht, glif agus cruthanna tarraingthe sa chaibidil seo?'; + + @override + String get studyRightUnderTheBoard => 'Díreach faoin gclár'; + + @override + String get studyNoPinnedComment => 'Faic'; + + @override + String get studyNormalAnalysis => 'Gnáth-anailís'; + + @override + String get studyHideNextMoves => 'Folaigh na bearta ina dhiaidh seo'; + + @override + String get studyInteractiveLesson => 'Ceacht idirghníomhach'; + + @override + String studyChapterX(String param) { + return 'Caibidil $param'; + } + + @override + String get studyEmpty => 'Folamh'; + + @override + String get studyStartFromInitialPosition => 'Tosaigh ón suíomh tosaigh'; + + @override + String get studyEditor => 'Eagarthóir'; + + @override + String get studyStartFromCustomPosition => 'Tosaigh ón suíomh saincheaptha'; + + @override + String get studyLoadAGameByUrl => 'Lód cluichí le URLanna'; + + @override + String get studyLoadAPositionFromFen => 'Luchtaigh suíomh ó FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lódáil cluichí ó PGN'; + + @override + String get studyAutomatic => 'Uathoibríoch'; + + @override + String get studyUrlOfTheGame => 'URL na gcluichí, ceann amháin an líne'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lódáil cluichí ó $param1 nó $param2'; + } + + @override + String get studyCreateChapter => 'Cruthaigh caibidil'; + + @override + String get studyCreateStudy => 'Cruthaigh staidéar'; + + @override + String get studyEditStudy => 'Cuir staidéar in eagar'; + + @override + String get studyVisibility => 'Infheictheacht'; + + @override + String get studyPublic => 'Poiblí'; + + @override + String get studyUnlisted => 'Neamhliostaithe'; + + @override + String get studyInviteOnly => 'Tabhair cuireadh amháin'; + + @override + String get studyAllowCloning => 'Lig clónáil'; + + @override + String get studyNobody => 'Níl einne'; + + @override + String get studyOnlyMe => 'Mise amháin'; + + @override + String get studyContributors => 'Rannpháirtithe'; + + @override + String get studyMembers => 'Baill'; + + @override + String get studyEveryone => 'Gach duine'; + + @override + String get studyEnableSync => 'Cuir sinc ar chumas'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Cinnte: coinnigh gach duine ar an suíomh céanna'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Na déan: lig do dhaoine brabhsáil go saor'; + + @override + String get studyPinnedStudyComment => 'Trácht staidéir greamaithe'; + @override String get studyStart => 'Tosú'; + + @override + String get studySave => 'Sábháil'; + + @override + String get studyClearChat => 'Glan comhrá'; + + @override + String get studyDeleteTheStudyChatHistory => 'Scrios an stair comhrá staidéir? Níl aon dul ar ais!'; + + @override + String get studyDeleteStudy => 'Scrios an staidéar'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Scrios an staidéar iomlán? Níl aon dul ar ais! Clóscríobh ainm an staidéar le deimhniú: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Cá háit ar mhaith leat staidéar a dhéanamh air sin?'; + + @override + String get studyGoodMove => 'Beart maith'; + + @override + String get studyMistake => 'Botún'; + + @override + String get studyBrilliantMove => 'Beart iontach'; + + @override + String get studyBlunder => 'Botún'; + + @override + String get studyInterestingMove => 'Beart suimiúil'; + + @override + String get studyDubiousMove => 'Beart amhrasach'; + + @override + String get studyOnlyMove => 'Beart dleathach'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Suíomh cothrom'; + + @override + String get studyUnclearPosition => 'Suíomh doiléir'; + + @override + String get studyWhiteIsSlightlyBetter => 'Tá bán píosa beag níos fearr'; + + @override + String get studyBlackIsSlightlyBetter => 'Tá dubh píosa beag níos fearr'; + + @override + String get studyWhiteIsBetter => 'Tá bán níos fearr'; + + @override + String get studyBlackIsBetter => 'Tá dubh níos fearr'; + + @override + String get studyWhiteIsWinning => 'Bán ag bua'; + + @override + String get studyBlackIsWinning => 'Dubh ag bua'; + + @override + String get studyNovelty => 'Nuaga'; + + @override + String get studyDevelopment => 'Forbairt'; + + @override + String get studyInitiative => 'Tionscnamh'; + + @override + String get studyAttack => 'Ionsaí'; + + @override + String get studyCounterplay => 'Frithimirt'; + + @override + String get studyTimeTrouble => 'Trioblóid ama'; + + @override + String get studyWithCompensation => 'Le cúiteamh'; + + @override + String get studyWithTheIdea => 'Le smaoineamh'; + + @override + String get studyNextChapter => 'Céad chaibidil eile'; + + @override + String get studyPrevChapter => 'Caibidil roimhe seo'; + + @override + String get studyStudyActions => 'Déan staidéar ar ghníomhartha'; + + @override + String get studyTopics => 'Topaicí'; + + @override + String get studyMyTopics => 'Mo thopaicí'; + + @override + String get studyPopularTopics => 'Topaicí choitianta'; + + @override + String get studyManageTopics => 'Bainistigh topaicí'; + + @override + String get studyBack => 'Siar'; + + @override + String get studyPlayAgain => 'Imir arís'; + + @override + String get studyWhatWouldYouPlay => 'Cad a dhéanfá sa suíomh seo?'; + + @override + String get studyYouCompletedThisLesson => 'Comhghairdeas! Chríochnaigh tú an ceacht seo.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Caibidil', + many: '$count Caibidil', + few: '$count gCaibidil', + two: '$count Chaibidil', + one: '$count Caibidil', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Cluiche', + many: '$count Cluiche', + few: '$count gCluiche', + two: '$count Chluiche', + one: '$count Cluiche', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Comhalta', + many: '$count Comhalta', + few: '$count gComhalta', + two: '$count Chomhalta', + one: '$count Comhalta', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + many: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + few: 'Greamaigh do théacs PGN anseo, suas le $count gcluiche', + two: 'Greamaigh do théacs PGN anseo, suas le $count chluiche', + one: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'díreach anois'; + + @override + String get timeagoRightNow => 'anois'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count soicind', + many: 'i $count soicind', + few: 'i $count soicind', + two: 'i $count soicind', + one: 'i soicind amháín', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count nóiméad', + many: 'i $count nóiméad', + few: 'i $count nóiméad', + two: 'i $count nóiméad', + one: 'i nóiméad amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count uair', + many: 'i $count uair', + few: 'i $count uair', + two: 'i $count uair', + one: 'in uair amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count lá', + many: 'i $count lá', + few: 'i $count lá', + two: 'i $count lá', + one: 'i lá amháín', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count seachtain', + many: 'i $count seachtain', + few: 'i $count seachtain', + two: 'i $count sheachtain', + one: 'i seachtain amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count mí', + many: 'i $count mí', + few: 'i $count mhí', + two: 'i $count mhí', + one: 'i gceann míosa', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i gceann $count bliain', + many: 'i gceann $count mbliain', + few: 'i gceann $count bhliain', + two: 'i gceann $count bhliain', + one: 'i gceann bliana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nóiméad ó shin', + many: '$count nóiméad ó shin', + few: '$count nóiméad ó shin', + two: '$count nóiméad ó shin', + one: 'nóiméad amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uair ó shin', + many: '$count uair ó shin', + few: '$count uair ó shin', + two: '$count uair ó shin', + one: 'uair amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lá ó shin', + many: '$count lá ó shin', + few: '$count lá ó shin', + two: '$count lá ó shin', + one: 'lá amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seachtain ó shin', + many: '$count seachtain ó shin', + few: '$count sheachtain ó shin', + two: '$count sheachtain ó shin', + one: 'seachtain amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mí ó shin', + many: '$count mí ó shin', + few: '$count mí ó shin', + two: '$count mhí ó shin', + one: 'mí amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bliain ó shin', + many: '$count mbliain ó shin', + few: '$count mbliain ó shin', + two: '$count bhliain ó shin', + one: 'Bliain amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 9630689049..83efbaa00d 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsGl extends AppLocalizations { AppLocalizationsGl([String locale = 'gl']) : super(locale); @override - String get mobileHomeTab => 'Inicio'; + String get mobileAllGames => 'Todas as partidas'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get mobileAreYouSure => 'Estás seguro?'; @override - String get mobileToolsTab => 'Ferramentas'; + String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; @override - String get mobileWatchTab => 'Ver'; + String get mobileClearButton => 'Borrar'; @override - String get mobileSettingsTab => 'Axustes'; + String get mobileCorrespondenceClearSavedMove => 'Borrar a xogada gardada'; @override - String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta páxina.'; + String get mobileCustomGameJoinAGame => 'Unirse a unha partida'; @override - String get mobileSystemColors => 'Cores do sistema'; + String get mobileFeedbackButton => 'Comentarios'; @override - String get mobileFeedbackButton => 'Comentarios'; + String mobileGreeting(String param) { + return 'Ola, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Ola'; @override - String get mobileSettingsHapticFeedback => 'Vibración ó mover'; + String get mobileHideVariation => 'Ocultar variantes'; @override - String get mobileSettingsImmersiveMode => 'Pantalla completa'; + String get mobileHomeTab => 'Inicio'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.'; + String get mobileLiveStreamers => 'Presentadores en directo'; @override - String get mobileNotFollowingAnyUser => 'Non estás a seguir a ningún usuario.'; + String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta páxina.'; @override - String get mobileAllGames => 'Todas as partidas'; + String get mobileNoSearchResults => 'Sen resultados'; @override - String get mobileRecentSearches => 'Procuras recentes'; + String get mobileNotFollowingAnyUser => 'Non estás a seguir a ningún usuario.'; @override - String get mobileClearButton => 'Borrar'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Xogadores con \"$param\"'; + return 'O nome de usuario contén \"$param\"'; } @override - String get mobileNoSearchResults => 'Sen resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar a peza arrastrada'; @override - String get mobileAreYouSure => 'Estás seguro?'; + String get mobilePuzzleStormConfirmEndRun => 'Queres rematar esta quenda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderás a túa secuencia actual e o teu resultado gardarase.'; + String get mobilePuzzleStormFilterNothingToShow => 'Non aparece nada. Por favor, cambia os filtros'; @override String get mobilePuzzleStormNothingToShow => 'Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Compartir este crebacabezas'; + String get mobilePuzzleStormSubtitle => 'Resolve tantos crebacabezas como sexa posible en 3 minutos.'; @override - String get mobileShareGameURL => 'Compartir a URL da partida'; + String get mobilePuzzleStreakAbortWarning => 'Perderás a túa secuencia actual e o teu resultado gardarase.'; @override - String get mobileShareGamePGN => 'Compartir PGN'; + String get mobilePuzzleThemesSubtitle => 'Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.'; @override - String get mobileSharePositionAsFEN => 'Compartir a posición coma FEN'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get mobileShowVariations => 'Amosar variantes'; + String get mobileRecentSearches => 'Procuras recentes'; @override - String get mobileHideVariation => 'Ocultar variantes'; + String get mobileSettingsHapticFeedback => 'Vibración ó mover'; @override - String get mobileShowComments => 'Amosar comentarios'; + String get mobileSettingsImmersiveMode => 'Pantalla completa'; @override - String get mobilePuzzleStormConfirmEndRun => 'Queres rematar esta quenda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Non aparece nada. Por favor, cambia os filtros'; + String get mobileSettingsTab => 'Axustes'; @override - String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; + String get mobileShareGamePGN => 'Compartir PGN'; @override - String get mobileCancelDrawOffer => 'Cancelar a oferta de táboas'; + String get mobileShareGameURL => 'Compartir a URL da partida'; @override - String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; + String get mobileSharePositionAsFEN => 'Compartir a posición coma FEN'; @override - String get mobileBlindfoldMode => 'Á cega'; + String get mobileSharePuzzle => 'Compartir este crebacabezas'; @override - String get mobileLiveStreamers => 'Presentadores en directo'; + String get mobileShowComments => 'Amosar comentarios'; @override - String get mobileCustomGameJoinAGame => 'Unirse a unha partida'; + String get mobileShowResult => 'Amosar o resultado'; @override - String get mobileCorrespondenceClearSavedMove => 'Borrar a xogada gardada'; + String get mobileShowVariations => 'Amosar variantes'; @override String get mobileSomethingWentWrong => 'Algo foi mal.'; @override - String get mobileShowResult => 'Amosar o resultado'; - - @override - String get mobilePuzzleThemesSubtitle => 'Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resolve tantos crebacabezas como sexa posible en 3 minutos.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Ola, $param'; - } + String get mobileToolsTab => 'Ferrament.'; @override - String get mobileGreetingWithoutName => 'Ola'; + String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Ver'; @override String get activityActivity => 'Actividade'; @@ -246,6 +243,17 @@ class AppLocalizationsGl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Finalizou $count partidas $param2 por correspondencia', + one: 'Finalizou $count partida $param2 por correspondencia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsGl extends AppLocalizations { @override String get broadcastBroadcasts => 'Emisións en directo'; + @override + String get broadcastMyBroadcasts => 'As miñas emisións'; + @override String get broadcastLiveBroadcasts => 'Emisións de torneos en directo'; + @override + String get broadcastBroadcastCalendar => 'Calendario de emisións'; + + @override + String get broadcastNewBroadcast => 'Nova emisión en directo'; + + @override + String get broadcastSubscribedBroadcasts => 'Emisións subscritas'; + + @override + String get broadcastAboutBroadcasts => 'Sobre as retransmisións'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como usar as Retransmisións de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'A nova rolda terá os mesmos membros e colaboradores cá rolda anterior.'; + + @override + String get broadcastAddRound => 'Engadir unha rolda'; + + @override + String get broadcastOngoing => 'En curso'; + + @override + String get broadcastUpcoming => 'Proximamente'; + + @override + String get broadcastCompleted => 'Completadas'; + + @override + String get broadcastCompletedHelp => 'Malia que Lichess detecta o final das roldas, pódese equivocar. Usa esta opción para facelo manualmente.'; + + @override + String get broadcastRoundName => 'Nome da rolda'; + + @override + String get broadcastRoundNumber => 'Número de rolda'; + + @override + String get broadcastTournamentName => 'Nome do torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descrición do torneo'; + + @override + String get broadcastFullDescription => 'Descrición completa do torneo'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrición longa opcional do torneo. $param1 está dispoñíbel. A lonxitude debe ser menor de $param2 caracteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL de orixe do arquivo PGN'; + + @override + String get broadcastSourceUrlHelp => 'Ligazón que Lichess comprobará para obter actualizacións dos PGN. Debe ser publicamente accesíbel desde a Internet.'; + + @override + String get broadcastSourceGameIds => 'Até 64 identificadores de partidas de Lichess, separados por espazos.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data de inicio do torneo (na zona horaria local): $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, se sabes cando comeza o evento'; + + @override + String get broadcastCurrentGameUrl => 'Ligazón da partida actual'; + + @override + String get broadcastDownloadAllRounds => 'Descargar todas as roldas'; + + @override + String get broadcastResetRound => 'Restablecer esta rolda'; + + @override + String get broadcastDeleteRound => 'Borrar esta rolda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente a rolda e todas as súas partidas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todas as partidas desta rolda. A transmisión en orixe terá que estar activa para volver crealas.'; + + @override + String get broadcastEditRoundStudy => 'Editar o estudo da rolda'; + + @override + String get broadcastDeleteTournament => 'Eliminar este torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Eliminar o torneo de forma definitiva, con todas as súas roldas e partidas.'; + + @override + String get broadcastShowScores => 'Amosar os puntos dos xogadores en función dos resultados das partidas'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: substituír os nomes dos xogadores, as puntuacións e os títulos'; + + @override + String get broadcastFideFederations => 'Federacións FIDE'; + + @override + String get broadcastTop10Rating => 'Media do top 10'; + + @override + String get broadcastFidePlayers => 'Xogadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Xogador FIDE non atopado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federación'; + + @override + String get broadcastAgeThisYear => 'Idade actual'; + + @override + String get broadcastUnrated => 'Sen puntuar'; + + @override + String get broadcastRecentTournaments => 'Torneos recentes'; + + @override + String get broadcastOpenLichess => 'Abrir en Lichess'; + + @override + String get broadcastTeams => 'Equipos'; + + @override + String get broadcastBoards => 'Taboleiros'; + + @override + String get broadcastOverview => 'Visión de conxunto'; + + @override + String get broadcastSubscribeTitle => 'Subscríbete para ser notificado ó comezo de cada rolda. Podes activar/desactivar o son das notificacións ou as notificacións emerxentes para as emisións en directo nas preferencias da túa conta.'; + + @override + String get broadcastUploadImage => 'Subir a imaxe do torneo'; + + @override + String get broadcastNoBoardsYet => 'Aínda non hai taboleiros. Aparecerán cando se suban as partidas.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Os taboleiros pódense cargar dende a fonte ou a través da $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Comeza tras a $param'; + } + + @override + String get broadcastStartVerySoon => 'A emisión comeza decontado.'; + + @override + String get broadcastNotYetStarted => 'A emisión aínda non comezou.'; + + @override + String get broadcastOfficialWebsite => 'Páxina web oficial'; + + @override + String get broadcastStandings => 'Clasificación'; + + @override + String get broadcastOfficialStandings => 'Clasificación oficial'; + + @override + String broadcastIframeHelp(String param) { + return 'Máis opcións na $param'; + } + + @override + String get broadcastWebmastersPage => 'páxina do administrador web'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Unha fonte dos PGN pública e en tempo real para esta rolda. Tamén ofrecemos unha $param para unha sincronización máis rápida e eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incrustar esta emisión na túa páxina web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incrustar a $param na túa páxina web'; + } + + @override + String get broadcastRatingDiff => 'Diferenza de puntuación'; + + @override + String get broadcastGamesThisTournament => 'Partidas neste torneo'; + + @override + String get broadcastScore => 'Resultado'; + + @override + String get broadcastAllTeams => 'Todos os equipos'; + + @override + String get broadcastTournamentFormat => 'Formato do torneo'; + + @override + String get broadcastTournamentLocation => 'Lugar do torneo'; + + @override + String get broadcastTopPlayers => 'Mellores xogadores'; + + @override + String get broadcastTimezone => 'Zona horaria'; + + @override + String get broadcastFideRatingCategory => 'Categoría de puntuación FIDE'; + + @override + String get broadcastOptionalDetails => 'Detalles opcionais'; + + @override + String get broadcastPastBroadcasts => 'Emisións finalizadas'; + + @override + String get broadcastAllBroadcastsByMonth => 'Ver todas as emisións por mes'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count emisións', + one: '$count emisión', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafíos: $param1'; @@ -598,7 +853,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesShowFlairs => 'Amosar as habelencias dos xogadores'; @override - String get preferencesExplainShowPlayerRatings => 'Isto permite ocultar todas as puntuacións do sitio, para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.'; + String get preferencesExplainShowPlayerRatings => 'Oculta todas as puntuacións de Lichess para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.'; @override String get preferencesDisplayBoardResizeHandle => 'Mostrar o control de redimensionamento do taboleiro'; @@ -609,6 +864,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get preferencesInGameOnly => 'Só durante a partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Reloxo de xadrez'; @@ -628,7 +886,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesGiveMoreTime => 'Dar máis tempo'; @override - String get preferencesGameBehavior => 'Comportamento do xogo'; + String get preferencesGameBehavior => 'Comportamento durante a partida'; @override String get preferencesHowDoYouMovePieces => 'Como moves as pezas?'; @@ -688,7 +946,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesCastleByMovingTwoSquares => 'Movendo o rei dúas casas'; @override - String get preferencesCastleByMovingOntoTheRook => 'Movendo rei ata a torre'; + String get preferencesCastleByMovingOntoTheRook => 'Movendo o rei cara a torre'; @override String get preferencesInputMovesWithTheKeyboard => 'Introdución de movementos co teclado'; @@ -750,6 +1008,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Son da notificación'; + @override + String get preferencesBlindfold => 'Ás cegas'; + @override String get puzzlePuzzles => 'Crebacabezas'; @@ -919,7 +1180,7 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleSolved => 'resoltos'; @override - String get puzzleFailed => 'fallado'; + String get puzzleFailed => 'incorrecto'; @override String get puzzleStreakDescription => 'Soluciona exercicios cada vez máis difíciles e consigue unha secuencia de vitorias. Non hai conta atrás, así que podes ir amodo. Se te equivocas nun só movemento, acabouse! Pero lembra que podes omitir unha xogada por sesión.'; @@ -1150,7 +1411,7 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleThemeDiscoveredAttack => 'Ataque descuberto'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Apartar unha peza que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo un cabalo fóra do camiño dunha torre).'; + String get puzzleThemeDiscoveredAttackDescription => 'Apartar unha peza (por exemplo un cabalo) que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo unha torre).'; @override String get puzzleThemeDoubleCheck => 'Xaque dobre'; @@ -1390,10 +1651,10 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'O rival ten os movementos limitados e calquera xogada que faga empeora a súa posición.'; @override - String get puzzleThemeHealthyMix => 'Mestura equilibrada'; + String get puzzleThemeMix => 'Mestura equilibrada'; @override - String get puzzleThemeHealthyMixDescription => 'Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.'; + String get puzzleThemeMixDescription => 'Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.'; @override String get puzzleThemePlayerGames => 'Partidas de xogadores'; @@ -1767,9 +2028,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get byCPL => 'Por PCP'; - @override - String get openStudy => 'Abrir estudo'; - @override String get enable => 'Activar'; @@ -1797,9 +2055,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimina o límite de profundidade, e mantén o teu ordenador quente'; - @override - String get engineManager => 'Administrador do motor de análise'; - @override String get blunder => 'Metida de zoca'; @@ -1956,7 +2211,7 @@ class AppLocalizationsGl extends AppLocalizations { String get email => 'Correo electrónico'; @override - String get passwordReset => 'Cambiar contrasinal'; + String get passwordReset => 'restablecer o contrasinal'; @override String get forgotPassword => 'Esqueciches o teu contrasinal?'; @@ -2063,6 +2318,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get gamesPlayed => 'Partidas xogadas'; + @override + String get ok => 'Ok'; + @override String get cancel => 'Cancelar'; @@ -2326,7 +2584,7 @@ class AppLocalizationsGl extends AppLocalizations { String get averageElo => 'Puntuación media'; @override - String get location => 'Ubicación'; + String get location => 'Lugar'; @override String get filterGames => 'Filtrar partidas'; @@ -2437,9 +2695,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Séguete'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 comezou a seguir a $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsGl extends AppLocalizations { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Pega a ligazón á(s) partida(s) e explica o que é incorrecto no comportamento deste usuario. Non digas só \"fai trampas\", cóntanos como chegaches a esa conclusión. A túa denuncia será procesada máis rapidamente se está escrita en inglés.'; + String get reportCheatBoostHelp => 'Pega a ligazón á(s) partida(s) e explica que ten de malo o comportamento deste usuario. Non digas soamente \"fai trampas\", mais cóntanos como chegaches a esta conclusión.'; + + @override + String get reportUsernameHelp => 'Explica que ten de ofensivo este nome de usuario. Non digas soamente \"é ofensivo/inadecuado\". Cóntanos como chegaches a esta conclusión, especialmente se o insulto está camuflado, non está en inglés, é xerga ou é unha referencia histórica ou cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'A túa denuncia será procesada máis rápido se está escrita en Inglés.'; @override String get error_provideOneCheatedGameLink => 'Por favor, incorpora cando menos unha ligazón a unha partida na que se fixeron trampas.'; @@ -2985,7 +3246,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String error_maxLength(String param) { - return 'A lonxitude máxima é de $param caracteres'; + return 'Debe conter polo menos $param caracteres'; } @override @@ -3034,7 +3295,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String tpTimeSpentOnTV(String param) { - return 'Tempo en TV: $param'; + return 'Tempo saíndo en TV: $param'; } @override @@ -3119,7 +3380,7 @@ class AppLocalizationsGl extends AppLocalizations { String get aboutSimulRealLife => 'O concepto tómase de eventos reais. Nestes, o anfitrión das simultáneas móvese de mesa en mesa e fai unha xogada de cada vez.'; @override - String get aboutSimulRules => 'Cando as simultáneas comezan, cada xogador inicia unha partida contra o anfitrión. As simultáneas finalizan cando todas as partidas rematan.'; + String get aboutSimulRules => 'Cando comezan as simultáneas, cada xogador inicia unha partida co anfitrión. As simultáneas finalizan cando se completan todas as partidas.'; @override String get aboutSimulSettings => 'As simultáneas son sempre amigables. As opcións de desquite, de desfacer a xogada e de engadir tempo non están activadas.'; @@ -3140,7 +3401,7 @@ class AppLocalizationsGl extends AppLocalizations { String get simulAddExtraTime => 'Podes engadir tempo extra ó teu reloxo para axudarte coas simultáneas.'; @override - String get simulHostExtraTime => 'Tempo extra para o anfitrión'; + String get simulHostExtraTime => 'Tempo inicial extra para o anfitrión'; @override String get simulAddExtraTimePerPlayer => 'Engadir tempo inicial ao teu reloxo por cada xogador que se una ás simultáneas.'; @@ -3212,7 +3473,7 @@ class AppLocalizationsGl extends AppLocalizations { String get toggleGlyphAnnotations => 'Activar/desactivar as anotacións con símbolos'; @override - String get togglePositionAnnotations => 'Alternar anotaciones de posición'; + String get togglePositionAnnotations => 'Activar/desactivar as anotacións'; @override String get variationArrowsInfo => 'As frechas das variantes permítenche navegar sen usar a lista de movementos.'; @@ -3323,7 +3584,7 @@ class AppLocalizationsGl extends AppLocalizations { String get crosstable => 'Táboa de cruces'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Tamén podes usar a roda do rato sobre o taboleiro para moverte pola partida.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Usa a roda do rato sobre o taboleiro para moverte pola partida.'; @override String get scrollOverComputerVariationsToPreviewThem => 'Pasa o punteiro sobre as variantes da computadora para visualizalas.'; @@ -3915,7 +4176,7 @@ class AppLocalizationsGl extends AppLocalizations { String get tournDescriptionHelp => 'Queres dicirlles algo en especial ós participantes? Tenta ser breve. Hai dispoñibles ligazóns de Markdown: [name](https://url)'; @override - String get ratedFormHelp => 'As partidas son puntuadas\ne afectan ás puntuacións dos xogadores'; + String get ratedFormHelp => 'As partidas son puntuadas e afectan ás puntuacións dos xogadores'; @override String get onlyMembersOfTeam => 'Só membros do equipo'; @@ -4054,7 +4315,7 @@ class AppLocalizationsGl extends AppLocalizations { String get until => 'Ata'; @override - String get lichessDbExplanation => 'Partidas puntuadas de todos os xogadores de Lichess'; + String get lichessDbExplanation => 'Partidas puntuadas xogadas en Lichess'; @override String get switchSides => 'Cambiar de cor'; @@ -4077,6 +4338,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get nothingToSeeHere => 'Nada que ver aquí polo de agora.'; + @override + String get stats => 'Estatísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsGl extends AppLocalizations { @override String get streamerLichessStreamers => 'Presentadores de Lichess'; + @override + String get studyPrivate => 'Privado'; + + @override + String get studyMyStudies => 'Os meus estudos'; + + @override + String get studyStudiesIContributeTo => 'Estudos nos que contribúo'; + + @override + String get studyMyPublicStudies => 'Os meus estudos públicos'; + + @override + String get studyMyPrivateStudies => 'Os meus estudos privados'; + + @override + String get studyMyFavoriteStudies => 'Os meus estudos favoritos'; + + @override + String get studyWhatAreStudies => 'Que son os estudos?'; + + @override + String get studyAllStudies => 'Todos os estudos'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudos creados por $param'; + } + + @override + String get studyNoneYet => 'Aínda non hai.'; + + @override + String get studyHot => 'Candentes'; + + @override + String get studyDateAddedNewest => 'Data engadida (máis novos)'; + + @override + String get studyDateAddedOldest => 'Data engadida (máis antigos)'; + + @override + String get studyRecentlyUpdated => 'Actualizados recentemente'; + + @override + String get studyMostPopular => 'Máis populares'; + + @override + String get studyAlphabetical => 'Alfabeticamente'; + + @override + String get studyAddNewChapter => 'Engadir un novo capítulo'; + + @override + String get studyAddMembers => 'Engadir membros'; + + @override + String get studyInviteToTheStudy => 'Invitar ao estudo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convida só a persoas que te coñezan e que desexen activamente unirse a este estudo.'; + + @override + String get studySearchByUsername => 'Buscar por nome de usuario'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Colaborador'; + + @override + String get studyKick => 'Expulsar'; + + @override + String get studyLeaveTheStudy => 'Deixar o estudo'; + + @override + String get studyYouAreNowAContributor => 'Agora es un colaborador'; + + @override + String get studyYouAreNowASpectator => 'Agora es un espectador'; + + @override + String get studyPgnTags => 'Etiquetas PGN'; + + @override + String get studyLike => 'Gústame'; + + @override + String get studyUnlike => 'Xa non me gusta'; + + @override + String get studyNewTag => 'Nova etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar nesta posición'; + + @override + String get studyCommentThisMove => 'Comentar este movemento'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar con símbolos'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é demasiado curto para analizalo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Só os colaboradores do estudo poden solicitar unha análise por ordenador.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtén unha análise completa da liña principal dende o servidor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asegúrate de que o capítulo está completo. Só podes solicitar a análise unha vez.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros sincronizados permanecen na mesma posición'; + + @override + String get studyShareChanges => 'Comparte os cambios cos espectadores e gárdaos no servidor'; + + @override + String get studyPlaying => 'En xogo'; + + @override + String get studyShowEvalBar => 'Indicadores de avaliación'; + + @override + String get studyFirst => 'Primeiro'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Seguinte'; + + @override + String get studyLast => 'Último'; + @override String get studyShareAndExport => 'Compartir e exportar'; + @override + String get studyCloneStudy => 'Clonar'; + + @override + String get studyStudyPgn => 'PGN do estudo'; + + @override + String get studyDownloadAllGames => 'Descargar todas as partidas'; + + @override + String get studyChapterPgn => 'PGN do capítulo'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descargar partida'; + + @override + String get studyStudyUrl => 'URL do estudo'; + + @override + String get studyCurrentChapterUrl => 'Ligazón do capítulo actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Podes pegar esta URL no foro ou no teu blog de Lichess para incrustala'; + + @override + String get studyStartAtInitialPosition => 'Comezar desde a posición inicial do estudo'; + + @override + String studyStartAtX(String param) { + return 'Comezar en $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Incrustar na túa páxina web ou blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Ler máis sobre como inserir contido'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Só se poden inserir estudos públicos!'; + + @override + String get studyOpen => 'Abrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 traído para ti por $param2'; + } + + @override + String get studyStudyNotFound => 'Estudo non atopado'; + + @override + String get studyEditChapter => 'Editar capítulo'; + + @override + String get studyNewChapter => 'Novo capítulo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientación'; + + @override + String get studyAnalysisMode => 'Modo de análise'; + + @override + String get studyPinnedChapterComment => 'Comentario do capítulo fixado'; + + @override + String get studySaveChapter => 'Gardar capítulo'; + + @override + String get studyClearAnnotations => 'Borrar anotacións'; + + @override + String get studyClearVariations => 'Borrar variantes'; + + @override + String get studyDeleteChapter => 'Borrar capítulo'; + + @override + String get studyDeleteThisChapter => 'Realmente queres borrar o capítulo? Non hai volta atrás!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Borrar todos os comentarios, símbolos e marcas do capítulo'; + + @override + String get studyRightUnderTheBoard => 'Xusto debaixo do taboleiro'; + + @override + String get studyNoPinnedComment => 'Ningún'; + + @override + String get studyNormalAnalysis => 'Análise normal'; + + @override + String get studyHideNextMoves => 'Ocultar os seguintes movementos'; + + @override + String get studyInteractiveLesson => 'Lección interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítulo $param'; + } + + @override + String get studyEmpty => 'Baleiro'; + + @override + String get studyStartFromInitialPosition => 'Comezar desde a posición inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Comezar dende unha posición personalizada'; + + @override + String get studyLoadAGameByUrl => 'Cargar as partidas dende unha URL'; + + @override + String get studyLoadAPositionFromFen => 'Cargar unha posición dende o FEN'; + + @override + String get studyLoadAGameFromPgn => 'Cargar as partidas dende o PGN'; + + @override + String get studyAutomatic => 'Automática'; + + @override + String get studyUrlOfTheGame => 'Ligazóns das partidas, unha por liña'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Cargar partidas dende $param1 ou $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítulo'; + + @override + String get studyCreateStudy => 'Crear estudo'; + + @override + String get studyEditStudy => 'Editar estudo'; + + @override + String get studyVisibility => 'Visibilidade'; + + @override + String get studyPublic => 'Público'; + + @override + String get studyUnlisted => 'Sen listar'; + + @override + String get studyInviteOnly => 'Acceso só mediante invitación'; + + @override + String get studyAllowCloning => 'Permitir clonado'; + + @override + String get studyNobody => 'Ninguén'; + + @override + String get studyOnlyMe => 'Só eu'; + + @override + String get studyContributors => 'Colaboradores'; + + @override + String get studyMembers => 'Membros'; + + @override + String get studyEveryone => 'Todo o mundo'; + + @override + String get studyEnableSync => 'Activar sincronización'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Si: todos verán a mesma posición'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Non: permitir que a xente navegue libremente'; + + @override + String get studyPinnedStudyComment => 'Comentario fixado do estudo'; + @override String get studyStart => 'Comezar'; + + @override + String get studySave => 'Gardar'; + + @override + String get studyClearChat => 'Borrar a sala de conversa'; + + @override + String get studyDeleteTheStudyChatHistory => 'Borrar o historial da sala de conversa? Esta acción non se pode desfacer!'; + + @override + String get studyDeleteStudy => 'Borrar estudo'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Borrar todo o estudo? Non se poderá recuperar! Teclea o nome do estudo para confirmar: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Onde queres estudar isto?'; + + @override + String get studyGoodMove => 'Bo movemento'; + + @override + String get studyMistake => 'Erro'; + + @override + String get studyBrilliantMove => 'Movemento brillante'; + + @override + String get studyBlunder => 'Metida de zoca'; + + @override + String get studyInterestingMove => 'Movemento interesante'; + + @override + String get studyDubiousMove => 'Movemento dubidoso'; + + @override + String get studyOnlyMove => 'Movemento único'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posición igualada'; + + @override + String get studyUnclearPosition => 'Posición pouco clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'As brancas están lixeiramente mellor'; + + @override + String get studyBlackIsSlightlyBetter => 'As negras están lixeiramente mellor'; + + @override + String get studyWhiteIsBetter => 'As brancas están mellor'; + + @override + String get studyBlackIsBetter => 'As negras están mellor'; + + @override + String get studyWhiteIsWinning => 'As brancas están gañando'; + + @override + String get studyBlackIsWinning => 'As negras están gañando'; + + @override + String get studyNovelty => 'Novidade'; + + @override + String get studyDevelopment => 'Desenvolvemento'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Ataque'; + + @override + String get studyCounterplay => 'Contraataque'; + + @override + String get studyTimeTrouble => 'Apuros de tempo'; + + @override + String get studyWithCompensation => 'Con compensación'; + + @override + String get studyWithTheIdea => 'Coa idea'; + + @override + String get studyNextChapter => 'Capítulo seguinte'; + + @override + String get studyPrevChapter => 'Capítulo anterior'; + + @override + String get studyStudyActions => 'Accións de estudo'; + + @override + String get studyTopics => 'Temas'; + + @override + String get studyMyTopics => 'Os meus temas'; + + @override + String get studyPopularTopics => 'Temas populares'; + + @override + String get studyManageTopics => 'Administrar temas'; + + @override + String get studyBack => 'Voltar'; + + @override + String get studyPlayAgain => 'Xogar de novo'; + + @override + String get studyWhatWouldYouPlay => 'Que xogarías nesta posición?'; + + @override + String get studyYouCompletedThisLesson => 'Parabéns! Completaches esta lección.'; + + @override + String studyPerPage(String param) { + return '$param por páxina'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítulos', + one: '$count Capítulo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partidas', + one: '$count Partida', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membros', + one: '$count Membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pega o teu texto PGN aquí, ata $count partidas', + one: 'Pega o teu texto PGN aquí, ata $count partida', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'agora mesmo'; + + @override + String get timeagoRightNow => 'agora'; + + @override + String get timeagoCompleted => 'completado'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segundos', + one: 'en $count segundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutos', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horas', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count días', + one: 'en $count día', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semanas', + one: 'en $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count meses', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count anos', + one: 'en $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count minutos', + one: 'Hai $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count horas', + one: 'Hai $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count días', + one: 'Hai $count día', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count semanas', + one: 'Hai $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count meses', + one: 'Hai $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count anos', + one: 'Hai $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minuto restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count hora restante', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index 80685dbfce..8694267e0c 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsGsw extends AppLocalizations { AppLocalizationsGsw([String locale = 'gsw']) : super(locale); @override - String get mobileHomeTab => 'Afangssite'; + String get mobileAllGames => 'All Schpiel'; @override - String get mobilePuzzlesTab => 'Ufgabe'; + String get mobileAreYouSure => 'Bisch sicher?'; @override - String get mobileToolsTab => 'Werchzüg'; + String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; @override - String get mobileWatchTab => 'Luege'; + String get mobileClearButton => 'Leere'; @override - String get mobileSettingsTab => 'Ischtelle'; + String get mobileCorrespondenceClearSavedMove => 'Lösch die gschpeicherete Züg'; @override - String get mobileMustBeLoggedIn => 'Muesch iglogt si, zum die Site z\'gseh.'; + String get mobileCustomGameJoinAGame => 'Es Schpiel mitschpille'; @override - String get mobileSystemColors => 'Syschtem-Farbe'; + String get mobileFeedbackButton => 'Rückmäldig'; @override - String get mobileFeedbackButton => 'Rückmäldig'; + String mobileGreeting(String param) { + return 'Hoi, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hoi'; @override - String get mobileSettingsHapticFeedback => 'Rückmäldig mit Vibration'; + String get mobileHideVariation => 'Variante verberge'; @override - String get mobileSettingsImmersiveMode => 'Ibettete Modus'; + String get mobileHomeTab => 'Afangssite'; @override - String get mobileSettingsImmersiveModeSubtitle => 'UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d\'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.'; + String get mobileLiveStreamers => 'Live Streamer'; @override - String get mobileNotFollowingAnyUser => 'Du folgsch keim Schpiller.'; + String get mobileMustBeLoggedIn => 'Muesch iglogt si, zum die Site z\'gseh.'; @override - String get mobileAllGames => 'Alli Partie'; + String get mobileNoSearchResults => 'Nüt g\'funde'; @override - String get mobileRecentSearches => 'Kürzlich Gsuechts'; + String get mobileNotFollowingAnyUser => 'Du folgsch keim Schpiller.'; @override - String get mobileClearButton => 'Leere'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nüt g\'funde'; + String get mobilePrefMagnifyDraggedPiece => 'Vegrösserig vu de zogene Figur'; @override - String get mobileAreYouSure => 'Bisch sicher?'; + String get mobilePuzzleStormConfirmEndRun => 'Wottsch de Lauf beände?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du verlürsch din aktuelle Lauf und din Rekord wird g\'schpeicheret.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nüt zum Zeige, bitte d\'Filter ändere'; @override String get mobilePuzzleStormNothingToShow => 'Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Teil die Ufgab'; + String get mobilePuzzleStormSubtitle => 'Lös i 3 Minute so vill Ufgabe wie möglich.'; @override - String get mobileShareGameURL => 'Teil d\'Schpiel-URL'; + String get mobilePuzzleStreakAbortWarning => 'Du verlürsch din aktuelle Lauf und din Rekord wird g\'schpeicheret.'; @override - String get mobileShareGamePGN => 'Teil s\'PGN'; + String get mobilePuzzleThemesSubtitle => 'Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.'; @override - String get mobileSharePositionAsFEN => 'Teil d\'Position als FEN'; + String get mobilePuzzlesTab => 'Ufgabe'; @override - String get mobileShowVariations => 'Zeig Variante'; + String get mobileRecentSearches => 'Kürzlich Gsuechts'; @override - String get mobileHideVariation => 'Variante verberge'; + String get mobileSettingsHapticFeedback => 'Rückmäldig mit Vibration'; @override - String get mobileShowComments => 'Zeig Kommentär'; + String get mobileSettingsImmersiveMode => 'Ibettete Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wottsch de Lauf beände?'; + String get mobileSettingsImmersiveModeSubtitle => 'UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d\'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nüt zum Zeige, bitte d\'Filter ändere'; + String get mobileSettingsTab => 'Ischtelle'; @override - String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; + String get mobileShareGamePGN => 'Teil s\'PGN'; @override - String get mobileCancelDrawOffer => 'Remis-Agebot zruggzieh'; + String get mobileShareGameURL => 'Teil d\'Schpiel-URL'; @override - String get mobileWaitingForOpponentToJoin => 'Warte bis en Gegner erschint...'; + String get mobileSharePositionAsFEN => 'Teil d\'Position als FEN'; @override - String get mobileBlindfoldMode => 'Blind schpille'; + String get mobileSharePuzzle => 'Teil die Ufgab'; @override - String get mobileLiveStreamers => 'Live Streamer'; + String get mobileShowComments => 'Zeig Kommentär'; @override - String get mobileCustomGameJoinAGame => 'Bi ere Partie mitschpille'; + String get mobileShowResult => 'Resultat zeige'; @override - String get mobileCorrespondenceClearSavedMove => 'Lösch die gschpeicherete Züg'; + String get mobileShowVariations => 'Zeig Variante'; @override String get mobileSomethingWentWrong => 'Es isch öppis schief gange.'; @override - String get mobileShowResult => 'Resultat zeige'; - - @override - String get mobilePuzzleThemesSubtitle => 'Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.'; + String get mobileSystemColors => 'Syschtem-Farbe'; @override - String get mobilePuzzleStormSubtitle => 'Lös i 3 Minute so vill Ufgabe wie möglich.'; + String get mobileTheme => 'Farbschema'; @override - String mobileGreeting(String param) { - return 'Hoi, $param'; - } + String get mobileToolsTab => 'Werchzüg'; @override - String get mobileGreetingWithoutName => 'Hoi'; + String get mobileWaitingForOpponentToJoin => 'Warte bis en Gegner erschint...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Luege'; @override String get activityActivity => 'Aktivitäte'; @@ -196,8 +193,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Hät $count Partie $param2 gschpillt', - one: 'Hät $count Partie $param2 gschpillt', + other: 'Hät $count Schpiel $param2 gschpillt', + one: 'Hät $count Schpiel $param2 gschpillt', ); return '$_temp0'; } @@ -229,8 +226,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'i $count Fernschachpartie', - one: 'i $count Fernschachpartie', + other: 'in $count Korrespondänz-Schpiel', + one: 'in $count Korrespondänz-Schpiel', ); return '$_temp0'; } @@ -240,8 +237,19 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Hät $count Fernschachpartie gschpillt', - one: 'Hät $count Fernschachpartie gschpillt', + other: 'Hät $count Korrespondänz-Schpiel gschpillt', + one: 'Hät $count Korrespondänz-Schpiel gschpillt', + ); + return '$_temp0'; + } + + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 Korrespondänz-Schpiel gschpillt', + one: '$count $param2 Korrespondänz-Schpiel gschpillt', ); return '$_temp0'; } @@ -348,16 +356,263 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get broadcastBroadcasts => 'Überträgige'; + @override + String get broadcastMyBroadcasts => 'Eigeni Überträgige'; + @override String get broadcastLiveBroadcasts => 'Live Turnier-Überträgige'; + @override + String get broadcastBroadcastCalendar => 'Überträgigs-Kaländer'; + + @override + String get broadcastNewBroadcast => 'Neui Live-Überträgige'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonnierti Überträgige'; + + @override + String get broadcastAboutBroadcasts => 'Über Überträgige'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Wie mer Lichess-Überträgige benutzt.'; + + @override + String get broadcastTheNewRoundHelp => 'Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig.'; + + @override + String get broadcastAddRound => 'E Rundi zuefüege'; + + @override + String get broadcastOngoing => 'Laufend'; + + @override + String get broadcastUpcoming => 'Demnächscht'; + + @override + String get broadcastCompleted => 'Beändet'; + + @override + String get broadcastCompletedHelp => 'Lichess erkännt de Rundeschluss oder au nöd! De Schalter setz das uf \"manuell\".'; + + @override + String get broadcastRoundName => 'Runde Name'; + + @override + String get broadcastRoundNumber => 'Runde Nummere'; + + @override + String get broadcastTournamentName => 'Turnier Name'; + + @override + String get broadcastTournamentDescription => 'Churzi Turnier Beschribig'; + + @override + String get broadcastFullDescription => 'Vollschtändigi Ereignisbeschribig'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionali, usfüehrlichi Beschribig vu de Überträgig. $param1 isch verfügbar. Die Beschribig muess chürzer als $param2 Zeiche si.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Quälle URL'; + + @override + String get broadcastSourceUrlHelp => 'URL wo Lichess abfrögt, für PGN Aktualisierige z\'erhalte. Sie muess öffentlich im Internet zuegänglich si.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess Schpiel-IDs, trännt dur en Leerschlag.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum i de lokale Zitzone vum Turnier: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, falls du weisch, wänn das Ereignis afangt'; + + @override + String get broadcastCurrentGameUrl => 'URL vom laufende Schpiel'; + + @override + String get broadcastDownloadAllRounds => 'Alli Runde abelade'; + + @override + String get broadcastResetRound => 'Die Rundi zruggsetze'; + + @override + String get broadcastDeleteRound => 'Die Rundi lösche'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Die Rundi, mit allne Schpiel, definitiv lösche.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Lösch alli Schpiel vu dere Rundi. D\'Quälle muess aktiv si, dass sie neu erschtellt werde chönd.'; + + @override + String get broadcastEditRoundStudy => 'Runde-Schtudie bearbeite'; + + @override + String get broadcastDeleteTournament => 'Lösch das Turnier'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Das ganze Turnier, alli Runde und alli Schpiel definitiv lösche.'; + + @override + String get broadcastShowScores => 'Zeigt d\'Erfolg vu de Schpiller, anhand vu Schpiel-Ergäbnis'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Schpillernäme, Wertige und Titel weg lah'; + + @override + String get broadcastFideFederations => 'FIDE Wältschachverband'; + + @override + String get broadcastTop10Rating => 'Top 10 Ratings'; + + @override + String get broadcastFidePlayers => 'FIDE Schpiller'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE Schpiller nöd g\'funde'; + + @override + String get broadcastFideProfile => 'FIDE Profil'; + + @override + String get broadcastFederation => 'Verband'; + + @override + String get broadcastAgeThisYear => 'Alter i dem Jahr'; + + @override + String get broadcastUnrated => 'Ungwertet'; + + @override + String get broadcastRecentTournaments => 'Aktuellschti Turnier'; + + @override + String get broadcastOpenLichess => 'In Lichess öffne'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Brätter'; + + @override + String get broadcastOverview => 'Überblick'; + + @override + String get broadcastSubscribeTitle => 'Mäld dich a, zum über jede Rundeschtart informiert z\'werde. Du chasch de Alarm- oder d\'Push-Benachrichtigung, für Überträgige, i dine Kontoischtellige umschalte.'; + + @override + String get broadcastUploadImage => 'Turnier-Bild ufelade'; + + @override + String get broadcastNoBoardsYet => 'No kei Brätter. Die erschined, sobald Schpiel ufeglade sind.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brätter chönd mit ere Quälle oder via $param ufeglade werde'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Schtartet nach $param'; + } + + @override + String get broadcastStartVerySoon => 'Die Überträgig schtartet sehr bald.'; + + @override + String get broadcastNotYetStarted => 'Die Überträgig hät nonig agfange.'; + + @override + String get broadcastOfficialWebsite => 'Offizielli Website'; + + @override + String get broadcastStandings => 'Tabälle'; + + @override + String get broadcastOfficialStandings => 'Offizielli Ranglischte'; + + @override + String broadcastIframeHelp(String param) { + return 'Meh Optionen uf $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster Site'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Öffentlichi, real-time PGN Quälle, für die Rundi. Mir offeriered au $param für e schnälleri und effiziänteri Synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Nimm die Überträgig uf dini Website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Nimm $param uf dini Website'; + } + + @override + String get broadcastRatingDiff => 'Wertigs Differänz'; + + @override + String get broadcastGamesThisTournament => 'Schpiel i dem Turnier'; + + @override + String get broadcastScore => 'Resultat'; + + @override + String get broadcastAllTeams => 'Alli Teams'; + + @override + String get broadcastTournamentFormat => 'Turnier-Format'; + + @override + String get broadcastTournamentLocation => 'Turnier-Lokal'; + + @override + String get broadcastTopPlayers => 'Top-Schpiller'; + + @override + String get broadcastTimezone => 'Zitzone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-Wertigskategorie'; + + @override + String get broadcastOptionalDetails => 'Optionali Details'; + + @override + String get broadcastPastBroadcasts => 'G\'machti Überträgige'; + + @override + String get broadcastAllBroadcastsByMonth => 'Zeig alli Überträgige im Monet'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Überträgige', + one: '$count Überträgige', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Useforderige: $param1'; } @override - String get challengeChallengeToPlay => 'Zunere Partie usefordere'; + String get challengeChallengeToPlay => 'Zum Schpiel fordere'; @override String get challengeChallengeDeclined => 'Useforderig abglehnt'; @@ -456,7 +711,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatProvisional => 'provisorisch'; @override - String get perfStatNotEnoughRatedGames => 'Nöd gnueg gwerteti Schpil, für e verlässlichi Wertig z\'errächne.'; + String get perfStatNotEnoughRatedGames => 'Nöd gnueg g\'werteti Schpil, zum e verlässlichi Wertig z\'errächne.'; @override String perfStatProgressOverLastXGames(String param) { @@ -477,7 +732,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatTotalGames => 'Alli Schpil'; @override - String get perfStatRatedGames => 'Gwerteti Schpil'; + String get perfStatRatedGames => 'G\'werteti Schpiel'; @override String get perfStatTournamentGames => 'Turnier Schpil'; @@ -538,7 +793,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatBestRated => 'die beschte Sieg'; @override - String get perfStatGamesInARow => 'In Serie gschpillti Partie'; + String get perfStatGamesInARow => 'Nachenand g\'schpillti Schpiel'; @override String get perfStatLessThanOneHour => 'Weniger als 1 Schtund zwüsche de Schpil'; @@ -598,7 +853,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesShowFlairs => 'Benutzer-Emojis azeige'; @override - String get preferencesExplainShowPlayerRatings => 'Das erlaubt s\'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z\'konzentriere. Partie chönd immer no bewertet werde, es gaht nur um das, was du gsesch.'; + String get preferencesExplainShowPlayerRatings => 'Das erlaubt s\'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z\'konzentriere. s\'Schpiel wird immer no bewertet werde, es gaht nur drum, was mer gseht.'; @override String get preferencesDisplayBoardResizeHandle => 'Zeig de Brättgrössi Regler'; @@ -609,6 +864,9 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur im Schpiel'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachuhr'; @@ -649,7 +907,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesTakebacksWithOpponentApproval => 'Zugsrücknahm (mit Erlaubnis vom Gägner)'; @override - String get preferencesInCasualGamesOnly => 'Nur in ungwertete Schpiel'; + String get preferencesInCasualGamesOnly => 'Nur in nöd g\'wertete Schpiel'; @override String get preferencesPromoteToQueenAutomatically => 'Automatischi Umwandlig zur Dame'; @@ -709,7 +967,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesScrollOnTheBoardToReplayMoves => 'Mit em Muszeiger uf em Brätt, chasch mit em Musrad all Züg vor- und zrugg scrolle'; @override - String get preferencesCorrespondenceEmailNotification => 'Täglichi E-Mail-Benachrichtigung, wo Dini Fernschachpartie uflischtet'; + String get preferencesCorrespondenceEmailNotification => 'Täglichi E-Mail-Benachrichtigung, wo dini Korrespondänz-Schpiel uflischtet'; @override String get preferencesNotifyStreamStart => 'De Streamer gaht live'; @@ -750,6 +1008,9 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ton für Benachrichtige'; + @override + String get preferencesBlindfold => 'Blind schpille'; + @override String get puzzlePuzzles => 'Ufgabe'; @@ -843,7 +1104,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzlePuzzlesByOpenings => 'Ufgabe nach Eröffnige'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Dini meischt-gschpillte Eröffnige, i gwertete Partie'; + String get puzzleOpeningsYouPlayedTheMost => 'Dini meischt-gschpillte Eröffnige, i g\'wertete Schpiel'; @override String get puzzleUseFindInPage => 'Benutz im Browser \"Suchen...\", um dini bevorzugti Eröffnig z\'finde!'; @@ -867,7 +1128,7 @@ class AppLocalizationsGsw extends AppLocalizations { @override String puzzleFromGameLink(String param) { - return 'Us de Partie $param'; + return 'Vom Schpiel $param'; } @override @@ -942,22 +1203,22 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleFromMyGames => 'Us eigene Schpil'; @override - String get puzzleLookupOfPlayer => 'Suech Ufgabe us de Partie vu me Schpiller'; + String get puzzleLookupOfPlayer => 'Suech Ufgabe us de Schpiel vu me Schpiller'; @override String puzzleFromXGames(String param) { - return 'Ufgabe us Partie vu $param'; + return 'Ufgabe us Schpiel vu $param'; } @override String get puzzleSearchPuzzles => 'Suech Ufgabe'; @override - String get puzzleFromMyGamesNone => 'Eu häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und klassischi Partie, so erhöht sich d\'Chance, dass au Ufgabe us dine eigene Schpil zuegfüegt werded!'; + String get puzzleFromMyGamesNone => 'Du häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und au klassischi Schpiel, will so erhöht sich d\'Chance, dass Ufgabe vu dine eigene Schpiel zuegfüegt werded!'; @override String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 Ufgabe in $param2 Partie gfunde'; + return '$param1 Ufgabe in Schpiel vu $param2 gfunde'; } @override @@ -1162,7 +1423,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeEndgame => 'Ändschpil'; @override - String get puzzleThemeEndgameDescription => 'E Taktik für die letscht Fase vu de Partie.'; + String get puzzleThemeEndgameDescription => 'E Taktik für die letscht Fase vum Schpiel.'; @override String get puzzleThemeEnPassantDescription => 'E Taktik wo \"En-Passant\" beinhaltet - e Regle wo en Pur cha en gägnerische Pur schlaa, wänn de ihn mit em \"Zwei-Fälder-Zug\" übergange hät.'; @@ -1216,16 +1477,16 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeLongDescription => '3 Züg zum Sieg.'; @override - String get puzzleThemeMaster => 'Meischter Partie'; + String get puzzleThemeMaster => 'Meischter Schpiel'; @override - String get puzzleThemeMasterDescription => 'Ufgabe us Partie vu Schpiller mit Titel.'; + String get puzzleThemeMasterDescription => 'Ufgabe us Schpiel vu Schpiller mit Titel.'; @override - String get puzzleThemeMasterVsMaster => 'Meischter gäge Meischter Partie'; + String get puzzleThemeMasterVsMaster => 'Meischter gäge Meischter Schpiel'; @override - String get puzzleThemeMasterVsMasterDescription => 'Ufgabe us Partie vu 2 Schpiller mit Titel.'; + String get puzzleThemeMasterVsMasterDescription => 'Ufgabe us Schpiel vu 2 Schpiller mit Titel.'; @override String get puzzleThemeMate => 'Schachmatt'; @@ -1267,7 +1528,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeMiddlegame => 'Mittelschpiel'; @override - String get puzzleThemeMiddlegameDescription => 'E Taktik für die zweit Fase vu de Partie.'; + String get puzzleThemeMiddlegameDescription => 'E Taktik für die zweit Fase vum Schpiel.'; @override String get puzzleThemeOneMove => '1-zügigi Ufgab'; @@ -1279,7 +1540,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeOpening => 'Eröffnig'; @override - String get puzzleThemeOpeningDescription => 'E Taktik für die erscht Fase vu de Partie.'; + String get puzzleThemeOpeningDescription => 'E Taktik für die erscht Fase vum Schpiel.'; @override String get puzzleThemePawnEndgame => 'Pure Ändschpiel'; @@ -1354,10 +1615,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeSmotheredMateDescription => 'Es Schachmatt mit em Springer, wo sich de König nöd bewege cha, will er vu sine eigene Figuren umstellt-, also vollkomme igschlosse, wird.'; @override - String get puzzleThemeSuperGM => 'Super-Grossmeischter-Partie'; + String get puzzleThemeSuperGM => 'Super-Grossmeischter-Schpiel'; @override - String get puzzleThemeSuperGMDescription => 'Ufgabe us Partie, vu de beschte Schpiller uf de Wält.'; + String get puzzleThemeSuperGMDescription => 'Ufgabe us Schpiel, vu de beschte Schpiller uf de Wält.'; @override String get puzzleThemeTrappedPiece => 'G\'fangeni Figur'; @@ -1390,13 +1651,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De Gägner hät nur e limitierti Azahl Züg und Jede verschlächteret sini Schtellig.'; @override - String get puzzleThemeHealthyMix => 'En gsunde Mix'; + String get puzzleThemeMix => 'En gsunde Mix'; @override - String get puzzleThemeHealthyMixDescription => 'Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g\'fasst - genau wie bi richtige Schachschpiel.'; + String get puzzleThemeMixDescription => 'Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g\'fasst - genau wie bi richtige Schachschpiel.'; @override - String get puzzleThemePlayerGames => 'Schpiller Partie'; + String get puzzleThemePlayerGames => 'Schpiller-Schpiel'; @override String get puzzleThemePlayerGamesDescription => 'Suech nach Ufgabe us dine Schpiel oder Ufgabe us Schpiel vu Andere.'; @@ -1538,7 +1799,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get yourOpponentWantsToPlayANewGameWithYou => 'Din Gägner wett es neus Schpil mit dir schpille'; @override - String get joinTheGame => 'Tritt de Partie bi'; + String get joinTheGame => 'Gang is Schpiel'; @override String get whitePlays => 'Wiss am Zug'; @@ -1571,7 +1832,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get whiteLeftTheGame => 'Wiss hät d\'Partie verlah'; @override - String get blackLeftTheGame => 'Schwarz hät d\'Partie verlah'; + String get blackLeftTheGame => 'Schwarz hät s\'Schpiel verlah'; @override String get whiteDidntMove => 'Wiss hät nöd zoge'; @@ -1692,14 +1953,14 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get recentGames => 'Aktuelli Partie'; + String get recentGames => 'Aktuelli Schpiel'; @override - String get topGames => 'Beschti-Partie'; + String get topGames => 'Schpitzeschpiel'; @override String masterDbExplanation(String param1, String param2, String param3) { - return '2 Millione OTB Schpil vu $param1+ FIDE-gwertete Schpiller vu $param2 bis $param3'; + return '2 Millione OTB Schpiel vu $param1+ FIDE-g\'wertete Schpiller vu $param2 bis $param3'; } @override @@ -1767,9 +2028,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get byCPL => 'Nach CPL'; - @override - String get openStudy => 'Schtudie eröffne'; - @override String get enable => 'Ischalte'; @@ -1797,9 +2055,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt d\'Tüfebegränzig und haltet din Computer warm'; - @override - String get engineManager => 'Engine Betreuer'; - @override String get blunder => 'En Patzer'; @@ -1834,7 +2089,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get fiftyMovesWithoutProgress => '50 Züg ohni Fortschritt'; @override - String get currentGames => 'Laufendi Partie'; + String get currentGames => 'Laufendi Schpiel'; @override String get viewInFullSize => 'In voller Grössi azeige'; @@ -1858,7 +2113,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get computersAreNotAllowedToPlay => 'Computer und Computer-Understützig isch nöd erlaubt. Bitte lass dir bim Schpille nöd vu Schachengines, Datäbanke oder andere Schpiller hälfe. \nAu vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multikonteverhalte chann zume Usschluss fühere.'; @override - String get games => 'Partie'; + String get games => 'Schpiel'; @override String get forum => 'Forum'; @@ -2061,7 +2316,10 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get gamesPlayed => 'Gschpillti Partie'; + String get gamesPlayed => 'Schpiel g\'macht'; + + @override + String get ok => 'OK'; @override String get cancel => 'Abbräche'; @@ -2103,7 +2361,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get decline => 'Ablehnä'; @override - String get playingRightNow => 'Partie isch am laufe'; + String get playingRightNow => 'Schpiel lauft'; @override String get eventInProgress => 'Lauft jetzt'; @@ -2112,10 +2370,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get finished => 'Beändet'; @override - String get abortGame => 'Partie abbräche'; + String get abortGame => 'Schpiel abbräche'; @override - String get gameAborted => 'Partie abbroche'; + String get gameAborted => 'Schpiel abbroche'; @override String get standard => 'Standard'; @@ -2142,7 +2400,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get ratedTournament => 'Gwertet'; @override - String get thisGameIsRated => 'Das Schpiel isch gwertet'; + String get thisGameIsRated => 'Das Schpiel isch g\'wertet'; @override String get rematch => 'Revanche'; @@ -2202,7 +2460,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get freeOnlineChess => 'Gratis Online-Schach'; @override - String get exportGames => 'Partie exportiere'; + String get exportGames => 'Schpiel exportiere'; @override String get ratingRange => 'Wertigsbereich'; @@ -2280,12 +2538,12 @@ class AppLocalizationsGsw extends AppLocalizations { @override String ratedMoreThanInPerf(String param1, String param2) { - return 'Gwertet ≥ $param1 in $param2'; + return 'G\'wertet ≥ $param1 in $param2'; } @override String ratedLessThanInPerf(String param1, String param2) { - return 'Wertig ≤ $param1 im $param2 die letschte 7 Täg'; + return '≤ $param1 im $param2 die letscht Wuche'; } @override @@ -2329,7 +2587,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get location => 'Ortschaft/Land'; @override - String get filterGames => 'Partie filtere'; + String get filterGames => 'Schpiel filtere'; @override String get reset => 'Zruggsetze'; @@ -2368,7 +2626,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get toStudy => 'Schtudie'; @override - String get importGame => 'Partie importiere'; + String get importGame => 'Schpiel importiere'; @override String get importGameExplanation => 'Füeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL.'; @@ -2437,9 +2695,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get unblock => 'Blockierig ufhebe'; - @override - String get followsYou => 'Folgt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 folgt jetzt $param2'; @@ -2505,10 +2760,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get makePrivateTournament => 'Mach das Turnier privat und beschränk de Zuegang mit Passwort'; @override - String get join => 'Mach mit'; + String get join => 'Mitmache'; @override - String get withdraw => 'Usstige'; + String get withdraw => 'Usschtige'; @override String get points => 'Pünkt'; @@ -2534,7 +2789,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get pause => 'underbräche'; + String get pause => 'Pausiere'; @override String get resume => 'wieder ischtige'; @@ -2676,13 +2931,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get activePlayers => 'Aktivi Schpiller'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Achtung, das Schpiel isch gwertet, aber ohni Zitlimit!'; + String get bewareTheGameIsRatedButHasNoClock => 'Achtung, das Schpiel isch g\'wertet, aber ohni Zitlimit!'; @override String get success => 'Korräkt'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Nach em Zug automatisch zur nächschte Partie'; + String get automaticallyProceedToNextGameAfterMoving => 'Nach em Zug automatisch zum nächschte Schpiel'; @override String get autoSwitch => 'Automatische Wächsel'; @@ -2772,7 +3027,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get other => 'Suschtigs'; @override - String get reportDescriptionHelp => 'Füeg de Link dere/dene Partie bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).'; + String get reportCheatBoostHelp => 'Füeg de Link dem/dene Schpiel bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).'; + + @override + String get reportUsernameHelp => 'Erchlär, was am Benutzername nöd in Ornig isch: Säg nöd eifach \"er isch beleidigend/unagmässe\", sondern schrib eus, wie du zu dere Folgerig cho bisch. B\'sunders wänn die Beleidigung verschleieret-, nöd uf Änglisch- oder in Dialäkt isch oder wänn sie en historische oder kulturelle Bezug hät.'; + + @override + String get reportProcessedFasterInEnglish => 'Änglisch g\'schribeni Mäldige werded schnäller behandlet.'; @override String get error_provideOneCheatedGameLink => 'Bitte gib mindeschtens 1 Link zume Schpiel a, wo bschisse worde isch.'; @@ -2878,7 +3139,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get allSquaresOfTheBoard => 'Uf jedem Fäld'; @override - String get onSlowGames => 'Bi langsame Partie'; + String get onSlowGames => 'Bi langsame Schpiel'; @override String get always => 'Immer'; @@ -3030,7 +3291,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get watchGames => 'Partie zueluege'; + String get watchGames => 'Schpiel beobachte'; @override String tpTimeSpentOnTV(String param) { @@ -3113,16 +3374,16 @@ class AppLocalizationsGsw extends AppLocalizations { String get aboutSimul => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner.'; @override - String get aboutSimulImage => 'Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft; nur 1 Partie hät er verlore.'; + String get aboutSimulImage => 'Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft - nu 1 Schpiel hät er verlore.'; @override - String get aboutSimulRealLife => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Partie fertig gschpillt sind.'; + String get aboutSimulRealLife => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Schpiel fertig gschpillt sind.'; @override String get aboutSimulRules => 'Wie bi reale Simultanschach Veraschtaltige, gaht \nde Simultanschpiller vu eim Simultangägner zum \nNächschte und macht bi jedem Brätt 1 Zug.'; @override - String get aboutSimulSettings => 'Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch immer ungwertet.'; + String get aboutSimulSettings => 'Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch nie g\'wertet.'; @override String get create => 'Erschtelle'; @@ -3578,7 +3839,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get getAHint => 'Lass dir en Tipp geh'; @override - String get evaluatingYourMove => 'Din Zug wird gwertet ...'; + String get evaluatingYourMove => 'Din Zug wird g\'wertet ...'; @override String get whiteWinsGame => 'Wiss günnt'; @@ -3705,7 +3966,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get howToAvoidThis => 'Wie chasch das verhindere?'; @override - String get playEveryGame => 'Schpill jedi Partie, wo du afangsch, au fertig.'; + String get playEveryGame => 'Schpill jedes Schpiel, wo du afangsch, au fertig.'; @override String get tryToWin => 'Probier jedes Schpil z\'günne (oder mindeschtens es Remis z\'erreiche).'; @@ -3776,7 +4037,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get classicalDesc => 'Klassischi Schpiel: 25 Minute und meh'; @override - String get correspondenceDesc => 'Fernschach Partie: 1 oder mehreri Täg pro Zug'; + String get correspondenceDesc => 'Korrespondänz-Schpiel: 1 oder mehreri Täg pro Zug'; @override String get puzzleDesc => 'Schach-Taktik Trainer'; @@ -3830,7 +4091,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get youCannotPostYetPlaySomeGames => 'Du chasch nonig im Forum schribe: Schpill zerscht no es paar Partie!'; + String get youCannotPostYetPlaySomeGames => 'Du chasch no nüt im Forum schribe: Schpill vorher es paar Schpiel!'; @override String get subscribe => 'Abonniere'; @@ -3874,7 +4135,7 @@ class AppLocalizationsGsw extends AppLocalizations { @override String gameVsX(String param1) { - return 'Partie gäge $param1'; + return 'Schpiel gäge $param1'; } @override @@ -3915,7 +4176,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get tournDescriptionHelp => 'Wottsch de Teilnehmer öppis Speziells mitteile? Probier dich churz z\'fasse. Url mit Name sind möglich: [name](https://url)'; @override - String get ratedFormHelp => 'Alli Partie sind gwertet\nund beiflussed dini Wertig'; + String get ratedFormHelp => 'Alli Schpiel sind g\'wertet\nund beiflussed dini Wertig'; @override String get onlyMembersOfTeam => 'Nur Mitglider vum Team'; @@ -3924,7 +4185,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get noRestriction => 'Kei Ischränkige'; @override - String get minimumRatedGames => 'Minimum gwerteti Partie'; + String get minimumRatedGames => 'Minimum g\'werteti Schpiel'; @override String get minimumRating => 'Minimali Wertig'; @@ -3941,7 +4202,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get cancelSimul => 'Simultanschach abbräche'; @override - String get simulHostcolor => 'Farb vom Simultanschpiller, für jedi Partie'; + String get simulHostcolor => 'Farb vom Simultanschpiller, für jedes Schpiel'; @override String get estimatedStart => 'Vorussichtlichi Schtartzit'; @@ -4035,17 +4296,17 @@ class AppLocalizationsGsw extends AppLocalizations { @override String gameInProgress(String param) { - return 'Du häsch no e laufedi Partie mit $param.'; + return 'Du häsch no es laufeds Schpiel mit $param.'; } @override - String get abortTheGame => 'Partie abbräche'; + String get abortTheGame => 'Schpiel abbräche'; @override - String get resignTheGame => 'Partie ufgeh'; + String get resignTheGame => 'Schpiel ufgeh'; @override - String get youCantStartNewGame => 'Du chasch kei neui Partie starte, bevor die no Laufendi nöd fertig gschpillt isch.'; + String get youCantStartNewGame => 'Du chasch kei neus Schpiel schtarte, bevor s\'Laufende nöd fertig isch.'; @override String get since => 'Sit'; @@ -4054,7 +4315,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get until => 'Bis'; @override - String get lichessDbExplanation => 'Gwerteti Schpiel vu allne Lichess Schpiller'; + String get lichessDbExplanation => 'G\'werteti Schpiel vu allne Lichess Schpiller'; @override String get switchSides => 'Farb wächsle'; @@ -4077,13 +4338,16 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get nothingToSeeHere => 'Da gits im monumäntan nüt z\'gseh.'; + @override + String get stats => 'Schtatistike'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, other: 'Din Gägner hät d\'Partie verlah, du chasch in $count Sekunde din Sieg beaschpruche', - one: 'Din Gägner hät d\'Partie verlah. Du chasch in $count Sekunde din Sieg beaschpruche', + one: 'Din Gägner hät s\'Schpiel verlah. Du chasch in $count Sekunde de Sieg beaschpruche', ); return '$_temp0'; } @@ -4148,8 +4412,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Partie', - one: '$count Partie', + other: '$count Schpiel', + one: '$count Schpiel', ); return '$_temp0'; } @@ -4236,8 +4500,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Partie mit dir', - one: '$count Partie mit dir', + other: '$count Schpiel mit dir', + one: '$count Schpiel mit dir', ); return '$_temp0'; } @@ -4248,7 +4512,7 @@ class AppLocalizationsGsw extends AppLocalizations { count, locale: localeName, other: '$count gwerteti', - one: '$count gwertet', + one: '$count g\'wertet', ); return '$_temp0'; } @@ -4291,8 +4555,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count laufendi Partie', - one: '$count laufendi Partie', + other: '$count am Schpille', + one: '$count am Schpille', ); return '$_temp0'; } @@ -4346,8 +4610,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count gwerteti Schpiel', - one: '≥ $count gwertets Schpiel', + other: '≥ $count g\'werteti Schpiel', + one: '≥ $count g\'wertets Schpiel', ); return '$_temp0'; } @@ -4357,8 +4621,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count gwerteti $param2 Schpiel', - one: '≥ $count gwertets $param2 Schpiel', + other: '≥ $count g\'werteti $param2 Schpiel', + one: '≥ $count g\'wertets $param2 Schpiel', ); return '$_temp0'; } @@ -4368,8 +4632,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Du muesch no $count gwerteti Partie meh $param2 schpille', - one: 'Du muesch no $count gwerteti Partie meh $param2 schpille', + other: 'Du muesch no $count g\'werteti Schpiel meh $param2 schpille', + one: 'Du muesch no $count g\'wertets Schpiel meh $param2 schpille', ); return '$_temp0'; } @@ -4390,8 +4654,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count importierti Partie', - one: '$count importierti Partie', + other: '$count importierti Schpiel', + one: '$count importierts Schpiel', ); return '$_temp0'; } @@ -4445,8 +4709,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count laufendi Partie', - one: '$count laufendi Partie', + other: '$count laufendi Schpiel', + one: '$count laufends Schpiel', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer/-in'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mini Schtudie'; + + @override + String get studyStudiesIContributeTo => 'Schtudie, wo ich mitwürke'; + + @override + String get studyMyPublicStudies => 'Mini öffentliche Schtudie'; + + @override + String get studyMyPrivateStudies => 'Mini private Schtudie'; + + @override + String get studyMyFavoriteStudies => 'Mini liebschte Schtudie'; + + @override + String get studyWhatAreStudies => 'Was sind Schtudie?'; + + @override + String get studyAllStudies => 'All Schtudie'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Vu $param erschtellte Schtudie'; + } + + @override + String get studyNoneYet => 'No kei.'; + + @override + String get studyHot => 'agseit'; + + @override + String get studyDateAddedNewest => 'wänn zuegfüegt (neui)'; + + @override + String get studyDateAddedOldest => 'wänn zuegfüegt (älti)'; + + @override + String get studyRecentlyUpdated => 'frisch aktualisiert'; + + @override + String get studyMostPopular => 'beliebtschti'; + + @override + String get studyAlphabetical => 'alphabetisch'; + + @override + String get studyAddNewChapter => 'Neus Kapitel zuefüege'; + + @override + String get studyAddMembers => 'Mitglider zuefüege'; + + @override + String get studyInviteToTheStudy => 'Lad zu de Schtudie i'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bitte lad nur Lüt i, wo du kännsch und wo wänd aktiv a dere Schtudie teilneh.'; + + @override + String get studySearchByUsername => 'Suech noch Benutzername'; + + @override + String get studySpectator => 'Zueschauer'; + + @override + String get studyContributor => 'Mitwürkende'; + + @override + String get studyKick => 'Userüere'; + + @override + String get studyLeaveTheStudy => 'Schtudie verlah'; + + @override + String get studyYouAreNowAContributor => 'Du bisch jetzt en Mitwürkende'; + + @override + String get studyYouAreNowASpectator => 'Du bisch jetzt Zueschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gfallt mir'; + + @override + String get studyUnlike => 'Gfallt mir nümme'; + + @override + String get studyNewTag => 'Neue Tag'; + + @override + String get studyCommentThisPosition => 'Kommentier die Schtellig'; + + @override + String get studyCommentThisMove => 'Kommentier de Zug'; + + @override + String get studyAnnotateWithGlyphs => 'Mit Symbol kommentiere'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Das Kapitel isch z\'churz zum analysiere.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur wer a dere Schtudie mit macht, chann e Coputeranalyse afordere.'; + + @override + String get studyGetAFullComputerAnalysis => 'Erhalt e vollschtändigi, serversitigi Computeranalyse vu de Hauptvariante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Schtell sicher, dass das Kapitel vollschtändig isch. Die Analyse chann nur eimal agforderet werde.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alli synchronisierte Mitglider gsehnd die glich Schtellig'; + + @override + String get studyShareChanges => 'Teil Änderige mit de Zueschauer und speicher sie uf em Server'; + + @override + String get studyPlaying => 'Laufend'; + + @override + String get studyShowEvalBar => 'Bewertigs-Skala'; + + @override + String get studyFirst => 'zur erschte Site'; + + @override + String get studyPrevious => 'zrugg'; + + @override + String get studyNext => 'nächschti'; + + @override + String get studyLast => 'zur letschte Site'; + @override String get studyShareAndExport => 'Teile & exportiere'; + @override + String get studyCloneStudy => 'Klone'; + + @override + String get studyStudyPgn => 'Schtudie PGN'; + + @override + String get studyDownloadAllGames => 'All Schpiel abelade'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopiere'; + + @override + String get studyDownloadGame => 'Das Schpiel abelade'; + + @override + String get studyStudyUrl => 'Schtudie URL'; + + @override + String get studyCurrentChapterUrl => 'URL aktuells Kapitel'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du chasch das, zum ibinde, im Forum oder i dim Liches Tagebuech ifüege'; + + @override + String get studyStartAtInitialPosition => 'Fang ab de Grundschtellig a'; + + @override + String studyStartAtX(String param) { + return 'Fang mit $param a'; + } + + @override + String get studyEmbedInYourWebsite => 'I dini Website ibinde'; + + @override + String get studyReadMoreAboutEmbedding => 'Lies meh über s\'Ibinde'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Mer chann nur öffentlichi Schtudie ibinde!'; + + @override + String get studyOpen => 'Öffne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 präsentiert vu $param2'; + } + + @override + String get studyStudyNotFound => 'Schtudie nöd gfunde'; + + @override + String get studyEditChapter => 'Kapitel bearbeite'; + + @override + String get studyNewChapter => 'Neus Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importiers us $param'; + } + + @override + String get studyOrientation => 'Orientierig'; + + @override + String get studyAnalysisMode => 'Analyse Modus'; + + @override + String get studyPinnedChapterComment => 'Aghänkte Kapitel Kommentar'; + + @override + String get studySaveChapter => 'Kapitel schpeichere'; + + @override + String get studyClearAnnotations => 'Amerkige lösche'; + + @override + String get studyClearVariations => 'Variante lösche'; + + @override + String get studyDeleteChapter => 'Kapitel lösche'; + + @override + String get studyDeleteThisChapter => 'Kapitel lösche? Das chann nöd rückgängig gmacht werde!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Alli Kommentär, Symbol und Zeichnigsforme i dem Kapitel lösche?'; + + @override + String get studyRightUnderTheBoard => 'Diräkt underhalb vom Brätt'; + + @override + String get studyNoPinnedComment => 'Kei'; + + @override + String get studyNormalAnalysis => 'Normali Analyse'; + + @override + String get studyHideNextMoves => 'Nögschti Züg uusbländä'; + + @override + String get studyInteractiveLesson => 'Interaktivi Üäbig'; + + @override + String studyChapterX(String param) { + return 'Kapitäl $param'; + } + + @override + String get studyEmpty => 'Läär'; + + @override + String get studyStartFromInitialPosition => 'Fang vu de Usgangsschtellig a'; + + @override + String get studyEditor => 'Ändärä'; + + @override + String get studyStartFromCustomPosition => 'Fang vunere benutzerdefinierte Schtellig a'; + + @override + String get studyLoadAGameByUrl => 'Lad es Schpiel mit ere URL'; + + @override + String get studyLoadAPositionFromFen => 'Lad e Schtellig mit ere FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lad Schpiel mit eme PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL vu de Schpiel'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lad es Schpiel vo $param1 oder $param2'; + } + + @override + String get studyCreateChapter => 'Kapitäl ärschtelä'; + + @override + String get studyCreateStudy => 'Schtudie erschtelle'; + + @override + String get studyEditStudy => 'Schtudie bearbeite'; + + @override + String get studyVisibility => 'Sichtbarkeit'; + + @override + String get studyPublic => 'Öffentlich'; + + @override + String get studyUnlisted => 'Unglischtet'; + + @override + String get studyInviteOnly => 'Nur mit Iladig'; + + @override + String get studyAllowCloning => 'Chlone erlaube'; + + @override + String get studyNobody => 'Niemer'; + + @override + String get studyOnlyMe => 'Nur ich'; + + @override + String get studyContributors => 'Mitwirkändi'; + + @override + String get studyMembers => 'Mitglider'; + + @override + String get studyEveryone => 'Alli'; + + @override + String get studyEnableSync => 'Sync aktiviärä'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jawoll: Glichi Schtellig für alli'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: Unabhängigi Navigation für alli'; + + @override + String get studyPinnedStudyComment => 'Agheftete Schtudiekommentar'; + @override String get studyStart => 'Schtart'; + + @override + String get studySave => 'Schpeichärä'; + + @override + String get studyClearChat => 'Tschätt löschä'; + + @override + String get studyDeleteTheStudyChatHistory => 'Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!'; + + @override + String get studyDeleteStudy => 'Schtudie lösche'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Die ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Welli Schtudie wottsch bruche?'; + + @override + String get studyGoodMove => 'Guete Zug'; + + @override + String get studyMistake => 'Fähler'; + + @override + String get studyBrilliantMove => 'Briliantä Zug'; + + @override + String get studyBlunder => 'Grobä Patzer'; + + @override + String get studyInterestingMove => 'Intressantä Zug'; + + @override + String get studyDubiousMove => 'Frogwürdigä Zug'; + + @override + String get studyOnlyMove => 'Einzigä Zug'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Usglicheni Schtellig'; + + @override + String get studyUnclearPosition => 'Unklari Schtellig'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wiss schtaht es bitzli besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwarz schtaht es bitzli besser'; + + @override + String get studyWhiteIsBetter => 'Wiss schtaht besser'; + + @override + String get studyBlackIsBetter => 'Schwarz schtoht besser'; + + @override + String get studyWhiteIsWinning => 'Wiss schtaht uf Gwünn'; + + @override + String get studyBlackIsWinning => 'Schwarz schtoht uf Gwünn'; + + @override + String get studyNovelty => 'Neuerig'; + + @override + String get studyDevelopment => 'Entwicklig'; + + @override + String get studyInitiative => 'Initiativä'; + + @override + String get studyAttack => 'Agriff'; + + @override + String get studyCounterplay => 'Gägeschpiel'; + + @override + String get studyTimeTrouble => 'Zitnot'; + + @override + String get studyWithCompensation => 'Mit Kompänsation'; + + @override + String get studyWithTheIdea => 'Mit dä Idee'; + + @override + String get studyNextChapter => 'Nögschts Kapitäl'; + + @override + String get studyPrevChapter => 'Vorhärigs Kapitäl'; + + @override + String get studyStudyActions => 'Lärnaktionä'; + + @override + String get studyTopics => 'Theme'; + + @override + String get studyMyTopics => 'Mini Theme'; + + @override + String get studyPopularTopics => 'Beliebti Theme'; + + @override + String get studyManageTopics => 'Theme verwalte'; + + @override + String get studyBack => 'Zrugg'; + + @override + String get studyPlayAgain => 'Vo vornä'; + + @override + String get studyWhatWouldYouPlay => 'Was würdisch du ih derä Stellig spiele?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulation! Du häsch die Lektion abgschlosse.'; + + @override + String studyPerPage(String param) { + return '$param pro Site'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitäl', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Schpiel', + one: '$count Schpiel', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Mitglider', + one: '$count Mitglid', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Füeg din PGN Tegscht da i, bis zu $count Schpiel', + one: 'Füeg din PGN Tegscht da i, bis zu $count Schpiel', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'grad jetzt'; + + @override + String get timeagoRightNow => 'genau jetzt'; + + @override + String get timeagoCompleted => 'beändet'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Sekunde', + one: 'i $count Sekunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Minute', + one: 'in $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Schtunde', + one: 'i $count Schtund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Täg', + one: 'i $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Wuche', + one: 'i $count Wuche', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Mönet', + one: 'i $count Monet', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Jahr', + one: 'i $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Minute', + one: 'vor $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Schtunde', + one: 'vor $count Schtund', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Täg', + one: 'vor $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Wuche', + one: 'vor $count Wuche', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Mönet', + one: 'vor $count Monet', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Jahr', + one: 'vor $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minute blibed', + one: '$count Minute blibt', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Schtunde blibed', + one: '$count Schtund blibt', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 03f10af8d1..a9879e0dfc 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsHe extends AppLocalizations { AppLocalizationsHe([String locale = 'he']) : super(locale); @override - String get mobileHomeTab => 'בית'; + String get mobileAllGames => 'כל המשחקים'; @override - String get mobilePuzzlesTab => 'חידות'; + String get mobileAreYouSure => 'בטוח?'; @override - String get mobileToolsTab => 'כלים'; + String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; @override - String get mobileWatchTab => 'צפייה'; + String get mobileClearButton => 'ניקוי'; @override - String get mobileSettingsTab => 'הגדרות'; + String get mobileCorrespondenceClearSavedMove => 'ניקוי המהלך השמור'; @override - String get mobileMustBeLoggedIn => 'יש להתחבר כדי לצפות בדף זה.'; + String get mobileCustomGameJoinAGame => 'הצטרפות למשחק'; @override - String get mobileSystemColors => 'צבעי מערכת ההפעלה'; + String get mobileFeedbackButton => 'משוב'; @override - String get mobileFeedbackButton => 'משוב'; + String mobileGreeting(String param) { + return 'שלום, $param'; + } @override - String get mobileOkButton => 'בסדר'; + String get mobileGreetingWithoutName => 'שלום'; @override - String get mobileSettingsHapticFeedback => 'רטט בכל מהלך'; + String get mobileHideVariation => 'הסתרת וריאציות'; @override - String get mobileSettingsImmersiveMode => 'מצב ריכוז'; + String get mobileHomeTab => 'בית'; @override - String get mobileSettingsImmersiveModeSubtitle => 'הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.'; + String get mobileLiveStreamers => 'שדרנים בשידור חי'; @override - String get mobileNotFollowingAnyUser => 'אינכם עוקבים אחר אף אחד.'; + String get mobileMustBeLoggedIn => 'יש להתחבר כדי לצפות בדף זה.'; @override - String get mobileAllGames => 'כל המשחקים'; + String get mobileNoSearchResults => 'אין תוצאות'; @override - String get mobileRecentSearches => 'חיפושים אחרונים'; + String get mobileNotFollowingAnyUser => 'אינכם עוקבים אחר אף אחד.'; @override - String get mobileClearButton => 'ניקוי'; + String get mobileOkButton => 'בסדר'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,93 +64,88 @@ class AppLocalizationsHe extends AppLocalizations { } @override - String get mobileNoSearchResults => 'אין תוצאות'; + String get mobilePrefMagnifyDraggedPiece => 'הגדלת הכלי הנגרר'; @override - String get mobileAreYouSure => 'בטוח?'; + String get mobilePuzzleStormConfirmEndRun => 'האם לסיים את הסבב?'; @override - String get mobilePuzzleStreakAbortWarning => 'הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.'; + String get mobilePuzzleStormFilterNothingToShow => 'אין מה להראות. ניתן לשנות את חתכי הסינון'; @override String get mobilePuzzleStormNothingToShow => 'אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.'; @override - String get mobileSharePuzzle => 'שיתוף החידה'; + String get mobilePuzzleStormSubtitle => 'פתרו כמה שיותר חידות ב־3 דקות.'; @override - String get mobileShareGameURL => 'שיתוף הקישור למשחק'; + String get mobilePuzzleStreakAbortWarning => 'הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.'; @override - String get mobileShareGamePGN => 'שיתוף ה־PGN'; + String get mobilePuzzleThemesSubtitle => 'פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.'; @override - String get mobileSharePositionAsFEN => 'שיתוף העמדה כ־FEN'; + String get mobilePuzzlesTab => 'חידות'; @override - String get mobileShowVariations => 'הצגת וריאציות'; + String get mobileRecentSearches => 'חיפושים אחרונים'; @override - String get mobileHideVariation => 'הסתרת וריאציות'; + String get mobileSettingsHapticFeedback => 'רטט בכל מהלך'; @override - String get mobileShowComments => 'הצגת הערות'; + String get mobileSettingsImmersiveMode => 'מצב ריכוז'; @override - String get mobilePuzzleStormConfirmEndRun => 'האם לסיים את הסבב?'; + String get mobileSettingsImmersiveModeSubtitle => 'הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'אין מה להראות. ניתן לשנות את חתכי הסינון'; + String get mobileSettingsTab => 'הגדרות'; @override - String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; + String get mobileShareGamePGN => 'שיתוף ה־PGN'; @override - String get mobileCancelDrawOffer => 'ביטול הצעת התיקו'; + String get mobileShareGameURL => 'שיתוף הקישור למשחק'; @override - String get mobileWaitingForOpponentToJoin => 'ממתין שיריב יצטרף...'; + String get mobileSharePositionAsFEN => 'שיתוף העמדה כ־FEN'; @override - String get mobileBlindfoldMode => 'משחק עיוור'; + String get mobileSharePuzzle => 'שיתוף החידה'; @override - String get mobileLiveStreamers => 'שדרנים בשידור חי'; + String get mobileShowComments => 'הצגת הערות'; @override - String get mobileCustomGameJoinAGame => 'הצטרפות למשחק'; + String get mobileShowResult => 'הצגת תוצאת המשחק'; @override - String get mobileCorrespondenceClearSavedMove => 'ניקוי המהלך השמור'; + String get mobileShowVariations => 'הצגת וריאציות'; @override String get mobileSomethingWentWrong => 'משהו השתבש.'; @override - String get mobileShowResult => 'הצגת תוצאת המשחק'; - - @override - String get mobilePuzzleThemesSubtitle => 'פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.'; + String get mobileSystemColors => 'צבעי מערכת ההפעלה'; @override - String get mobilePuzzleStormSubtitle => 'פתרו כמה שיותר חידות ב־3 דקות.'; + String get mobileTheme => 'עיצוב'; @override - String mobileGreeting(String param) { - return 'שלום, $param'; - } + String get mobileToolsTab => 'כלים'; @override - String get mobileGreetingWithoutName => 'שלום'; + String get mobileWaitingForOpponentToJoin => 'ממתין שיריב יצטרף...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'צפייה'; @override String get activityActivity => 'פעילות'; @override - String get activityHostedALiveStream => 'עלה (או עלתה) לשידור חי'; + String get activityHostedALiveStream => 'על\\תה לשידור חי'; @override String activityRankedInSwissTournament(String param1, String param2) { @@ -166,7 +163,7 @@ class AppLocalizationsHe extends AppLocalizations { other: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', many: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', two: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', - one: 'תמכ/ה בליצ\'ס במשך חודש $count כ$param2', + one: 'תמכ/ה ב-lichess במשך חודש $count כ$param2', ); return '$_temp0'; } @@ -262,6 +259,19 @@ class AppLocalizationsHe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'השלים/ה $count משחקי התכתבות מסוג $param2', + many: 'השלים/ה $count משחקי התכתבות מסוג $param2', + two: 'השלים/ה $count משחקי התכתבות מסוג $param2', + one: 'השלים/ה משחק התכתבות $count מסוג $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsHe extends AppLocalizations { @override String get broadcastBroadcasts => 'הקרנות'; + @override + String get broadcastMyBroadcasts => 'ההקרנות שלי'; + @override String get broadcastLiveBroadcasts => 'צפייה ישירה בטורנירים'; + @override + String get broadcastBroadcastCalendar => 'לוח השידורים'; + + @override + String get broadcastNewBroadcast => 'הקרנה ישירה חדשה'; + + @override + String get broadcastSubscribedBroadcasts => 'הקרנות שנרשמת אליהן'; + + @override + String get broadcastAboutBroadcasts => 'הסבר על הקרנות'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'איך להשתמש בהקרנות ב־Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'הסבב החדש יכלול את אותם התורמים והחברים כמו בסבב הקודם.'; + + @override + String get broadcastAddRound => 'הוספת סבב'; + + @override + String get broadcastOngoing => 'כרגע'; + + @override + String get broadcastUpcoming => 'בקרוב'; + + @override + String get broadcastCompleted => 'שהושלמו'; + + @override + String get broadcastCompletedHelp => 'Lichess מאתר מתי הושלם הסבב על פי המשחקים שבקישור למהלכים בשידור חי (המקור). הפעילו את האפשרות הזאת אם אין מקור שממנו נשאבים המשחקים.'; + + @override + String get broadcastRoundName => 'שם סבב'; + + @override + String get broadcastRoundNumber => 'מספר סבב'; + + @override + String get broadcastTournamentName => 'שם הטורניר'; + + @override + String get broadcastTournamentDescription => 'תיאור הטורניר בקצרה'; + + @override + String get broadcastFullDescription => 'תיאור מלא של הטורניר'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'תיאור מפורט של הטורניר (אופציונאלי). $param1 זמין. אורך התיאור לא יעלה על $param2 תווים.'; + } + + @override + String get broadcastSourceSingleUrl => 'קישור המקור של ה־PGN'; + + @override + String get broadcastSourceUrlHelp => 'הקישור ש־Lichess יבדוק כדי לקלוט עדכונים ב־PGN. הוא חייב להיות פומבי ונגיש דרך האינטרנט.'; + + @override + String get broadcastSourceGameIds => 'עד 64 מזהי משחק של Lichess, מופרדים ברווחים.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'שעת ההתחלה באזור הזמן המקומי של הטורניר: $param'; + } + + @override + String get broadcastStartDateHelp => 'אופציונאלי, אם את/ה יודע/ת מתי האירוע צפוי להתחיל'; + + @override + String get broadcastCurrentGameUrl => 'הקישור למשחק הנוכחי'; + + @override + String get broadcastDownloadAllRounds => 'הורדת כל הסבבים'; + + @override + String get broadcastResetRound => 'אפס את הסיבוב הזה'; + + @override + String get broadcastDeleteRound => 'מחיקת הסבב הזה'; + + @override + String get broadcastDefinitivelyDeleteRound => 'מחיקת הסבב הזה והמשחקים שבו לצמיתות'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'מחיקת כל המשחקים בסבב הזה. כדי ליצור אותם מחדש, קישור המקור צריך להיות פעיל.'; + + @override + String get broadcastEditRoundStudy => 'עריכת לוח הלמידה של הסבב'; + + @override + String get broadcastDeleteTournament => 'מחיקת הטורניר הזה'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'מחיקה לצמיתות של הטורניר הזה, על כל סבביו והמשחקים שבו.'; + + @override + String get broadcastShowScores => 'הצגת הניקוד של השחקנים בהתבסס על תוצאות המשחקים'; + + @override + String get broadcastReplacePlayerTags => 'אופציונאלי: החלפה של שמות השחקנים, דירוגיהם ותאריהם'; + + @override + String get broadcastFideFederations => 'איגודי FIDE'; + + @override + String get broadcastTop10Rating => 'דירוג עשרת המובילים'; + + @override + String get broadcastFidePlayers => 'שחקני FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'לא נמצא שחקן FIDE'; + + @override + String get broadcastFideProfile => 'פרופיל FIDE'; + + @override + String get broadcastFederation => 'איגוד'; + + @override + String get broadcastAgeThisYear => 'גיל השנה'; + + @override + String get broadcastUnrated => 'לא מדורג'; + + @override + String get broadcastRecentTournaments => 'טורנירים אחרונים'; + + @override + String get broadcastOpenLichess => 'פתיחה ב־Lichess'; + + @override + String get broadcastTeams => 'קבוצות'; + + @override + String get broadcastBoards => 'לוחות'; + + @override + String get broadcastOverview => 'מידע כללי'; + + @override + String get broadcastSubscribeTitle => 'הירשמו כדי לקבל התראה בתחילת כל סבב. ניתן להפעיל או לבטל התראות קופצות או התראות ״פעמון״ בהגדרות החשבון שלך.'; + + @override + String get broadcastUploadImage => 'העלאת תמונה עבור הטורניר'; + + @override + String get broadcastNoBoardsYet => 'אין עדיין לוחות. הם יופיעו כשיעלו המשחקים.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'ניתן להעלות לוחות באמצעות קישור מקור או דרך $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'מתחיל אחרי $param'; + } + + @override + String get broadcastStartVerySoon => 'ההקרנה תחל ממש בקרוב.'; + + @override + String get broadcastNotYetStarted => 'ההקרנה טרם החלה.'; + + @override + String get broadcastOfficialWebsite => 'האתר הרשמי'; + + @override + String get broadcastStandings => 'תוצאות'; + + @override + String get broadcastOfficialStandings => 'טבלת מובילים רשמית'; + + @override + String broadcastIframeHelp(String param) { + return 'ישנן אפשרויות נוספות ב$param'; + } + + @override + String get broadcastWebmastersPage => 'עמוד המתכנתים'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'קישור ל־PGN פומבי המתעדכן בשידור חי. אנו מציעים גם $param לסנכרון מיטבי ומהיר.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'הטמעת ההקרנה באתר האינטרנט שלך'; + + @override + String broadcastEmbedThisRound(String param) { + return 'הטמעת $param באתר האינטרנט שלך'; + } + + @override + String get broadcastRatingDiff => 'הפרש הדירוג'; + + @override + String get broadcastGamesThisTournament => 'משחקים בטורניר זה'; + + @override + String get broadcastScore => 'ניקוד'; + + @override + String get broadcastAllTeams => 'כל הקבוצות'; + + @override + String get broadcastTournamentFormat => 'שיטת הטורניר'; + + @override + String get broadcastTournamentLocation => 'מיקום הטורניר'; + + @override + String get broadcastTopPlayers => 'שחקני צמרת'; + + @override + String get broadcastTimezone => 'אזור זמן'; + + @override + String get broadcastFideRatingCategory => 'קטגוריית דירוג FIDE'; + + @override + String get broadcastOptionalDetails => 'פרטים אופציונאליים'; + + @override + String get broadcastPastBroadcasts => 'הקרנות עבר'; + + @override + String get broadcastAllBroadcastsByMonth => 'צפו בכל ההקרנות לפי חודש'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count הקרנות', + many: '$count הקרנות', + two: '$count הקרנות', + one: 'הקרנה $count', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'הזמנות למשחק: $param1'; @@ -643,6 +902,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get preferencesInGameOnly => 'רק במהלך המשחק'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'שעון השחמט'; @@ -784,6 +1046,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'השמע צליל עבור התראות פעמון'; + @override + String get preferencesBlindfold => 'משחק עיוור'; + @override String get puzzlePuzzles => 'פאזלים'; @@ -1434,10 +1699,10 @@ class AppLocalizationsHe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'היריב מוגבל במסעים שביכולתו לבצע, וכל אחד מחמיר את מצבו.'; @override - String get puzzleThemeHealthyMix => 'שילוב בריא'; + String get puzzleThemeMix => 'שילוב בריא'; @override - String get puzzleThemeHealthyMixDescription => 'קצת מהכל. לא תדעו למה לצפות. עליכם להיות מוכנים להכל! בדיוק כמו משחקים אמיתיים.'; + String get puzzleThemeMixDescription => 'קצת מהכול. לא תדעו למה לצפות. עליכם להיות מוכנים להכול! בדיוק כמו במשחקים אמיתיים.'; @override String get puzzleThemePlayerGames => 'המשחקים שלי'; @@ -1466,7 +1731,7 @@ class AppLocalizationsHe extends AppLocalizations { String get settingsClosingIsDefinitive => 'הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?'; @override - String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות ולהיפך.'; + String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך.'; @override String get settingsChangedMindDoNotCloseAccount => 'שיניתי את דעתי, אל תסגרו את החשבון שלי'; @@ -1811,9 +2076,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get byCPL => 'עפ\"י CPL'; - @override - String get openStudy => 'פתח לוח למידה'; - @override String get enable => 'הפעלה'; @@ -1841,9 +2103,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get removesTheDepthLimit => 'מסיר את מגבלת העומק ו\"מחמם\" את המחשב'; - @override - String get engineManager => 'מנהל המנועים'; - @override String get blunder => 'טעות גסה'; @@ -2018,7 +2277,7 @@ class AppLocalizationsHe extends AppLocalizations { String get youAreLeavingLichess => 'את/ה עוזב/ת את Lichess'; @override - String get neverTypeYourPassword => 'לעולם אל תקלידו את סיסמתכם ב־Lichessבאף אתר אחר!'; + String get neverTypeYourPassword => 'לעולם אל תקלידו את סיסמתכם ב־Lichess באף אתר אחר!'; @override String proceedToX(String param) { @@ -2107,6 +2366,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get gamesPlayed => 'משחקים בטורניר'; + @override + String get ok => 'אוקיי'; + @override String get cancel => 'ביטול'; @@ -2252,7 +2514,7 @@ class AppLocalizationsHe extends AppLocalizations { String get ratingRange => 'טווח דירוג'; @override - String get thisAccountViolatedTos => 'החשבון הזה הפר את תנאי השימוש של ליצ\'ס'; + String get thisAccountViolatedTos => 'החשבון הזה הפר את תנאי השימוש של Lichess'; @override String get openingExplorerAndTablebase => 'סייר הפתיחות וטבלאות סיומים'; @@ -2481,9 +2743,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get unblock => 'בטל חסימה'; - @override - String get followsYou => 'עוקב/ת אחריך'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 התחיל/ה לעקוב אחרי $param2'; @@ -2652,7 +2911,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String reportXToModerators(String param) { - return 'דווח/י על $param למנהלים'; + return 'דווח על $param למנהלים'; } @override @@ -2711,7 +2970,7 @@ class AppLocalizationsHe extends AppLocalizations { String get clearSavedMoves => 'הסרת המהלכים'; @override - String get previouslyOnLichessTV => 'לאחרונה בטלוויזיה של ליצ\'ס'; + String get previouslyOnLichessTV => 'לאחרונה בטלוויזיה של Lichess'; @override String get onlinePlayers => 'שחקנים מחוברים'; @@ -2795,10 +3054,10 @@ class AppLocalizationsHe extends AppLocalizations { String get createTheTopic => 'צור אשכול'; @override - String get reportAUser => 'דווח/י על משתמש/ת'; + String get reportAUser => 'דיווח על משתמש/ת'; @override - String get user => 'משתמש/ת'; + String get user => 'משתמש'; @override String get reason => 'סיבה'; @@ -2816,7 +3075,13 @@ class AppLocalizationsHe extends AppLocalizations { String get other => 'אחר'; @override - String get reportDescriptionHelp => 'הדביקו את הקישור למשחק(ים) והסבירו מה לא בסדר בהתנהגות המשתמש. אל תכתבו סתם ״השחקן/ית מרמה״, הסבירו לנו כיצד הגעתם למסקנה הזו. הדיווח יטופל מהר יותר אם ייכתב באנגלית.'; + String get reportCheatBoostHelp => 'הדביקו את הקישור למשחקים שעליהם תרצו לדווח והסבירו מה הבעיה בהתנהגות המשתמש כפי שהיא משתקפת בהם. אל תכתבו סתם ״השחקן מרמה״. הסבירו לנו כיצד הגעתם למסקנה הזו.'; + + @override + String get reportUsernameHelp => 'הסבירו מה פוגעני בשם המשתמש הזה. אל תכתבו סתם ״שם המשתמש פוגעני״. הסבירו לנו כיצד הגעתם למסקנה הזו, במיוחד אם ההעלבה מוסווית, בשפה זרה (שאינה אנגלית), בלשון סלנג או תלויית תרבות והיסטוריה.'; + + @override + String get reportProcessedFasterInEnglish => 'הדיווח שלך יטופל מהר יותר אם ייכתב באנגלית.'; @override String get error_provideOneCheatedGameLink => 'בבקשה לספק לפחות קישור אחד למשחק עם רמאות.'; @@ -2987,7 +3252,7 @@ class AppLocalizationsHe extends AppLocalizations { String get opponent => 'יריב'; @override - String get learnMenu => 'למד/י'; + String get learnMenu => 'למדו'; @override String get studyMenu => 'לוחות למידה'; @@ -3193,7 +3458,7 @@ class AppLocalizationsHe extends AppLocalizations { String get simulHostExtraTimePerPlayer => 'זמן נוסף למארח/ת עבור כל שחקן/ית שמצטרף/ת'; @override - String get lichessTournaments => 'טורנירים של ליצ\'ס'; + String get lichessTournaments => 'טורנירים של Lichess'; @override String get tournamentFAQ => 'שאלות נפוצות לגבי טורנירי הזירה'; @@ -3349,7 +3614,7 @@ class AppLocalizationsHe extends AppLocalizations { } @override - String get networkLagBetweenYouAndLichess => 'עיכוב הרשת בינך לבין ליצ\'ס'; + String get networkLagBetweenYouAndLichess => 'עיכוב הרשת בינך לבין Lichess'; @override String get timeToProcessAMoveOnLichessServer => 'זמן לעיבוד מהלך בשרת ליצ\'ס'; @@ -3404,7 +3669,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'במצב ילדים הסמל של ליצ\'ס מקבל אייקון $param, כדי שתדעו שילדיכם מוגנים.'; + return 'במצב ילדים הסמל של Lichess מקבל אייקון $param, כדי שתדעו שילדיכם מוגנים.'; } @override @@ -3796,10 +4061,10 @@ class AppLocalizationsHe extends AppLocalizations { String get bullet => 'Bullet'; @override - String get blitz => 'Blitz'; + String get blitz => 'בזק'; @override - String get rapid => 'Rapid'; + String get rapid => 'זריז'; @override String get classical => 'Classical'; @@ -4121,6 +4386,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get nothingToSeeHere => 'אין כלום להצגה כאן, בינתיים.'; + @override + String get stats => 'סטטיסטיקות'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5123,731 @@ class AppLocalizationsHe extends AppLocalizations { @override String get streamerLichessStreamers => 'שדרני Lichess'; + @override + String get studyPrivate => 'פרטי'; + + @override + String get studyMyStudies => 'לוחות הלמידה שלי'; + + @override + String get studyStudiesIContributeTo => 'לוחות למידה שתרמתי להם'; + + @override + String get studyMyPublicStudies => 'לוחות הלמידה הפומביים שלי'; + + @override + String get studyMyPrivateStudies => 'לוחות הלמידה הפרטיים שלי'; + + @override + String get studyMyFavoriteStudies => 'לוחות הלמידה המועדפים שלי'; + + @override + String get studyWhatAreStudies => 'מה הם לוחות למידה?'; + + @override + String get studyAllStudies => 'כל לוחות הלמידה'; + + @override + String studyStudiesCreatedByX(String param) { + return 'לוחות למידה שנוצרו על ידי $param'; + } + + @override + String get studyNoneYet => 'אין עדיין.'; + + @override + String get studyHot => 'כוכבים עולים'; + + @override + String get studyDateAddedNewest => 'תאריך הוספה (החדש ביותר)'; + + @override + String get studyDateAddedOldest => 'תאריך הוספה (הישן ביותר)'; + + @override + String get studyRecentlyUpdated => 'עודכן לאחרונה'; + + @override + String get studyMostPopular => 'הכי פופולריים'; + + @override + String get studyAlphabetical => 'בסדר האלפבית'; + + @override + String get studyAddNewChapter => 'הוסיפו פרק חדש'; + + @override + String get studyAddMembers => 'הוספת משתמשים'; + + @override + String get studyInviteToTheStudy => 'הזמינו ללוח הלמידה'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'אנא הזמינו רק שחקנים שאתם מכירים המעוניינים להצטרף ללוח הלמידה הזה.'; + + @override + String get studySearchByUsername => 'חיפוש לפי שם משתמש'; + + @override + String get studySpectator => 'צופה'; + + @override + String get studyContributor => 'תורם'; + + @override + String get studyKick => 'הסרה'; + + @override + String get studyLeaveTheStudy => 'צא/י מלוח הלמידה'; + + @override + String get studyYouAreNowAContributor => 'כעת את/ה תורם/ת'; + + @override + String get studyYouAreNowASpectator => 'כעת את/ת צופה'; + + @override + String get studyPgnTags => 'תוויות PGN'; + + @override + String get studyLike => 'אהבתי'; + + @override + String get studyUnlike => 'ביטול \"אהבתי\"'; + + @override + String get studyNewTag => 'תג חדש'; + + @override + String get studyCommentThisPosition => 'הערה לגבי העמדה'; + + @override + String get studyCommentThisMove => 'הערה לגבי המסע'; + + @override + String get studyAnnotateWithGlyphs => 'השתמשו בסימנים מוסכמים כדי להגיב על מהלכים'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'פרק זה קצר מכדי להצדיק ניתוח.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'רק תורמי לוח הלמידה יכולים לבקש ניתוח ממוחשב.'; + + @override + String get studyGetAFullComputerAnalysis => 'קבל/י ניתוח צד־שרת מלא של המסעים העיקריים (mainline).'; + + @override + String get studyMakeSureTheChapterIsComplete => 'ניתן לבקש ניתוח ממוחשב רק פעם אחת, ולכן ודאו שהפרק הושלם.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'כולם צופים באותה העמדה'; + + @override + String get studyShareChanges => 'שתפו שינויים עם הצופים ושמרו אותם על השרת'; + + @override + String get studyPlaying => 'מתקיים כעת'; + + @override + String get studyShowEvalBar => 'מדי הערכה'; + + @override + String get studyFirst => 'ראשון'; + + @override + String get studyPrevious => 'הקודם'; + + @override + String get studyNext => 'הבא'; + + @override + String get studyLast => 'אחרון'; + @override String get studyShareAndExport => 'שיתוף & ייצוא'; + @override + String get studyCloneStudy => 'שכפול'; + + @override + String get studyStudyPgn => 'ה-PGN של לוח הלמידה'; + + @override + String get studyDownloadAllGames => 'הורדת כל המשחקים'; + + @override + String get studyChapterPgn => 'ה-PGN של הפרק'; + + @override + String get studyCopyChapterPgn => 'העתקת ה־PGN'; + + @override + String get studyDownloadGame => 'הורדת המשחק'; + + @override + String get studyStudyUrl => 'כתובת לוח הלמידה'; + + @override + String get studyCurrentChapterUrl => 'כתובת האינטרנט של הפרק הנוכחי'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'את/ה יכול/ה לפרסם את זה בפורום כדי להטמיע'; + + @override + String get studyStartAtInitialPosition => 'התחילו בעמדת הפתיחה'; + + @override + String studyStartAtX(String param) { + return 'התחילו ב$param'; + } + + @override + String get studyEmbedInYourWebsite => 'הטמעה באתר שלך'; + + @override + String get studyReadMoreAboutEmbedding => 'קראו עוד על הטמעה'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'ניתן להטמיע אך ורק לוחות למידה פומביים!'; + + @override + String get studyOpen => 'פתח'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, מוגש על ידי $param2'; + } + + @override + String get studyStudyNotFound => 'לוח הלמידה לא נמצא'; + + @override + String get studyEditChapter => 'עריכת הפרק'; + + @override + String get studyNewChapter => 'פרק חדש'; + + @override + String studyImportFromChapterX(String param) { + return 'ייבא מתוך $param'; + } + + @override + String get studyOrientation => 'כיוון הלוח'; + + @override + String get studyAnalysisMode => 'מצב ניתוח'; + + @override + String get studyPinnedChapterComment => 'תגובה מוצמדת לפרק'; + + @override + String get studySaveChapter => 'שמור פרק'; + + @override + String get studyClearAnnotations => 'נקה הערות'; + + @override + String get studyClearVariations => 'נקה וריאציות'; + + @override + String get studyDeleteChapter => 'מחיקת הפרק'; + + @override + String get studyDeleteThisChapter => 'למחוק את הפרק? אין דרך חזרה!'; + + @override + String get studyClearAllCommentsInThisChapter => 'ניקוי כל ההערות, הרישומים והציורים בפרק זה'; + + @override + String get studyRightUnderTheBoard => 'ממש מתחת ללוח'; + + @override + String get studyNoPinnedComment => 'ללא'; + + @override + String get studyNormalAnalysis => 'ניתוח רגיל'; + + @override + String get studyHideNextMoves => 'הסתרת המסעים הבאים'; + + @override + String get studyInteractiveLesson => 'שיעור אינטראקטיבי'; + + @override + String studyChapterX(String param) { + return 'פרק $param'; + } + + @override + String get studyEmpty => 'ריק'; + + @override + String get studyStartFromInitialPosition => 'התחילו מהעמדה ההתחלתית'; + + @override + String get studyEditor => 'עורך'; + + @override + String get studyStartFromCustomPosition => 'התחילו מעמדה מותאמת אישית'; + + @override + String get studyLoadAGameByUrl => 'טען משחק ע\"י כתובת אינטרנט'; + + @override + String get studyLoadAPositionFromFen => 'טען עמדה מFEN'; + + @override + String get studyLoadAGameFromPgn => 'טען משחק מPGN'; + + @override + String get studyAutomatic => 'אוטומטי'; + + @override + String get studyUrlOfTheGame => 'כתובת אינטרנטית של משחק'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'טען משחק מ$param1 או מ$param2'; + } + + @override + String get studyCreateChapter => 'צור פרק'; + + @override + String get studyCreateStudy => 'יצירת לוח למידה'; + + @override + String get studyEditStudy => 'עריכת לוח למידה'; + + @override + String get studyVisibility => 'חשיפה'; + + @override + String get studyPublic => 'פומבי'; + + @override + String get studyUnlisted => 'באמצעות קישור'; + + @override + String get studyInviteOnly => 'מוזמנים בלבד'; + + @override + String get studyAllowCloning => 'אפשרו יצירת עותקים'; + + @override + String get studyNobody => 'אף אחד'; + + @override + String get studyOnlyMe => 'רק אני'; + + @override + String get studyContributors => 'תורמים'; + + @override + String get studyMembers => 'חברים'; + + @override + String get studyEveryone => 'כולם'; + + @override + String get studyEnableSync => 'הפעל סנכרון'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'כן: שמור את כולם באותה העמדה'; + + @override + String get studyNoLetPeopleBrowseFreely => 'לא: תן לאנשים לדפדף בחופשיות'; + + @override + String get studyPinnedStudyComment => 'תגובה מוצמדת ללוח הלמידה'; + @override String get studyStart => 'שמירה'; + + @override + String get studySave => 'שמירה'; + + @override + String get studyClearChat => 'ניקוי הצ\'אט'; + + @override + String get studyDeleteTheStudyChatHistory => 'למחוק את היסטוריית הצ\'אט של לוח הלמידה? אין דרך חזרה!'; + + @override + String get studyDeleteStudy => 'מחיקת לוח למידה'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'האם למחוק את כל לוח הלמידה? אין דרך חזרה! הקלידו את שם לוח הלמידה לאישור: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'היכן ליצור את לוח הלמידה?'; + + @override + String get studyGoodMove => 'מסע טוב'; + + @override + String get studyMistake => 'טעות'; + + @override + String get studyBrilliantMove => 'מסע מבריק'; + + @override + String get studyBlunder => 'טעות חמורה'; + + @override + String get studyInterestingMove => 'מסע מעניין'; + + @override + String get studyDubiousMove => 'מסע מפוקפק'; + + @override + String get studyOnlyMove => 'המסע היחיד'; + + @override + String get studyZugzwang => 'כפאי'; + + @override + String get studyEqualPosition => 'עמדה מאוזנת'; + + @override + String get studyUnclearPosition => 'עמדה לא ברורה'; + + @override + String get studyWhiteIsSlightlyBetter => 'יתרון קל ללבן'; + + @override + String get studyBlackIsSlightlyBetter => 'יתרון קל לשחור'; + + @override + String get studyWhiteIsBetter => 'יתרון ללבן'; + + @override + String get studyBlackIsBetter => 'יתרון לשחור'; + + @override + String get studyWhiteIsWinning => 'יתרון מכריע ללבן'; + + @override + String get studyBlackIsWinning => 'יתרון מכריע לשחור'; + + @override + String get studyNovelty => 'חידוש'; + + @override + String get studyDevelopment => 'פיתוח'; + + @override + String get studyInitiative => 'יוזמה'; + + @override + String get studyAttack => 'התקפה'; + + @override + String get studyCounterplay => 'מתקפת נגד'; + + @override + String get studyTimeTrouble => 'מצוקת זמן'; + + @override + String get studyWithCompensation => 'עם פיצוי'; + + @override + String get studyWithTheIdea => 'עם הרעיון'; + + @override + String get studyNextChapter => 'הפרק הבא'; + + @override + String get studyPrevChapter => 'הפרק הקודם'; + + @override + String get studyStudyActions => 'פעולות לוח למידה'; + + @override + String get studyTopics => 'נושאים'; + + @override + String get studyMyTopics => 'הנושאים שלי'; + + @override + String get studyPopularTopics => 'נושאים פופולריים'; + + @override + String get studyManageTopics => 'עריכת נושאים'; + + @override + String get studyBack => 'חזרה'; + + @override + String get studyPlayAgain => 'הפעל שוב'; + + @override + String get studyWhatWouldYouPlay => 'מה הייתם משחקים בעמדה הזו?'; + + @override + String get studyYouCompletedThisLesson => 'מזל טוב! סיימתם את השיעור.'; + + @override + String studyPerPage(String param) { + return '$param לכל עמוד'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count פרקים', + many: '$count פרקים', + two: '$count פרקים', + one: 'פרק $count', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count משחקים', + many: '$count משחקים', + two: '$count משחקים', + one: '$count משחק', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count משתמשים', + many: '$count משתמשים', + two: '$count משתמשים', + one: 'משתמש אחד', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + many: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + two: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + one: 'הדבק את טקסט הPGN שלך כאן, עד למשחק $count', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'בדיוק עכשיו'; + + @override + String get timeagoRightNow => 'עכשיו'; + + @override + String get timeagoCompleted => 'הושלם'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שניות', + many: 'עוד $count שניות', + two: 'עוד $count שניות', + one: 'עוד שנייה', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count דקות', + many: 'עוד $count דקות', + two: 'עוד $count דקות', + one: 'עוד דקה $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שעות', + many: 'עוד $count שעות', + two: 'עוד $count שעות', + one: 'עוד שעה $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count ימים', + many: 'עוד $count ימים', + two: 'עוד $count ימים', + one: 'עוד יום $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שבועות', + many: 'עוד $count שבועות', + two: 'עוד $count שבועות', + one: 'עוד שבוע $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count חודשים', + many: 'עוד $count חודשים', + two: 'עוד $count חודשים', + one: 'עוד חודש $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שנים', + many: 'עוד $count שנים', + two: 'עוד $count שנים', + one: 'עוד שנה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count דקות', + many: 'לפני $count דקות', + two: 'לפני $count דקות', + one: 'לפני דקה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שעות', + many: 'לפני $count שעות', + two: 'לפני $count שעות', + one: 'לפני שעה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count ימים', + many: 'לפני $count ימים', + two: 'לפני $count ימים', + one: 'לפני יום $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שבועות', + many: 'לפני $count שבועות', + two: 'לפני $count שבועות', + one: 'לפני שבוע $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count חודשים', + many: 'לפני $count חודשים', + two: 'לפני $count חודשים', + one: 'לפני חודש $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שנים', + many: 'לפני $count שנים', + two: 'לפני $count שנים', + one: 'לפני שנה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count דקות נותרו', + many: '$count דקות נותרו', + two: '$count דקות נותרו', + one: 'דקה $count נותרה', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count שעות נותרו', + many: '$count שעות נותרו', + two: '$count שעות נותרו', + one: 'שעה $count נותרה', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 338aa918c6..729b7cd889 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsHi extends AppLocalizations { AppLocalizationsHi([String locale = 'hi']) : super(locale); @override - String get mobileHomeTab => 'होम'; + String get mobileAllGames => 'सारे गेम्स'; @override - String get mobilePuzzlesTab => 'पज़ल'; + String get mobileAreYouSure => 'क्या आप सुनिश्चित हैं?'; @override - String get mobileToolsTab => 'टूल्स'; + String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; @override - String get mobileWatchTab => 'देखें'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'सेटिंग'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'इस पेज को देखने के लिए आपको login करना होगा'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'फीडबैक'; @override - String get mobileFeedbackButton => 'फीडबैक'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'ओके'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'कंपन फीडबैक'; + String get mobileHideVariation => 'वेरिएशन छुपाए'; @override - String get mobileSettingsImmersiveMode => 'इमर्सिव मोड'; + String get mobileHomeTab => 'होम'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'लाइव स्ट्रीमर्स'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'इस पेज को देखने के लिए आपको login करना होगा'; @override - String get mobileAllGames => 'सारे गेम्स'; + String get mobileNoSearchResults => 'कोई परिणाम नहीं'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'ओके'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsHi extends AppLocalizations { } @override - String get mobileNoSearchResults => 'कोई परिणाम नहीं'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'क्या आप सुनिश्चित हैं?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'पज़ल शरीर करें'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'गेम URL शेयर करें'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'PGN शेयर करें'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'पोजीशन FEN के रूप में शेयर करें'; + String get mobilePuzzlesTab => 'पज़ल'; @override - String get mobileShowVariations => 'वेरिएशन देखें'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'वेरिएशन छुपाए'; + String get mobileSettingsHapticFeedback => 'कंपन फीडबैक'; @override - String get mobileShowComments => 'कमेंट्स देखें'; + String get mobileSettingsImmersiveMode => 'इमर्सिव मोड'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'सेटिंग'; @override - String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; + String get mobileShareGamePGN => 'PGN शेयर करें'; @override - String get mobileCancelDrawOffer => 'Draw प्रस्ताव रद्द करें'; + String get mobileShareGameURL => 'गेम URL शेयर करें'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'पोजीशन FEN के रूप में शेयर करें'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'पज़ल शरीर करें'; @override - String get mobileLiveStreamers => 'लाइव स्ट्रीमर्स'; + String get mobileShowComments => 'कमेंट्स देखें'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'वेरिएशन देखें'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'टूल्स'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'देखें'; @override String get activityActivity => 'कार्यकलाप'; @@ -246,6 +243,17 @@ class AppLocalizationsHi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsHi extends AppLocalizations { @override String get broadcastBroadcasts => 'प्रसारण'; + @override + String get broadcastMyBroadcasts => 'मेरा प्रसारण'; + @override String get broadcastLiveBroadcasts => 'लाइव टूर्नामेंट प्रसारण'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'नया लाइव प्रसारण'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'एक दौर जोड़ें'; + + @override + String get broadcastOngoing => 'चल रही है'; + + @override + String get broadcastUpcoming => 'आगामी'; + + @override + String get broadcastCompleted => 'पूर्ण'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'दौर का नाम'; + + @override + String get broadcastRoundNumber => 'दौर संख्या'; + + @override + String get broadcastTournamentName => 'प्रतियोगिता का नाम'; + + @override + String get broadcastTournamentDescription => 'संक्षिप्त प्रतियोगिता वर्णन'; + + @override + String get broadcastFullDescription => 'संक्षिप्त वर्णन'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'प्रसारण का वैकल्पिक लंबा विवरण. $param1 उपलब्ध है. लंबाई $param2 से कम होना चाहिए'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL जो Lichess PGN अपडेट प्राप्त करने के लिए जाँच करेगा। यह सार्वजनिक रूप से इंटरनेट पर सुलभ होना चाहिए।'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'वैकल्पिक, यदि आप जानना चाहते हो की प्रतिस्प्रधा कब शुरू होगी'; + + @override + String get broadcastCurrentGameUrl => 'वर्तमान अध्याय URL'; + + @override + String get broadcastDownloadAllRounds => 'सभी राउंड डाउनलोड करें'; + + @override + String get broadcastResetRound => 'इस फॉर्म को रीसेट करें'; + + @override + String get broadcastDeleteRound => 'इस राउंड को डिलीट करें'; + + @override + String get broadcastDefinitivelyDeleteRound => 'राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'इस दौर के सभी गेम हटाएं. उन्हें पुनः बनाने के लिए स्रोत को सक्रिय होने की आवश्यकता होगी।'; + + @override + String get broadcastEditRoundStudy => 'राउंड स्टडी संपादित करें'; + + @override + String get broadcastDeleteTournament => 'इस टूर्नामेंट को हटाएं'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'संपूर्ण टूर्नामेंट, उसके सभी राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count प्रसारण', + one: '$count प्रसारण', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get preferencesInGameOnly => 'केवल खेल में'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'शतरंज की घड़ी'; @@ -750,6 +1008,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'पहेलियाँ'; @@ -1388,10 +1649,10 @@ class AppLocalizationsHi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'प्रतिद्वंद्वी उन चालों में सीमित है जो वे कर सकते हैं, और सभी चालें उनकी स्थिति को खराब करती हैं।'; @override - String get puzzleThemeHealthyMix => 'स्वस्थ मिश्रण'; + String get puzzleThemeMix => 'स्वस्थ मिश्रण'; @override - String get puzzleThemeHealthyMixDescription => 'सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।'; + String get puzzleThemeMixDescription => 'सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।'; @override String get puzzleThemePlayerGames => 'खिलाड़ियों के खेल'; @@ -1765,9 +2026,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get byCPL => 'CPL द्वारा'; - @override - String get openStudy => 'अध्ययन खोलो'; - @override String get enable => 'सक्षम करें'; @@ -1795,9 +2053,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get removesTheDepthLimit => 'गहराई सीमा को निकालता है, और आपके कंप्यूटर को गर्म रखता है'; - @override - String get engineManager => 'इंजन प्रबंधक'; - @override String get blunder => 'भयंकर गलती'; @@ -2061,6 +2316,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get gamesPlayed => 'खेले हुए खेल'; + @override + String get ok => 'OK'; + @override String get cancel => 'रद्द करें'; @@ -2435,9 +2693,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get unblock => 'अवस्र्द्ध (ब्लॉक) न करें'; - @override - String get followsYou => 'आपका अनुसरण कर रहे हैं'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ने $param2 का अनुसरण करना शुरू किया'; @@ -2770,7 +3025,13 @@ class AppLocalizationsHi extends AppLocalizations { String get other => 'दूसरा'; @override - String get reportDescriptionHelp => 'खेल/खेलों के लिंक को लगाएं (paste) और बताएँ की यूज़र के व्यवहार में क्या खराबी है|'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'कृपया ठगे गए खेल के लिए कम से कम एक लिंक प्रदान करें।'; @@ -4073,7 +4334,10 @@ class AppLocalizationsHi extends AppLocalizations { String get lichessPatronInfo => 'Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है।\nसभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं।'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'इस समय यहां देखने को कुछ भी नहीं है।'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4721,9 +4985,693 @@ class AppLocalizationsHi extends AppLocalizations { @override String get streamerLichessStreamers => 'लिचेस स्ट्रीमर'; + @override + String get studyPrivate => 'गोपनीय'; + + @override + String get studyMyStudies => 'मेरे अध्ययन'; + + @override + String get studyStudiesIContributeTo => 'मेरे योगदान वाले अध्ययन'; + + @override + String get studyMyPublicStudies => 'मेरे सार्वजनिक अध्ययन'; + + @override + String get studyMyPrivateStudies => 'मेरे निजी अध्ययन'; + + @override + String get studyMyFavoriteStudies => 'मेरे पसंदीदा अध्ययन'; + + @override + String get studyWhatAreStudies => 'अध्ययन सामग्री क्या है'; + + @override + String get studyAllStudies => 'सभी अध्ययन'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param द्वारा बनाए गए अध्ययन'; + } + + @override + String get studyNoneYet => 'अभी तक नहीं।'; + + @override + String get studyHot => 'लोकप्रिय'; + + @override + String get studyDateAddedNewest => 'जोड़ा गया (नवीनतम)'; + + @override + String get studyDateAddedOldest => 'जोड़ा गया (सबसे पुराना)'; + + @override + String get studyRecentlyUpdated => 'हाल ही में अद्यतित'; + + @override + String get studyMostPopular => 'सबसे लोकप्रिय'; + + @override + String get studyAlphabetical => 'वर्णक्रमानुसार'; + + @override + String get studyAddNewChapter => 'एक नया अध्याय जोड़ें'; + + @override + String get studyAddMembers => 'सदस्य जोड़ें'; + + @override + String get studyInviteToTheStudy => 'अध्ययन के लिए आमंत्रित करें'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'कृपया केवल उन लोगों को आमंत्रित करें जिन्हें आप जानते हैं, और जो इस अध्ययन में सक्रिय रूप से शामिल होना चाहते हैं।'; + + @override + String get studySearchByUsername => 'यूज़रनेम से खोजें'; + + @override + String get studySpectator => 'दर्शक'; + + @override + String get studyContributor => 'योगदानकर्ता'; + + @override + String get studyKick => 'बाहर निकालें'; + + @override + String get studyLeaveTheStudy => 'अध्ययन छोड़े'; + + @override + String get studyYouAreNowAContributor => 'अब आप एक योगदानकर्ता हैं'; + + @override + String get studyYouAreNowASpectator => 'अब आप एक दर्शक हैं'; + + @override + String get studyPgnTags => 'PGN टैग'; + + @override + String get studyLike => 'लाइक'; + + @override + String get studyUnlike => 'नापसन्द करे'; + + @override + String get studyNewTag => 'नया टैग'; + + @override + String get studyCommentThisPosition => 'इस स्थिति पर टिप्पणी करें'; + + @override + String get studyCommentThisMove => 'इस चाल पर टिप्पणी करें'; + + @override + String get studyAnnotateWithGlyphs => 'प्रतीक के साथ टिप्पणी करें'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'यह अध्याय विश्लेषण के लिए बहुत छोटा है'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'केवल अध्ययन योगदानकर्ता ही कंप्यूटर विश्लेषण का अनुरोध कर सकते हैं।'; + + @override + String get studyGetAFullComputerAnalysis => 'मेनलाइन का पूर्ण सर्वर-साइड कंप्यूटर विश्लेषण प्राप्त करें।'; + + @override + String get studyMakeSureTheChapterIsComplete => 'सुनिश्चित करें कि अध्याय पूरा हो गया है। आप केवल एक बार विश्लेषण का अनुरोध कर सकते हैं'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'सभी SYNC सदस्य एक ही स्थिति पर रहेंगे'; + + @override + String get studyShareChanges => 'दर्शकों के साथ परिवर्तन साझा करें और उन्हें सर्वर पर सहेजें'; + + @override + String get studyPlaying => 'वर्तमान खेल'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'प्रथम'; + + @override + String get studyPrevious => 'पिछला'; + + @override + String get studyNext => 'अगला'; + + @override + String get studyLast => 'अंतिम'; + @override String get studyShareAndExport => 'शेयर & एक्सपोर्ट करें'; + @override + String get studyCloneStudy => 'प्रतिलिपि'; + + @override + String get studyStudyPgn => 'PGN का अध्ययन करें'; + + @override + String get studyDownloadAllGames => 'सभी खेल नीचे लादें'; + + @override + String get studyChapterPgn => 'अध्याय PGN'; + + @override + String get studyCopyChapterPgn => 'पीजीएन की नकल लें'; + + @override + String get studyDownloadGame => 'खेल नीचे लादें'; + + @override + String get studyStudyUrl => 'अध्ययन का URL'; + + @override + String get studyCurrentChapterUrl => 'वर्तमान अध्याय URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'आप अध्याय को जोड़ने के लिए इसे फ़ोरम में जोर सकते हैं'; + + @override + String get studyStartAtInitialPosition => 'प्रारंभिक स्थिति में शुरू करें'; + + @override + String studyStartAtX(String param) { + return '$param से प्रारंभ करें'; + } + + @override + String get studyEmbedInYourWebsite => 'अपनी वेबसाइट अथवा ब्लॉग पर प्रकाशित करें'; + + @override + String get studyReadMoreAboutEmbedding => 'एम्बेड करने के बारे में और पढ़ें'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'केवल सार्वजनिक अध्ययनों को एम्बेड किया जा सकता है!'; + + @override + String get studyOpen => 'खोलें'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, $param2 द्वारा आपके लिए'; + } + + @override + String get studyStudyNotFound => 'अध्ययन नहीं मिला'; + + @override + String get studyEditChapter => 'अध्याय संपादित करें'; + + @override + String get studyNewChapter => 'नया अध्याय'; + + @override + String studyImportFromChapterX(String param) { + return '$param से आयात करें'; + } + + @override + String get studyOrientation => 'अभिविन्यास'; + + @override + String get studyAnalysisMode => 'विश्लेषण प्रणाली'; + + @override + String get studyPinnedChapterComment => 'अध्याय पर की गयी महत्वपूर्ण टिप्पणी'; + + @override + String get studySaveChapter => 'अध्याय सहेजें'; + + @override + String get studyClearAnnotations => 'टिप्पणी मिटाएँ'; + + @override + String get studyClearVariations => 'विविधताओं को मिटाये'; + + @override + String get studyDeleteChapter => 'अध्याय हटाएं'; + + @override + String get studyDeleteThisChapter => 'इस अध्याय को हटाएं? हटाने के पश्चात वापसी नहीं होगी!'; + + @override + String get studyClearAllCommentsInThisChapter => 'इस अध्याय में सभी टिप्पणियाँ, प्रतीक, और आकृतियाँ साफ़ करें?'; + + @override + String get studyRightUnderTheBoard => 'बोर्ड के ठीक नीचे'; + + @override + String get studyNoPinnedComment => 'खाली'; + + @override + String get studyNormalAnalysis => 'सामान्य विश्लेषण'; + + @override + String get studyHideNextMoves => 'अगली चालें छिपाएँ'; + + @override + String get studyInteractiveLesson => 'संवादमूलक सबक'; + + @override + String studyChapterX(String param) { + return 'अध्याय $param'; + } + + @override + String get studyEmpty => 'खाली'; + + @override + String get studyStartFromInitialPosition => 'प्रारंभिक स्थिति से शुरू करें'; + + @override + String get studyEditor => 'संपादक'; + + @override + String get studyStartFromCustomPosition => 'कृत्रिम स्थिति से शुरू करें'; + + @override + String get studyLoadAGameByUrl => 'URL द्वारा एक गेम लोड करें'; + + @override + String get studyLoadAPositionFromFen => 'FEN द्वारा स्थिति लोड करें'; + + @override + String get studyLoadAGameFromPgn => 'PGN से एक गेम लोड करें'; + + @override + String get studyAutomatic => 'स्वचालित'; + + @override + String get studyUrlOfTheGame => 'खेल का URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 या $param2 से एक गेम लोड करें'; + } + + @override + String get studyCreateChapter => 'अध्याय बनाएँ'; + + @override + String get studyCreateStudy => 'अध्ययन बनाएँ'; + + @override + String get studyEditStudy => 'अध्ययन संपादित करें'; + + @override + String get studyVisibility => 'दृश्यता'; + + @override + String get studyPublic => 'सार्वजनिक'; + + @override + String get studyUnlisted => 'असूचीबद्ध'; + + @override + String get studyInviteOnly => 'केवल आमंत्रित'; + + @override + String get studyAllowCloning => 'नकल की अनुमति दें'; + + @override + String get studyNobody => 'कोई भी नहीं'; + + @override + String get studyOnlyMe => 'केवल मैं'; + + @override + String get studyContributors => 'योगदानकर्ता'; + + @override + String get studyMembers => 'सदस्य'; + + @override + String get studyEveryone => 'सभी'; + + @override + String get studyEnableSync => 'Sync चालू'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'जी हां सभी को एक ही स्थान पर रखे'; + + @override + String get studyNoLetPeopleBrowseFreely => 'नहीं सभी लोगो को अपनी इच्छा से ब्राउज करने दें'; + + @override + String get studyPinnedStudyComment => 'रुकिए पढ़िए विचार रखिए'; + @override String get studyStart => 'शुरू करिए'; + + @override + String get studySave => 'बचा कर रखिए'; + + @override + String get studyClearChat => 'बातें मिटा दे'; + + @override + String get studyDeleteTheStudyChatHistory => 'क्या इस पड़ाई से सम्बन्धित बातों को मिटा देना चाहिए? इससे पीछे जाने का कोई रास्ता शेष नहीं है!'; + + @override + String get studyDeleteStudy => 'अध्याय को मिटा दे'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'संपूर्ण अध्ययन हटाएं? वहां से कोई वापसी नहीं है! पुष्टि करने के लिए अध्ययन का नाम टाइप करें:$param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'आप इसको खा से पड़ना चाहते है'; + + @override + String get studyGoodMove => 'अच्छी चाल!'; + + @override + String get studyMistake => 'ग़लती'; + + @override + String get studyBrilliantMove => 'अद्भुत चाल​।'; + + @override + String get studyBlunder => 'भयंकर गलती'; + + @override + String get studyInterestingMove => 'दिलचस्प चाल​ |'; + + @override + String get studyDubiousMove => 'संदिग्ध चाल'; + + @override + String get studyOnlyMove => 'इकलौता चाल'; + + @override + String get studyZugzwang => 'जबरन चाल'; + + @override + String get studyEqualPosition => 'बराबर स्थिति'; + + @override + String get studyUnclearPosition => 'अस्पष्ट स्थिति'; + + @override + String get studyWhiteIsSlightlyBetter => 'सफेद थोड़ा सा बेहतर है'; + + @override + String get studyBlackIsSlightlyBetter => 'काला थोड़ा बेहतर है'; + + @override + String get studyWhiteIsBetter => 'सफेद बेहतर है!'; + + @override + String get studyBlackIsBetter => 'काला बेहतर है।'; + + @override + String get studyWhiteIsWinning => 'सफेद जीत रहा है'; + + @override + String get studyBlackIsWinning => 'काला जीत रहा है'; + + @override + String get studyNovelty => 'नवीनता'; + + @override + String get studyDevelopment => 'विकास'; + + @override + String get studyInitiative => 'पहल'; + + @override + String get studyAttack => 'आक्रमण'; + + @override + String get studyCounterplay => 'काउंटरप्ले'; + + @override + String get studyTimeTrouble => 'समय की समस्या'; + + @override + String get studyWithCompensation => 'लग मुआवजा।'; + + @override + String get studyWithTheIdea => 'विचीर के साथ।'; + + @override + String get studyNextChapter => 'अगला अध्याय।'; + + @override + String get studyPrevChapter => 'पिछला अध्याय।'; + + @override + String get studyStudyActions => 'अध्ययन क्रिया'; + + @override + String get studyTopics => 'विषय'; + + @override + String get studyMyTopics => 'मेरे विषय'; + + @override + String get studyPopularTopics => 'लोकप्रिय विषय'; + + @override + String get studyManageTopics => 'विषय प्रबंधन'; + + @override + String get studyBack => 'पीछे'; + + @override + String get studyPlayAgain => 'फिर से खेलेंगे?'; + + @override + String get studyWhatWouldYouPlay => 'आप इस स्थिति में क्या खेलेंगे?'; + + @override + String get studyYouCompletedThisLesson => 'बधाई हो! आपने यह सबक पूरा कर लिया है।'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count अध्याय', + one: '$count अध्याय', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count खेल', + one: '$count खेल', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सदस्य', + one: '$count सदस्य', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'यहां अपना PGN टेक्स्ट डाले,$count खेल तक', + one: 'यहां अपना PGN टेक्स्ट डाले,$count खेल तक', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'अभी'; + + @override + String get timeagoRightNow => 'अभी'; + + @override + String get timeagoCompleted => 'पूर्ण'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सेकंड में', + one: '$count सेकेंड में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनट में', + one: '$count मिनट में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटों में', + one: '$count घंटों में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count दिनो में', + one: '$count दिन में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count हफ़्तों में', + one: '$count हफ़्ते में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count महीनो में', + one: '$count महीने बाद​', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सालों में', + one: '$count साल में', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनटों पहले', + one: '$count मिनट पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटे पहले', + one: '$count घंटे पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count दिनों पहले', + one: '$count दिन पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सप्ताह पहले', + one: '$count सप्ताह पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count महीने पहले', + one: '$count महीने पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count वर्षों पहले', + one: '$count वर्ष पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनट बचे हैं', + one: '$count मिनट बचा है', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटे बचे हैं', + one: '$count घंटा बचा है', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 3270304cf5..73a4941508 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsHr extends AppLocalizations { AppLocalizationsHr([String locale = 'hr']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsHr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivnost'; @@ -254,6 +251,17 @@ class AppLocalizationsHr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,12 +373,260 @@ class AppLocalizationsHr extends AppLocalizations { @override String get broadcastBroadcasts => 'Prijenosi'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Prijenosi turnira uživo'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Novi prijenos uživo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Dodajte rundu'; + + @override + String get broadcastOngoing => 'U tijeku'; + + @override + String get broadcastUpcoming => 'Nadolazi'; + + @override + String get broadcastCompleted => 'Završeno'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ime runde'; + + @override + String get broadcastRoundNumber => 'Broj runde'; + + @override + String get broadcastTournamentName => 'Ime turnira'; + + @override + String get broadcastTournamentDescription => 'Kratak opis turnira'; + + @override + String get broadcastFullDescription => 'Potpuni opis događaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobavezni dugi opis prijenosa. $param1 je dostupno. Duljina mora biti manja od $param2 znakova.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Link koji će Lichess ispitavati kako bi dobio PGN ažuriranja. Mora biti javno dostupan s interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobavezno, ako znaš kada događaj počinje'; + + @override + String get broadcastCurrentGameUrl => 'URL trenutne igre'; + + @override + String get broadcastDownloadAllRounds => 'Preuzmite sve igre'; + + @override + String get broadcastResetRound => 'Resetiraj ovu rundu'; + + @override + String get broadcastDeleteRound => 'Izbriši ovu rundu'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivno izbrišite rundu i njezine igre.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbriši sve igre ovog kola. Izvor mora biti aktivan kako bi ih se ponovno stvorilo.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Izbriši ovaj turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count prijenosa', + few: '$count prijenosa', + one: '$count prijenos', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { - return 'Challenges: $param1'; + return 'Izazova: $param1'; } @override @@ -626,6 +882,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get preferencesInGameOnly => 'Samo unutar igre'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sat'; @@ -756,7 +1015,7 @@ class AppLocalizationsHr extends AppLocalizations { String get preferencesNotifyBell => 'Obavijest zvonom unutar Lichessa'; @override - String get preferencesNotifyPush => 'Obavijest uređaja kada niste na Lichessu'; + String get preferencesNotifyPush => 'Obavijest uređaja kada niste na Lichess-u'; @override String get preferencesNotifyWeb => 'Preglednik'; @@ -767,6 +1026,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Obavijest kao zvuk'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Zadaci'; @@ -1412,10 +1674,10 @@ class AppLocalizationsHr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Protivnik je prisiljen odigrati potez koji mu pogoršava poziciju.'; @override - String get puzzleThemeHealthyMix => 'Pomalo svega'; + String get puzzleThemeMix => 'Pomalo svega'; @override - String get puzzleThemeHealthyMixDescription => 'Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.'; + String get puzzleThemeMixDescription => 'Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.'; @override String get puzzleThemePlayerGames => 'Igračeve partije'; @@ -1789,9 +2051,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get byCPL => 'Po SDP'; - @override - String get openStudy => 'Otvori studiju'; - @override String get enable => 'Omogući'; @@ -1819,9 +2078,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get removesTheDepthLimit => 'Uklanja granicu do koje računalo može analizirati, i održava tvoje računalo toplim'; - @override - String get engineManager => 'Upravitelj enginea'; - @override String get blunder => 'Gruba greška'; @@ -1963,7 +2219,7 @@ class AppLocalizationsHr extends AppLocalizations { String get signupUsernameHint => 'Obavezno odaberi obiteljsko korisničko ime. Ne možeš ga kasnije promijeniti i svi računi s neprikladnim korisničkim imenima bit će zatvoreni!'; @override - String get signupEmailHint => 'Koristit ćemo ga samo za ponovno postavljanje lozinke.'; + String get signupEmailHint => 'Koristiti ćemo ga samo za ponovno postavljanje lozinke.'; @override String get password => 'Lozinka'; @@ -1987,7 +2243,7 @@ class AppLocalizationsHr extends AppLocalizations { String get error_weakPassword => 'Ova je lozinka iznimno česta i previše je lako pogoditi.'; @override - String get error_namePassword => 'Molimo da ne koristitiš svoje korisničko ime kao lozinku.'; + String get error_namePassword => 'Molimo da ne koristiš svoje korisničko ime kao lozinku.'; @override String get blankedPassword => 'Koristio si istu lozinku na drugom mjestu, a to je mjesto ugroženo. Kako bismo osigurali sigurnost tvoga Lichess računa, potrebno je da postaviš novu lozinku. Hvala na razumijevanju.'; @@ -2085,6 +2341,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get gamesPlayed => 'Broj odigranih partija'; + @override + String get ok => 'OK'; + @override String get cancel => 'Odustani'; @@ -2459,9 +2718,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Prati te'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je počeo pratiti $param2'; @@ -2653,7 +2909,7 @@ class AppLocalizationsHr extends AppLocalizations { String get editProfile => 'Uredi profil'; @override - String get realName => 'Real name'; + String get realName => 'Puno ime'; @override String get setFlair => 'Set your flair'; @@ -2668,7 +2924,7 @@ class AppLocalizationsHr extends AppLocalizations { String get biography => 'Životopis'; @override - String get countryRegion => 'Country or region'; + String get countryRegion => 'Država ili regija'; @override String get thankYou => 'Hvala!'; @@ -2794,7 +3050,13 @@ class AppLocalizationsHr extends AppLocalizations { String get other => 'Ostalo'; @override - String get reportDescriptionHelp => 'Zalijepi link na partiju/e u pitanju i objasni što nije u redu s ponašanjem korisnika. Nemoj samo reći \"varao je\", nego reci kako si došao/la do tog zaključka. Tvoja prijava bit će obrađena brže ako ju napišeš na engleskom jeziku.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Molimo navedite barem jedan link igre u kojoj je igrač varao.'; @@ -3204,10 +3466,10 @@ class AppLocalizationsHr extends AppLocalizations { String get keyEnterOrExitVariation => 'otvori/zatvori varijantu'; @override - String get keyRequestComputerAnalysis => 'Request computer analysis, Learn from your mistakes'; + String get keyRequestComputerAnalysis => 'Zatraži računalnu analizu, Uči na svojim greškama'; @override - String get keyNextLearnFromYourMistakes => 'Next (Learn from your mistakes)'; + String get keyNextLearnFromYourMistakes => 'Sljedeće (Uči na svojim greškama)'; @override String get keyNextBlunder => 'Next blunder'; @@ -3493,16 +3755,16 @@ class AppLocalizationsHr extends AppLocalizations { String get backgroundImageUrl => 'URL pozadinske slike:'; @override - String get board => 'Board'; + String get board => 'Ploča'; @override - String get size => 'Size'; + String get size => 'Veličina'; @override String get opacity => 'Opacity'; @override - String get brightness => 'Brightness'; + String get brightness => 'Svjetlina'; @override String get hue => 'Hue'; @@ -3554,7 +3816,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String notificationsX(String param1) { - return 'Notifications: $param1'; + return 'Obavijesti: $param1'; } @override @@ -3703,7 +3965,7 @@ class AppLocalizationsHr extends AppLocalizations { String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; @override - String get clickHereToReadIt => 'Click here to read it'; + String get clickHereToReadIt => 'Klikni ovdje da pročitaš'; @override String get sorry => 'Oprosti :('; @@ -3956,7 +4218,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String positionInputHelp(String param) { - return 'Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti $param za generiranje FEN pozicije, a zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.'; + return 'Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti $param za generiranje FEN pozicije, zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.'; } @override @@ -4099,6 +4361,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Statistika'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4787,9 +5052,710 @@ class AppLocalizationsHr extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess emiteri'; + @override + String get studyPrivate => 'Privatno'; + + @override + String get studyMyStudies => 'Moje studije'; + + @override + String get studyStudiesIContributeTo => 'Studije kojima pridonosim'; + + @override + String get studyMyPublicStudies => 'Moje javne studije'; + + @override + String get studyMyPrivateStudies => 'Moje privatne studije'; + + @override + String get studyMyFavoriteStudies => 'Moje omiljene studije'; + + @override + String get studyWhatAreStudies => 'Što su studije?'; + + @override + String get studyAllStudies => 'Sve studije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studije koje je stvorio $param'; + } + + @override + String get studyNoneYet => 'Još niti jedna.'; + + @override + String get studyHot => 'Aktualno'; + + @override + String get studyDateAddedNewest => 'Po datumu (najnovije)'; + + @override + String get studyDateAddedOldest => 'Po datumu (najstarije)'; + + @override + String get studyRecentlyUpdated => 'Nedavno objavljene'; + + @override + String get studyMostPopular => 'Najpopularnije'; + + @override + String get studyAlphabetical => 'Abecednim redom'; + + @override + String get studyAddNewChapter => 'Dodaj novo poglavlje'; + + @override + String get studyAddMembers => 'Dodaj članove'; + + @override + String get studyInviteToTheStudy => 'Pozovi na učenje'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Molimo da pozovete ljude koje znate i koji su voljni sudjelovati u ovoj studiji.'; + + @override + String get studySearchByUsername => 'Traži prema korisničkom imenu'; + + @override + String get studySpectator => 'Gledatelj'; + + @override + String get studyContributor => 'Suradnik'; + + @override + String get studyKick => 'Izbaci'; + + @override + String get studyLeaveTheStudy => 'Napusti studiju'; + + @override + String get studyYouAreNowAContributor => 'Postao si suradnik'; + + @override + String get studyYouAreNowASpectator => 'Postao si gledatelj'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Sviđa mi se'; + + @override + String get studyUnlike => 'Ne sviđa mi se'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentiraj ovu poziciju'; + + @override + String get studyCommentThisMove => 'Komentiraj ovaj potez'; + + @override + String get studyAnnotateWithGlyphs => 'Pribilježi glifovima'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Poglavlje je prekratko za analizu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo suradnici u studiji mogu zahtijevati računalnu analizu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dobi potpunu analizu \"main-line\" od servera.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Budite sigurni da je poglavlje gotovo. Zahtjev za računalnom analizom se može dobiti samo jednom.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Svi sinkronizirani članovi ostaju na istoj poziciji'; + + @override + String get studyShareChanges => 'Podijeli promjene sa gledateljima i pohrani ih na server'; + + @override + String get studyPlaying => 'U tijeku'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Prvi'; + + @override + String get studyPrevious => 'Prethodno'; + + @override + String get studyNext => 'Sljedeće'; + + @override + String get studyLast => 'Posljednja'; + @override String get studyShareAndExport => 'Podijeli & izvozi'; + @override + String get studyCloneStudy => 'Kloniraj'; + + @override + String get studyStudyPgn => 'Studiraj PGN'; + + @override + String get studyDownloadAllGames => 'Preuzmite sve igre'; + + @override + String get studyChapterPgn => 'PGN poglavlja'; + + @override + String get studyCopyChapterPgn => 'Kopiraj PGN'; + + @override + String get studyDownloadGame => 'Preuzmi igru'; + + @override + String get studyStudyUrl => 'Studiraj URL'; + + @override + String get studyCurrentChapterUrl => 'URL trenutnog poglavlja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Možete zaljepiti ovo u forum da ugradite poglavlje'; + + @override + String get studyStartAtInitialPosition => 'Kreni s početne pozicije'; + + @override + String studyStartAtX(String param) { + return 'Započni na $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ugradi u svoju stranicu ili blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Pročitajte više o ugradnji'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Samo javne studije mogu biti uključene!'; + + @override + String get studyOpen => 'Otvori'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam je donio $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nije pronađena'; + + @override + String get studyEditChapter => 'Uredi poglavlje'; + + @override + String get studyNewChapter => 'Novo poglavlje'; + + @override + String studyImportFromChapterX(String param) { + return 'Unesi iz $param'; + } + + @override + String get studyOrientation => 'Orijentacija'; + + @override + String get studyAnalysisMode => 'Tip analize'; + + @override + String get studyPinnedChapterComment => 'Stalni komentar na poglavlje'; + + @override + String get studySaveChapter => 'Spremi poglavlje'; + + @override + String get studyClearAnnotations => 'Očisti pribilješke'; + + @override + String get studyClearVariations => 'Očistiti varijacije'; + + @override + String get studyDeleteChapter => 'Obriši poglavlje'; + + @override + String get studyDeleteThisChapter => 'Dali želite obrisati ovo poglavlje? Nakon ovoga nema povratka!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Želite li očistiti sve komentare, glifove i nacrtane oblike u ovom poglavlju?'; + + @override + String get studyRightUnderTheBoard => 'Točno ispod table'; + + @override + String get studyNoPinnedComment => 'Ništa'; + + @override + String get studyNormalAnalysis => 'Normalna analiza'; + + @override + String get studyHideNextMoves => 'Sakrij sljedeći potez'; + + @override + String get studyInteractiveLesson => 'Interaktivna poduka'; + + @override + String studyChapterX(String param) { + return 'Poglavlje $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Kreni s početne pozicije'; + + @override + String get studyEditor => 'Uređivač'; + + @override + String get studyStartFromCustomPosition => 'Kreni s prilagođene pozicije'; + + @override + String get studyLoadAGameByUrl => 'Učitaj igru prema URL'; + + @override + String get studyLoadAPositionFromFen => 'Učitaj poziciju od FENa'; + + @override + String get studyLoadAGameFromPgn => 'Učitaj igru od PGNa'; + + @override + String get studyAutomatic => 'Automatski'; + + @override + String get studyUrlOfTheGame => 'URL igre'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Učitaj igru sa $param1 ili $param2'; + } + + @override + String get studyCreateChapter => 'Stvori poglavlje'; + + @override + String get studyCreateStudy => 'Stvori studiju'; + + @override + String get studyEditStudy => 'Uredi studiju'; + + @override + String get studyVisibility => 'Vidljivost'; + + @override + String get studyPublic => 'Javno'; + + @override + String get studyUnlisted => 'Neizlistane'; + + @override + String get studyInviteOnly => 'Samo na poziv'; + + @override + String get studyAllowCloning => 'Dopusti kloniranje'; + + @override + String get studyNobody => 'Nitko'; + + @override + String get studyOnlyMe => 'Samo ja'; + + @override + String get studyContributors => 'Suradnici'; + + @override + String get studyMembers => 'Članovi'; + + @override + String get studyEveryone => 'Svi'; + + @override + String get studyEnableSync => 'Aktiviraj sinkronizaciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: drži sve u istoj poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: neka ljudi slobodno pregledavaju'; + + @override + String get studyPinnedStudyComment => 'Stalni komentar na studije'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Spremi'; + + @override + String get studyClearChat => 'Očistite razgovor'; + + @override + String get studyDeleteTheStudyChatHistory => 'Dali želite obrisati povijest razgovora? Nakon ovoga nema povratka!'; + + @override + String get studyDeleteStudy => 'Izbriši studiju'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdje želiš to studirati?'; + + @override + String get studyGoodMove => 'Dobar potez'; + + @override + String get studyMistake => 'Greška'; + + @override + String get studyBrilliantMove => 'Briljantan potez'; + + @override + String get studyBlunder => 'Gruba greška'; + + @override + String get studyInterestingMove => 'Zanimljiv potez'; + + @override + String get studyDubiousMove => 'Sumnjiv potez'; + + @override + String get studyOnlyMove => 'Jedini potez'; + + @override + String get studyZugzwang => 'Iznudica'; + + @override + String get studyEqualPosition => 'Jednaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bijeli je u blagoj prednosti'; + + @override + String get studyBlackIsSlightlyBetter => 'Crni je u blagoj prednosti'; + + @override + String get studyWhiteIsBetter => 'Bijeli je bolji'; + + @override + String get studyBlackIsBetter => 'Crni je bolji'; + + @override + String get studyWhiteIsWinning => 'Bijeli dobija'; + + @override + String get studyBlackIsWinning => 'Crni dobija'; + + @override + String get studyNovelty => 'Nov potez'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Inicijativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protunapad'; + + @override + String get studyTimeTrouble => 'Vremenska nevolja'; + + @override + String get studyWithCompensation => 'S kompenzacijom'; + + @override + String get studyWithTheIdea => 'S idejom'; + + @override + String get studyNextChapter => 'Sljedeće poglavlje'; + + @override + String get studyPrevChapter => 'Prethodno poglavlje'; + + @override + String get studyStudyActions => 'Studijske radnje'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Popularne teme'; + + @override + String get studyManageTopics => 'Upravljaj temama'; + + @override + String get studyBack => 'Nazad'; + + @override + String get studyPlayAgain => 'Igraj ponovno'; + + @override + String get studyWhatWouldYouPlay => 'Što bi igrali u ovoj poziciji?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitamo! Završili ste lekciju.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Poglavlja', + few: '$count Poglavlja', + one: '$count Poglavlje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partije', + few: '$count Partije', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članova', + few: '$count Član', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ovdje zalijepite svoj PGN tekst, do $count igara', + few: 'Ovdje zalijepite svoj PGN tekst, do $count igri', + one: 'Ovdje zalijepite svoj PGN tekst, do $count igre', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'upravo sada'; + + @override + String get timeagoRightNow => 'upravo sada'; + + @override + String get timeagoCompleted => 'završeno'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekunda', + few: 'za $count sekundi', + one: 'za $count sekunda', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minuta', + few: 'za $count minute', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sati', + few: 'za $count sata', + one: 'za $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dana', + few: 'za $count dana', + one: 'za $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count tjedana', + few: 'za $count tjedna', + one: 'za $count tjedan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count mjeseci', + few: 'za $count mjeseca', + one: 'za $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godina', + few: 'za $count godine', + one: 'za $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count minuta', + few: 'prije $count minute', + one: 'prije $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sati', + few: 'prije $count sata', + one: 'prije $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count dana', + few: 'prije $count dana', + one: 'prije $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count tjedna', + few: 'prije $count tjedna', + one: 'prije $count tjedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count mjeseci', + few: 'prije $count mjeseca', + one: 'prije $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count godina', + few: 'prije $count godine', + one: 'prije $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index 7eec643fcf..cc9a7917b2 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsHu extends AppLocalizations { AppLocalizationsHu([String locale = 'hu']) : super(locale); @override - String get mobileHomeTab => 'Kezdőlap'; + String get mobileAllGames => 'Összes játszma'; @override - String get mobilePuzzlesTab => 'Feladvány'; + String get mobileAreYouSure => 'Biztos vagy benne?'; @override - String get mobileToolsTab => 'Eszközök'; + String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; @override - String get mobileWatchTab => 'Néznivaló'; + String get mobileClearButton => 'Törlés'; @override - String get mobileSettingsTab => 'Beállítás'; + String get mobileCorrespondenceClearSavedMove => 'Mentett lépés törlése'; @override - String get mobileMustBeLoggedIn => 'Az oldal megtekintéséhez be kell jelentkezned.'; + String get mobileCustomGameJoinAGame => 'Csatlakozás játszmához'; @override - String get mobileSystemColors => 'Rendszerszínek'; + String get mobileFeedbackButton => 'Visszajelzés'; @override - String get mobileFeedbackButton => 'Visszajelzés'; + String mobileGreeting(String param) { + return 'Üdv $param!'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Üdv'; @override - String get mobileSettingsHapticFeedback => 'Haptikus visszajelzés'; + String get mobileHideVariation => 'Változatok elrejtése'; @override - String get mobileSettingsImmersiveMode => 'Teljes képernyős mód'; + String get mobileHomeTab => 'Kezdőlap'; @override - String get mobileSettingsImmersiveModeSubtitle => 'A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.'; + String get mobileLiveStreamers => 'Lichess streamerek'; @override - String get mobileNotFollowingAnyUser => 'Jelenleg nem követsz senkit.'; + String get mobileMustBeLoggedIn => 'Az oldal megtekintéséhez be kell jelentkezned.'; @override - String get mobileAllGames => 'Az összes játszma'; + String get mobileNoSearchResults => 'Nincs találat'; @override - String get mobileRecentSearches => 'Keresési előzmények'; + String get mobileNotFollowingAnyUser => 'Jelenleg nem követsz senkit.'; @override - String get mobileClearButton => 'Törlés'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nincs találat'; + String get mobilePrefMagnifyDraggedPiece => 'Mozdított bábu nagyítása'; @override - String get mobileAreYouSure => 'Biztos vagy benne?'; + String get mobilePuzzleStormConfirmEndRun => 'Befejezed a futamot?'; @override - String get mobilePuzzleStreakAbortWarning => 'A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nincs megjeleníthető elem, változtasd meg a szűrőket'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Feladvány megosztása'; + String get mobilePuzzleStormSubtitle => 'Oldd meg a lehető legtöbb feladványt 3 perc alatt.'; @override - String get mobileShareGameURL => 'Játszma URL megosztása'; + String get mobilePuzzleStreakAbortWarning => 'A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.'; @override - String get mobileShareGamePGN => 'PGN megosztása'; + String get mobilePuzzleThemesSubtitle => 'Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.'; @override - String get mobileSharePositionAsFEN => 'Állás megosztása FEN-ként'; + String get mobilePuzzlesTab => 'Feladvány'; @override - String get mobileShowVariations => 'Változatok megjelenítése'; + String get mobileRecentSearches => 'Keresési előzmények'; @override - String get mobileHideVariation => 'Változatok elrejtése'; + String get mobileSettingsHapticFeedback => 'Érintésalapú visszajelzés'; @override - String get mobileShowComments => 'Megjegyzések megjelenítése'; + String get mobileSettingsImmersiveMode => 'Teljes képernyős mód'; @override - String get mobilePuzzleStormConfirmEndRun => 'Befejezed a futamot?'; + String get mobileSettingsImmersiveModeSubtitle => 'A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nincs megjeleníthető elem, változtasd meg a szűrőket'; + String get mobileSettingsTab => 'Beállítás'; @override - String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; + String get mobileShareGamePGN => 'PGN megosztása'; @override - String get mobileCancelDrawOffer => 'Döntetlenkérés visszavonása'; + String get mobileShareGameURL => 'Játszma URL megosztása'; @override - String get mobileWaitingForOpponentToJoin => 'Várakozás az ellenfél csatlakozására...'; + String get mobileSharePositionAsFEN => 'Állás megosztása FEN-ként'; @override - String get mobileBlindfoldMode => 'Vakjátszma mód'; + String get mobileSharePuzzle => 'Feladvány megosztása'; @override - String get mobileLiveStreamers => 'Lichess streamerek'; + String get mobileShowComments => 'Megjegyzések megjelenítése'; @override - String get mobileCustomGameJoinAGame => 'Csatlakozás játszmához'; + String get mobileShowResult => 'Eredmény mutatása'; @override - String get mobileCorrespondenceClearSavedMove => 'Mentett lépés törlése'; + String get mobileShowVariations => 'Változatok megjelenítése'; @override String get mobileSomethingWentWrong => 'Hiba történt.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Rendszerszínek'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Eszközök'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Várakozás az ellenfél csatlakozására...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Néznivaló'; @override String get activityActivity => 'Aktivitás'; @@ -246,6 +243,17 @@ class AppLocalizationsHu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsHu extends AppLocalizations { @override String get broadcastBroadcasts => 'Versenyközvetítések'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Közvetítések élő versenyekről'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Új élő versenyközvetítés'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Forduló hozzáadása'; + + @override + String get broadcastOngoing => 'Folyamatban'; + + @override + String get broadcastUpcoming => 'Közelgő'; + + @override + String get broadcastCompleted => 'Befejeződött'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Forduló neve'; + + @override + String get broadcastRoundNumber => 'Forduló száma'; + + @override + String get broadcastTournamentName => 'Verseny neve'; + + @override + String get broadcastTournamentDescription => 'Verseny rövid leírása'; + + @override + String get broadcastFullDescription => 'Esemény teljes leírása'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opcionális a közvetítés még bővebb leírása. $param1 használható. A hossz nem lehet több, mint $param2 karakter.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL amit a Lichess időnként PGN frissítésekért ellenőriz. Ennek nyilvános internetcímnek kell lennie.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcionális, ha tudod mikor kezdődik az esemény'; + + @override + String get broadcastCurrentGameUrl => 'Jelenlegi játszma URL'; + + @override + String get broadcastDownloadAllRounds => 'Összes játszma letöltése'; + + @override + String get broadcastResetRound => 'A forduló újrakezdése'; + + @override + String get broadcastDeleteRound => 'A forduló törlése'; + + @override + String get broadcastDefinitivelyDeleteRound => 'A forduló és játszmáinak végleges törlése.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Minden játék törlése ebben a fordulóban. A forrásnak aktívnak kell lennie, hogy újra létre lehessen hozni őket.'; + + @override + String get broadcastEditRoundStudy => 'Forduló tanulmányának szerkesztése'; + + @override + String get broadcastDeleteTournament => 'Verseny törlése'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Az egész verseny végleges törlése az összes fordulóval és játszmával együtt.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count versenyközvetítés', + one: '$count versenyközvetítés', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Kihívások: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get preferencesInGameOnly => 'Csak játék közben'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sakkóra'; @@ -750,6 +1008,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Hangjelzés'; + @override + String get preferencesBlindfold => 'Vakjátszma mód'; + @override String get puzzlePuzzles => 'Feladványok'; @@ -1390,10 +1651,10 @@ class AppLocalizationsHu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Az ellenfélnek kevés lehetséges lépése van, és mind csak tovább rontja a pozícióját.'; @override - String get puzzleThemeHealthyMix => 'Vegyes mix'; + String get puzzleThemeMix => 'Vegyes mix'; @override - String get puzzleThemeHealthyMixDescription => 'Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.'; + String get puzzleThemeMixDescription => 'Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.'; @override String get puzzleThemePlayerGames => 'Felhasználók játszmái'; @@ -1767,9 +2028,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Tanulmány megnyitása'; - @override String get enable => 'Engedélyezve'; @@ -1797,9 +2055,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get removesTheDepthLimit => 'Feloldja a mélységi korlátot, és melegen tartja a számítógéped'; - @override - String get engineManager => 'Motor menedzser'; - @override String get blunder => 'Baklövés'; @@ -2063,6 +2318,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get gamesPlayed => 'Lejátszott játszmák'; + @override + String get ok => 'OK'; + @override String get cancel => 'Mégse'; @@ -2437,9 +2695,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get unblock => 'Letiltás feloldása'; - @override - String get followsYou => 'Követ téged'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 követője lett'; @@ -2772,7 +3027,13 @@ class AppLocalizationsHu extends AppLocalizations { String get other => 'Egyéb'; @override - String get reportDescriptionHelp => 'Másold be a játék(ok) linkjét, és mondd el, mi a gond a játékos viselkedésével. Ne csak annyit írj, hogy \"csalt\", hanem próbáld elmondani, miből gondolod ezt. A jelentésedet hamarabb feldolgozzák, ha angolul írod.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Kérünk, legalább adj meg linket legalább egy csalt játszmához.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get nothingToSeeHere => 'Itt nincs semmi látnivaló jelenleg.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsHu extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamerek'; + @override + String get studyPrivate => 'Privát'; + + @override + String get studyMyStudies => 'Tanulmányaim'; + + @override + String get studyStudiesIContributeTo => 'Tanulmányaim szerkesztőként'; + + @override + String get studyMyPublicStudies => 'Nyilvános tanulmányaim'; + + @override + String get studyMyPrivateStudies => 'Saját tanulmányaim'; + + @override + String get studyMyFavoriteStudies => 'Kedvenc tanulmányaim'; + + @override + String get studyWhatAreStudies => 'Mik azok a tanulmányok?'; + + @override + String get studyAllStudies => 'Összes tanulmány'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param tanulmányai'; + } + + @override + String get studyNoneYet => 'Nincs még ilyen tanulmány.'; + + @override + String get studyHot => 'Felkapott'; + + @override + String get studyDateAddedNewest => 'Újabbak elöl'; + + @override + String get studyDateAddedOldest => 'Hozzáadva (legrégebbi)'; + + @override + String get studyRecentlyUpdated => 'Nemrégiben frissítve'; + + @override + String get studyMostPopular => 'Legnépszerűbb'; + + @override + String get studyAlphabetical => 'Betűrendben'; + + @override + String get studyAddNewChapter => 'Új fejezet hozzáadása'; + + @override + String get studyAddMembers => 'Tagok hozzáadása'; + + @override + String get studyInviteToTheStudy => 'Meghívás a tanulmányba'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Csak olyan ismerőst hívj meg, aki szeretne részt venni a tanulmány készítésében.'; + + @override + String get studySearchByUsername => 'Keresés felhasználónév alapján'; + + @override + String get studySpectator => 'Néző'; + + @override + String get studyContributor => 'Szerkesztő'; + + @override + String get studyKick => 'Eltávolítás'; + + @override + String get studyLeaveTheStudy => 'Tanulmány elhagyása'; + + @override + String get studyYouAreNowAContributor => 'Szerkesztő lettél'; + + @override + String get studyYouAreNowASpectator => 'Néző lettél'; + + @override + String get studyPgnTags => 'PGN címkék'; + + @override + String get studyLike => 'Kedvel'; + + @override + String get studyUnlike => 'Mégse tetszik'; + + @override + String get studyNewTag => 'Új címke'; + + @override + String get studyCommentThisPosition => 'Megjegyzés ehhez az álláshoz'; + + @override + String get studyCommentThisMove => 'Megjegyzés ehhez a lépéshez'; + + @override + String get studyAnnotateWithGlyphs => 'Lépések megjelölése'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'A fejezet túl rövid számítógépes elemzéshez.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Csak a tanulmány szerkesztői kérhetnek számítógépes elemzést.'; + + @override + String get studyGetAFullComputerAnalysis => 'Teljes szerveroldali számítógépes elemzés kérése a főváltozatról.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ellenőrizd, hogy a fejezet elkészült-e. Csak egyszer kérhető számítógépes elemzés.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Minden szinkronizált tag ugyanazt az állást látja'; + + @override + String get studyShareChanges => 'A módosítások láthatóak a nézők számára, és mentésre kerülnek a szerveren'; + + @override + String get studyPlaying => 'Folyamatban'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Első'; + + @override + String get studyPrevious => 'Előző'; + + @override + String get studyNext => 'Következő'; + + @override + String get studyLast => 'Utolsó'; + @override String get studyShareAndExport => 'Megosztás és exportálás'; + @override + String get studyCloneStudy => 'Klónozás'; + + @override + String get studyStudyPgn => 'PGN a tanulmányról'; + + @override + String get studyDownloadAllGames => 'Az összes játszma letöltése'; + + @override + String get studyChapterPgn => 'PGN a fejezetről'; + + @override + String get studyCopyChapterPgn => 'PGN másolása'; + + @override + String get studyDownloadGame => 'Játszma letöltése'; + + @override + String get studyStudyUrl => 'Tanulmány URL'; + + @override + String get studyCurrentChapterUrl => 'URL erre a fejezetre'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ezzel a linkkel beágyazhatod a fejezetet a Lichess blogodban vagy a fórumon'; + + @override + String get studyStartAtInitialPosition => 'Kezdés a kiinduló állásból'; + + @override + String studyStartAtX(String param) { + return 'Kezdés innen: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Beágyazás saját weboldalba'; + + @override + String get studyReadMoreAboutEmbedding => 'A beágyazásról bővebben'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Csak nyilvános tanulmányokat lehet beágyazni!'; + + @override + String get studyOpen => 'Megnyitás'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, a $param2 jóvoltából'; + } + + @override + String get studyStudyNotFound => 'Tanulmány nem található'; + + @override + String get studyEditChapter => 'Fejezet szerkesztése'; + + @override + String get studyNewChapter => 'Új fejezet'; + + @override + String studyImportFromChapterX(String param) { + return 'Importálás innen: $param'; + } + + @override + String get studyOrientation => 'Szemszög'; + + @override + String get studyAnalysisMode => 'Elemzés típusa'; + + @override + String get studyPinnedChapterComment => 'Rögzített megjegyzés a fejezethez'; + + @override + String get studySaveChapter => 'Fejezet mentése'; + + @override + String get studyClearAnnotations => 'Megjegyzések törlése'; + + @override + String get studyClearVariations => 'Változatok törlése'; + + @override + String get studyDeleteChapter => 'Fejezet törlése'; + + @override + String get studyDeleteThisChapter => 'Törlöd a fejezetet? Ezt nem lehet visszavonni!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Minden megjegyzés, lépésjelölés és rajz törlése a fejezetből'; + + @override + String get studyRightUnderTheBoard => 'Közvetlenül a tábla alatt'; + + @override + String get studyNoPinnedComment => 'Nincs'; + + @override + String get studyNormalAnalysis => 'Normál elemzés'; + + @override + String get studyHideNextMoves => 'Következő lépések elrejtése'; + + @override + String get studyInteractiveLesson => 'Interaktív lecke'; + + @override + String studyChapterX(String param) { + return '$param. fejezet'; + } + + @override + String get studyEmpty => 'Üres'; + + @override + String get studyStartFromInitialPosition => 'Kezdés az alapállásból'; + + @override + String get studyEditor => 'Szerkesztő'; + + @override + String get studyStartFromCustomPosition => 'Kezdés tetszőleges állásból'; + + @override + String get studyLoadAGameByUrl => 'Játszmák betöltése linkkel'; + + @override + String get studyLoadAPositionFromFen => 'Állás betöltése FEN-ből'; + + @override + String get studyLoadAGameFromPgn => 'Játszmák betöltése PGN-ből'; + + @override + String get studyAutomatic => 'Automatikus'; + + @override + String get studyUrlOfTheGame => 'Játszmák linkje, soronként egy'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Játszmák betöltése $param1 vagy $param2 szerverről'; + } + + @override + String get studyCreateChapter => 'Fejezet létrehozása'; + + @override + String get studyCreateStudy => 'Tanulmány létrehozása'; + + @override + String get studyEditStudy => 'Tanulmány szerkesztése'; + + @override + String get studyVisibility => 'Láthatóság'; + + @override + String get studyPublic => 'Nyilvános'; + + @override + String get studyUnlisted => 'Nincs listázva'; + + @override + String get studyInviteOnly => 'Csak meghívással'; + + @override + String get studyAllowCloning => 'Klónozható'; + + @override + String get studyNobody => 'Senki'; + + @override + String get studyOnlyMe => 'Csak én'; + + @override + String get studyContributors => 'Szerkesztők'; + + @override + String get studyMembers => 'Tagok'; + + @override + String get studyEveryone => 'Mindenki'; + + @override + String get studyEnableSync => 'Sync engedélyezése'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Igen: mindenki ugyanazt az állást látja'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nem: szabadon böngészhető'; + + @override + String get studyPinnedStudyComment => 'Rögzített megjegyzés a tanulmányhoz'; + @override String get studyStart => 'Mehet'; + + @override + String get studySave => 'Mentés'; + + @override + String get studyClearChat => 'Chat törlése'; + + @override + String get studyDeleteTheStudyChatHistory => 'Biztosan törlöd a chat előzményeket a tanulmányból? Ezt nem lehet visszavonni!'; + + @override + String get studyDeleteStudy => 'Tanulmány törlése'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Törlöd a teljes tanulmányt? Ezt nem lehet visszavonni! Gépeld be a tanulmány nevét a megerősítéshez: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Melyik tanulmányba kerüljön?'; + + @override + String get studyGoodMove => 'Jó lépés'; + + @override + String get studyMistake => 'Hiba'; + + @override + String get studyBrilliantMove => 'Kiváló lépés'; + + @override + String get studyBlunder => 'Durva hiba'; + + @override + String get studyInterestingMove => 'Érdekes lépés'; + + @override + String get studyDubiousMove => 'Szokatlan lépés'; + + @override + String get studyOnlyMove => 'Egyetlen lépés'; + + @override + String get studyZugzwang => 'Lépéskényszer'; + + @override + String get studyEqualPosition => 'Egyenlő állás'; + + @override + String get studyUnclearPosition => 'Zavaros állás'; + + @override + String get studyWhiteIsSlightlyBetter => 'Világos kicsit jobban áll'; + + @override + String get studyBlackIsSlightlyBetter => 'Sötét kicsit jobban áll'; + + @override + String get studyWhiteIsBetter => 'Világos jobban áll'; + + @override + String get studyBlackIsBetter => 'Sötét jobban áll'; + + @override + String get studyWhiteIsWinning => 'Világos nyerésre áll'; + + @override + String get studyBlackIsWinning => 'Sötét nyerésre áll'; + + @override + String get studyNovelty => 'Újítás'; + + @override + String get studyDevelopment => 'Fejlődés'; + + @override + String get studyInitiative => 'Kezdeményezés'; + + @override + String get studyAttack => 'Támadás'; + + @override + String get studyCounterplay => 'Ellenjáték'; + + @override + String get studyTimeTrouble => 'Időzavar'; + + @override + String get studyWithCompensation => 'Kompenzáció'; + + @override + String get studyWithTheIdea => 'Elképzelés'; + + @override + String get studyNextChapter => 'Következő fejezet'; + + @override + String get studyPrevChapter => 'Előző fejezet'; + + @override + String get studyStudyActions => 'Műveletek a tanulmányban'; + + @override + String get studyTopics => 'Témakörök'; + + @override + String get studyMyTopics => 'Témaköreim'; + + @override + String get studyPopularTopics => 'Népszerű témakörök'; + + @override + String get studyManageTopics => 'Témakörök kezelése'; + + @override + String get studyBack => 'Vissza'; + + @override + String get studyPlayAgain => 'Újra'; + + @override + String get studyWhatWouldYouPlay => 'Mit lépnél ebben az állásban?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulálok! A fejezet végére értél.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Fejezet', + one: '$count Fejezet', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Játszma', + one: '$count Játszma', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Tag', + one: '$count Tag', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Illeszd be a PGN szövegét (legfeljebb $count játszma)', + one: 'Illeszd be a PGN szövegét legfeljebb $count játszmáig', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'épp most'; + + @override + String get timeagoRightNow => 'épp most'; + + @override + String get timeagoCompleted => 'befejeződött'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count másodperc múlva', + one: '$count másodperc múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perc múlva', + one: '$count perc múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count óra múlva', + one: '$count óra múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nap múlva', + one: '$count nap múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hét múlva', + one: '$count hét múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hónap múlva', + one: '$count hónap múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count év múlva', + one: '$count év múlva', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perce', + one: '$count perce', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count órája', + one: '$count órája', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count napja', + one: '$count napja', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hete', + one: '$count hete', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hónapja', + one: '$count hónapja', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count éve', + one: '$count éve', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perc van hátra', + one: '$count perc van hátra', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count óra van hátra', + one: '$count óra van hátra', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index e67f7d9221..21a800e7fe 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsHy extends AppLocalizations { AppLocalizationsHy([String locale = 'hy']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsHy extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Գործունեություն'; @@ -246,6 +243,17 @@ class AppLocalizationsHy extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsHy extends AppLocalizations { @override String get broadcastBroadcasts => 'Հեռարձակումներ'; + @override + String get broadcastMyBroadcasts => 'Իմ հեռարձակումները'; + @override String get broadcastLiveBroadcasts => 'Մրցաշարի ուղիղ հեռարձակումներ'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Նոր ուղիղ հեռարձակում'; + + @override + String get broadcastSubscribedBroadcasts => 'Բաժանորդագրված հեռարձակումներ'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Ավելացնել խաղափուլ'; + + @override + String get broadcastOngoing => 'Ընթացիկ'; + + @override + String get broadcastUpcoming => 'Առաջիկայում սպասվող'; + + @override + String get broadcastCompleted => 'Ավարտված'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Խաղափուլի անվանում'; + + @override + String get broadcastRoundNumber => 'Խաղափուլի համար'; + + @override + String get broadcastTournamentName => 'Մրցաշարի անվանում'; + + @override + String get broadcastTournamentDescription => 'Իրադարձության համառոտ նկարագրություն'; + + @override + String get broadcastFullDescription => 'Իրադարձության ամբողջական նկարագրություն'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Լրացուցիչ, եթե գիտեք, թե երբ է սկսվելու իրադարձությունը'; + + @override + String get broadcastCurrentGameUrl => 'Ընթացիկ պարտիայի URL-հասցեն'; + + @override + String get broadcastDownloadAllRounds => 'Բեռնել բոլոր խաղափուլերը'; + + @override + String get broadcastResetRound => 'Հեռացնել այս խաղափուլը'; + + @override + String get broadcastDeleteRound => 'Հեռացնել այս խաղափուլը'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Խմբագրել խաղափուլի ստուդիան'; + + @override + String get broadcastDeleteTournament => 'Հեռացնել այս մրցաշարը'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Վերջնականապես հեռացնել ամբողջ մրցաշարը, նրա խաղափուլերը և պարտիաները։'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Մարտահրավերներ: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get preferencesInGameOnly => 'Միայն խաղի մեջ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'շախմատի ժամացույց'; @@ -750,6 +1008,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ծանուցումների զանգակի ձայնը'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Խնդիրներ'; @@ -1390,10 +1651,10 @@ class AppLocalizationsHy extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Մրցակիցը ստիպված է անել հնարավոր փոքրաթիվ քայլերից մեկը, բայց քայլերից ցանկացածը տանում է դիրքի վատացման։'; @override - String get puzzleThemeHealthyMix => 'Խառը խնդիրներ'; + String get puzzleThemeMix => 'Խառը խնդիրներ'; @override - String get puzzleThemeHealthyMixDescription => 'Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։'; + String get puzzleThemeMixDescription => 'Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։'; @override String get puzzleThemePlayerGames => 'Խաղացողի պարտիաները'; @@ -1767,9 +2028,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get byCPL => 'Ըստ սխալների'; - @override - String get openStudy => 'Բացել ուսուցումը'; - @override String get enable => 'Միացնել'; @@ -1797,9 +2055,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get removesTheDepthLimit => 'Վերացնում է խորության սահմանափակումը և տաք պահում ձեր համակարգիչը'; - @override - String get engineManager => 'Շարժիչի մենեջեր'; - @override String get blunder => 'Վրիպում'; @@ -2063,6 +2318,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get gamesPlayed => 'Խաղացած խաղեր'; + @override + String get ok => 'OK'; + @override String get cancel => 'Հերքել'; @@ -2437,9 +2695,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get unblock => 'Հանել արգելափակումը'; - @override - String get followsYou => 'Հետևում են ձեզ'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1-ը այժմ հետևում է $param2-ին'; @@ -2772,7 +3027,13 @@ class AppLocalizationsHy extends AppLocalizations { String get other => 'այլ'; @override - String get reportDescriptionHelp => 'Կիսվեք մեզ հետ Այն խաղերի հղումներով, որտեղ կարծում եք, որ կանոնները խախտվել են և նկարագրեք, թե ինչն է սխալ: Բավական չէ պարզապես գրել \"Նա խարդախում է\", խնդրում ենք նկարագրել, թե ինչպես եք եկել այս եզրակացության: Մենք ավելի արագ կաշխատենք, եթե գրեք անգլերեն:'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Խնդրում ենք ավելացնել առնվազն մեկ խաղի հղում, որտեղ ձեր կարծիքով խախտվել են կանոնները:'; @@ -4077,6 +4338,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsHy extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-ի հեռարձակողներ'; + @override + String get studyPrivate => 'Անձնական'; + + @override + String get studyMyStudies => 'Իմ ստուդիաները'; + + @override + String get studyStudiesIContributeTo => 'Իմ մասնակցությամբ ստուդիաները'; + + @override + String get studyMyPublicStudies => 'Իմ հանրային ստուդիաները'; + + @override + String get studyMyPrivateStudies => 'Իմ անձնական ստուդիաները'; + + @override + String get studyMyFavoriteStudies => 'Իմ սիրելի ստուդիաները'; + + @override + String get studyWhatAreStudies => 'Ի՞նչ են «ստուդիաները»'; + + @override + String get studyAllStudies => 'Բոլոր ստուդիաները'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param-ի ստեղծած ստուդիաները'; + } + + @override + String get studyNoneYet => 'Առայժմ ոչինչ։'; + + @override + String get studyHot => 'Ամենաակտիվները'; + + @override + String get studyDateAddedNewest => 'Վերջերս ավելացվածները'; + + @override + String get studyDateAddedOldest => 'Վաղուց ավելացվածները'; + + @override + String get studyRecentlyUpdated => 'Վերջերս թարմացվածները'; + + @override + String get studyMostPopular => 'Ամենահայտնիները'; + + @override + String get studyAlphabetical => 'Այբբենական կարգով'; + + @override + String get studyAddNewChapter => 'Ավելացնել նոր գլուխ'; + + @override + String get studyAddMembers => 'Ավելացնել մասնակիցների'; + + @override + String get studyInviteToTheStudy => 'Հրավիրել ստուդիա'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Հրավիրեք միայն այն մասնակիցներին, որոնց ճանաչում եք, և որոնք ակտիվորեն ցանկանում են միանալ այս ստուդիային։'; + + @override + String get studySearchByUsername => 'Որոնում ըստ մասնակցային անվան'; + + @override + String get studySpectator => 'Հանդիսատես'; + + @override + String get studyContributor => 'Խմբագիր'; + + @override + String get studyKick => 'Վռնդել'; + + @override + String get studyLeaveTheStudy => 'Լքել ստուդիան'; + + @override + String get studyYouAreNowAContributor => 'Այժմ Դուք խմբագիր եք'; + + @override + String get studyYouAreNowASpectator => 'Այժմ Դուք հանդիսական եք'; + + @override + String get studyPgnTags => 'PGN-ի թեգերը'; + + @override + String get studyLike => 'Հավանել'; + + @override + String get studyUnlike => 'Չեմ հավանում'; + + @override + String get studyNewTag => 'Նոր թեգ'; + + @override + String get studyCommentThisPosition => 'Մեկնաբանել այս դիրքը'; + + @override + String get studyCommentThisMove => 'Մեկնաբանել այս քայլը'; + + @override + String get studyAnnotateWithGlyphs => 'Ավելացնել սիմվոլներով անոտացիա'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Վերլուծության համար գլուխը չափազանց կարճ է։'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Միայն ստուդիայի խմբագիրները կարող են խնդրել համակարգչային վերլուծություն։'; + + @override + String get studyGetAFullComputerAnalysis => 'Սերվերից ստանալ գլխավոր գծի ամբողջական համակարգչային վերլուծություն։'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Համոզվեք, որ գլուխն ավարտված է։ Համակարգչային վերլուծություն կարող եք խնդրել միայն մեկ անգամ։'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Բոլոր սինքրոնիզացված մասնակիցները մնում են նույն դիրքում'; + + @override + String get studyShareChanges => 'Փոփոխությունները տարածել հանդիսականների շրջանում և դրանք պահպանել սերվերում'; + + @override + String get studyPlaying => 'Ակտիվ'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Առաջինը'; + + @override + String get studyPrevious => 'Նախորդը'; + + @override + String get studyNext => 'Հաջորդը'; + + @override + String get studyLast => 'Վերջինը'; + @override String get studyShareAndExport => 'Տարածել & և արտահանել'; + @override + String get studyCloneStudy => 'Կլոնավորել'; + + @override + String get studyStudyPgn => 'Ստուդիայի PGN-ն'; + + @override + String get studyDownloadAllGames => 'Ներբեռնել բոլոր պարտիաները'; + + @override + String get studyChapterPgn => 'Գլխի PGN-ը'; + + @override + String get studyCopyChapterPgn => 'Պատճենել PGN-ը'; + + @override + String get studyDownloadGame => 'Ներբեռնել պարտիան'; + + @override + String get studyStudyUrl => 'Ստուդիայի հղումը'; + + @override + String get studyCurrentChapterUrl => 'Այս գլխի հղումը'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ֆորումում կամ Lichess-ի բլոգում ներդնելու համար տեղադրեք այս կոդը'; + + @override + String get studyStartAtInitialPosition => 'Բացել սկզբնական դիրքում'; + + @override + String studyStartAtX(String param) { + return 'Սկսել $param-ից'; + } + + @override + String get studyEmbedInYourWebsite => 'Ներդնել սեփական կայքում կամ բլոգում'; + + @override + String get studyReadMoreAboutEmbedding => 'Մանրամասն կայքում ներդնելու մասին'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Կայքում կարելի է ներդնել միայն հրապարակային ստուդիաները։'; + + @override + String get studyOpen => 'Բացել'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1-ը $param2-ից'; + } + + @override + String get studyStudyNotFound => 'Ստուդիան չի գտնվել'; + + @override + String get studyEditChapter => 'Խմբագրել գլուխը'; + + @override + String get studyNewChapter => 'Նոր գլուխ'; + + @override + String studyImportFromChapterX(String param) { + return 'Ներդնել $param-ից'; + } + + @override + String get studyOrientation => 'Կողմնորոշում'; + + @override + String get studyAnalysisMode => 'Վերլուծության ռեժիմ'; + + @override + String get studyPinnedChapterComment => 'Գլխի ամրակցված մեկնաբանություն'; + + @override + String get studySaveChapter => 'Պահպանել գլուխը'; + + @override + String get studyClearAnnotations => 'Հեռացնել անոտացիան'; + + @override + String get studyClearVariations => 'Հեռացնել տարբերակները'; + + @override + String get studyDeleteChapter => 'Հեռացնել գլուխը'; + + @override + String get studyDeleteThisChapter => 'Հեռացնե՞լ գլուխը։ Վերականգնել հնարավոր չի լինի։'; + + @override + String get studyClearAllCommentsInThisChapter => 'Մաքրե՞լ այս գլխի բոլոր մեկնաբանություններն ու նշումները'; + + @override + String get studyRightUnderTheBoard => 'Անմիջապես տախտակի տակ'; + + @override + String get studyNoPinnedComment => 'Ոչ'; + + @override + String get studyNormalAnalysis => 'Սովորական վերլուծություն'; + + @override + String get studyHideNextMoves => 'Թաքցնել հետագա քայլերը'; + + @override + String get studyInteractiveLesson => 'Ինտերակտիվ դասընթաց'; + + @override + String studyChapterX(String param) { + return 'Գլուխ $param'; + } + + @override + String get studyEmpty => 'Դատարկ է'; + + @override + String get studyStartFromInitialPosition => 'Սկսել նախնական դիրքից'; + + @override + String get studyEditor => 'Խմբագիր'; + + @override + String get studyStartFromCustomPosition => 'Սկսել սեփական դիրքից'; + + @override + String get studyLoadAGameByUrl => 'Բեռնել պարտիան ըստ URL-ի'; + + @override + String get studyLoadAPositionFromFen => 'Բեռնել դիրքը FEN-ով'; + + @override + String get studyLoadAGameFromPgn => 'Բեռնել դիրքն ըստ PGN-ի'; + + @override + String get studyAutomatic => 'Ինքնաբերաբար'; + + @override + String get studyUrlOfTheGame => 'Պարտիայի URL-ը, մեկ տողով'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ներբեռնել խաղը $param1-ից կամ $param2-ից'; + } + + @override + String get studyCreateChapter => 'Ստեղծել գլուխը'; + + @override + String get studyCreateStudy => 'Ստեղծել ստուդիա'; + + @override + String get studyEditStudy => 'Խմբագրել ստուդիան'; + + @override + String get studyVisibility => 'Հասանելի է դիտման համար'; + + @override + String get studyPublic => 'Հրապարակային'; + + @override + String get studyUnlisted => 'Հղումով'; + + @override + String get studyInviteOnly => 'Միայն հրավերով'; + + @override + String get studyAllowCloning => 'Թույլատրել պատճենումը'; + + @override + String get studyNobody => 'Ոչ ոք'; + + @override + String get studyOnlyMe => 'Միայն ես'; + + @override + String get studyContributors => 'Համահեղինակներ'; + + @override + String get studyMembers => 'Անդամները'; + + @override + String get studyEveryone => 'Բոլորը'; + + @override + String get studyEnableSync => 'Միացնել սինքրոնացումը'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Այո. բոլորի համար դնել միևնույն դիրքը'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ոչ. թույլատրել մասնակիցներին ազատ ուսումնասիրել բոլոր դիրքերը'; + + @override + String get studyPinnedStudyComment => 'Ստուդիայի ամրակցված մեկնաբանություն'; + @override String get studyStart => 'Սկսել'; + + @override + String get studySave => 'Պահպանել'; + + @override + String get studyClearChat => 'Մաքրել զրուցարանը'; + + @override + String get studyDeleteTheStudyChatHistory => 'Հեռացնե՞լ ստուդիայի զրուցարանը։ Վերականգնել հնարավոր չի լինի։'; + + @override + String get studyDeleteStudy => 'Հեռացնել ստուդիան'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Հեռացնե՞լ ամբողջ ստուդիան։ Հեռացումն անդառնալի կլինի։ Հաստատելու համար մուտքագրեք ստուդիայի անվանումը՝ $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Որտե՞ղ եք ցանկանում ստեղծել ստուդիան։'; + + @override + String get studyGoodMove => 'Լավ քայլ է'; + + @override + String get studyMistake => 'Սխալ'; + + @override + String get studyBrilliantMove => 'Գերազանց քայլ է'; + + @override + String get studyBlunder => 'Վրիպում'; + + @override + String get studyInterestingMove => 'Հետաքրքիր քայլ է'; + + @override + String get studyDubiousMove => 'Կասկածելի քայլ'; + + @override + String get studyOnlyMove => 'Միակ քայլ'; + + @override + String get studyZugzwang => 'Ցուգցվանգ'; + + @override + String get studyEqualPosition => 'Հավասար դիրք'; + + @override + String get studyUnclearPosition => 'Անորոշ դիրք'; + + @override + String get studyWhiteIsSlightlyBetter => 'Սպիտակները մի քիչ լավ են'; + + @override + String get studyBlackIsSlightlyBetter => 'Սևերը մի քիչ լավ են'; + + @override + String get studyWhiteIsBetter => 'Սպիտակները լավ են'; + + @override + String get studyBlackIsBetter => 'Սևերը լավ են'; + + @override + String get studyWhiteIsWinning => 'Սպիտակները հաղթում են'; + + @override + String get studyBlackIsWinning => 'Սևերը հաղթում են'; + + @override + String get studyNovelty => 'Նորույթ'; + + @override + String get studyDevelopment => 'Զարգացում'; + + @override + String get studyInitiative => 'Նախաձեռնություն'; + + @override + String get studyAttack => 'Գրոհ'; + + @override + String get studyCounterplay => 'Հակախաղ'; + + @override + String get studyTimeTrouble => 'Ցայտնոտ'; + + @override + String get studyWithCompensation => 'Փոխհատուցմամբ'; + + @override + String get studyWithTheIdea => 'Մտահղացմամբ'; + + @override + String get studyNextChapter => 'Հաջորդ գլուխը'; + + @override + String get studyPrevChapter => 'Նախորդ գլուխը'; + + @override + String get studyStudyActions => 'Գործողությունները ստուդիայում'; + + @override + String get studyTopics => 'Թեմաներ'; + + @override + String get studyMyTopics => 'Իմ թեմաները'; + + @override + String get studyPopularTopics => 'Շատ դիտվող թեմաներ'; + + @override + String get studyManageTopics => 'Թեմաների կառավարում'; + + @override + String get studyBack => 'Հետ'; + + @override + String get studyPlayAgain => 'Կրկին խաղալ'; + + @override + String get studyWhatWouldYouPlay => 'Ինչպե՞ս կխաղայիք այս դիրքում'; + + @override + String get studyYouCompletedThisLesson => 'Շնորհավորո՜ւմ ենք։ Դուք ավարեցիք այս դասը։'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count գլուխ', + one: '$count գլուխ', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count պարտիա', + one: '$count պարտիա', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count մասնակից', + one: '$count մասնակից', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Տեղադրեք տեսքտը PGN ձևաչափով, $count պարտիայից ոչ ավելի', + one: 'Տեղադրեք տեսքտը PGN ձևաչափով, $count պարտիայից ոչ ավելի', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'հենց հիմա'; + + @override + String get timeagoRightNow => 'հենց հիմա'; + + @override + String get timeagoCompleted => 'ավարտված'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count վայրկյաններ հետո', + one: '$count վայրկյան հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count րոպեներ հետո', + one: '$count րոպե հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ժամ հետո', + one: '$count ժամ հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count օր հետո', + one: '$count օր հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count շաբաթ հետո', + one: '$count շաբաթ հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ամիս հետո', + one: '$count ամիս հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count տարի հետո', + one: '$count տարի հետո', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count րոպե առաջ', + one: '$count րոպե առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ժամ առաջ', + one: '$count ժամ առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count օր առաջ', + one: '$count օր առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count շաբաթ առաջ', + one: '$count շաբաթ առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ամիս առաջ', + one: '$count ամիս առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count տարի առաջ', + one: '$count տարի առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 45042ee484..4f0e02db18 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Semua permainan'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Apa kamu yakin?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Ulas balik'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Halo, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Halo'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Sembunyikan variasi'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Beranda'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'Oke'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Teka-teki'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Pengaturan'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Bagikan GPN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Bagikan URL permainan'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Bagikan teka-teki ini'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Tampilkan komentar'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Tampilkan hasil'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Tampilkan variasi'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Tontonan'; @override String get activityActivity => 'Aktivitas'; @@ -238,6 +235,17 @@ class AppLocalizationsId extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +339,256 @@ class AppLocalizationsId extends AppLocalizations { @override String get broadcastBroadcasts => 'Siaran'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Siaran turnamen langsung'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Siaran langsung baru'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Tambakan ronde'; + + @override + String get broadcastOngoing => 'Sedang berlangsung'; + + @override + String get broadcastUpcoming => 'Akan datang'; + + @override + String get broadcastCompleted => 'Telah selesai'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Nama ronde'; + + @override + String get broadcastRoundNumber => 'Babak ronde'; + + @override + String get broadcastTournamentName => 'Nama turnamen'; + + @override + String get broadcastTournamentDescription => 'Deskripsi singkat turnamen'; + + @override + String get broadcastFullDescription => 'Keterangan acara secara penuh'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Deskripsi panjang opsional dari siaran. $param1 tersedia. Panjangnya harus kurang dari $param2 karakter.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL yang akan di-polling oleh Lichess untuk mendapatkan pembaruan PGN. Itu harus dapat diakses publik dari Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opsional, jika Anda tahu kapan acara dimulai'; + + @override + String get broadcastCurrentGameUrl => 'Tautan permainan ini'; + + @override + String get broadcastDownloadAllRounds => 'Unduh semua ronde'; + + @override + String get broadcastResetRound => 'Atur ulang ronde ini'; + + @override + String get broadcastDeleteRound => 'Hapus ronde ini'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -590,7 +845,10 @@ class AppLocalizationsId extends AppLocalizations { String get preferencesOnlyOnInitialPosition => 'Hanya di posisi awal'; @override - String get preferencesInGameOnly => 'In-game only'; + String get preferencesInGameOnly => 'Hanya di dalam permainan'; + + @override + String get preferencesExceptInGame => 'Except in-game'; @override String get preferencesChessClock => 'Jam catur'; @@ -733,6 +991,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Suara pemberitahuan'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Teka-teki'; @@ -1368,10 +1629,10 @@ class AppLocalizationsId extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Musuh dibatasi gerakan yang dapat mereka lakukan, dan semua gerakan memperburuk posisi mereka.'; @override - String get puzzleThemeHealthyMix => 'Campuran baik'; + String get puzzleThemeMix => 'Campuran baik'; @override - String get puzzleThemeHealthyMixDescription => 'Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.'; + String get puzzleThemeMixDescription => 'Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.'; @override String get puzzleThemePlayerGames => 'Permainan pemain'; @@ -1510,7 +1771,7 @@ class AppLocalizationsId extends AppLocalizations { String get variantEnding => 'Akhir sesuai aturan variasi'; @override - String get newOpponent => 'Penantang baru'; + String get newOpponent => 'Permainan yang baru'; @override String get yourOpponentWantsToPlayANewGameWithYou => 'Lawan Anda ingin bermain lagi dengan Anda'; @@ -1745,9 +2006,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get byCPL => 'Secara CPL'; - @override - String get openStudy => 'Buka studi'; - @override String get enable => 'Aktifkan'; @@ -1775,9 +2033,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get removesTheDepthLimit => 'Menghapus batas kedalaman, dan membuat komputer Anda hangat'; - @override - String get engineManager => 'Pengaturan komputer'; - @override String get blunder => 'Blunder'; @@ -1856,7 +2111,7 @@ class AppLocalizationsId extends AppLocalizations { String get friends => 'Teman'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'pemain lainnya'; @override String get discussions => 'Diskusi'; @@ -2041,6 +2296,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get gamesPlayed => 'Permainan yang telah dimainkan'; + @override + String get ok => 'OK'; + @override String get cancel => 'Batal'; @@ -2415,9 +2673,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get unblock => 'Buka blokir'; - @override - String get followsYou => 'Mengikuti anda'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 mulai mengikuti $param2'; @@ -2609,7 +2864,7 @@ class AppLocalizationsId extends AppLocalizations { String get editProfile => 'Ubah profil'; @override - String get realName => 'Real name'; + String get realName => 'Nama asli'; @override String get setFlair => 'Sunting flair anda'; @@ -2690,7 +2945,7 @@ class AppLocalizationsId extends AppLocalizations { String get yes => 'Ya'; @override - String get website => 'Website'; + String get website => 'Situs Web'; @override String get mobile => 'Mobile'; @@ -2750,7 +3005,13 @@ class AppLocalizationsId extends AppLocalizations { String get other => 'Lainnya'; @override - String get reportDescriptionHelp => 'Paste link berikut ke dalam permainan dan jelaskan apa masalah tentang pengguna ini.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Harap berikan setidaknya satu tautan ke permainan yang curang.'; @@ -3449,22 +3710,22 @@ class AppLocalizationsId extends AppLocalizations { String get backgroundImageUrl => 'URL gambar latar belakang:'; @override - String get board => 'Board'; + String get board => 'Papan'; @override - String get size => 'Size'; + String get size => 'Ukuran'; @override - String get opacity => 'Opacity'; + String get opacity => 'Transparansi'; @override - String get brightness => 'Brightness'; + String get brightness => 'Kecerahan'; @override - String get hue => 'Hue'; + String get hue => 'Rona'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Kembalikan warna ke pengaturan semula'; @override String get pieceSet => 'Susunan buah catur'; @@ -3656,10 +3917,10 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; + String get showUnreadLichessMessage => 'Anda telah menerima pesan pribadi dari Lichess.'; @override - String get clickHereToReadIt => 'Click here to read it'; + String get clickHereToReadIt => 'Klik di sini untuk membaca'; @override String get sorry => 'Maaf :('; @@ -3727,7 +3988,7 @@ class AppLocalizationsId extends AppLocalizations { String get edit => 'Ubah'; @override - String get bullet => 'Bullet'; + String get bullet => 'Peluru'; @override String get blitz => 'Blitz'; @@ -4044,16 +4305,19 @@ class AppLocalizationsId extends AppLocalizations { String get ourEventTips => 'Tips dari kami terkait penyelenggaraan acara'; @override - String get instructions => 'Instructions'; + String get instructions => 'Instruksi'; @override - String get showMeEverything => 'Show me everything'; + String get showMeEverything => 'Tunjukkan semuanya'; @override String get lichessPatronInfo => 'Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas.\nSemua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Tidak ada yang bisa dilihat untuk saat ini.'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4658,8 +4922,673 @@ class AppLocalizationsId extends AppLocalizations { String get streamerLichessStreamers => 'Streamer Lichess'; @override - String get studyShareAndExport => 'Bagikan & ekspor'; + String get studyPrivate => 'Pribadi'; @override - String get studyStart => 'Mulai'; + String get studyMyStudies => 'Studi saya'; + + @override + String get studyStudiesIContributeTo => 'Studi yang saya ikut berkontribusi'; + + @override + String get studyMyPublicStudies => 'Studi publik saya'; + + @override + String get studyMyPrivateStudies => 'Studi pribadi saya'; + + @override + String get studyMyFavoriteStudies => 'Studi favorit saya'; + + @override + String get studyWhatAreStudies => 'Apa itu studi?'; + + @override + String get studyAllStudies => 'Semua studi'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studi dibuat oleh $param'; + } + + @override + String get studyNoneYet => 'Tidak ada.'; + + @override + String get studyHot => 'Terhangat'; + + @override + String get studyDateAddedNewest => 'Tanggal ditambahkan (terbaru)'; + + @override + String get studyDateAddedOldest => 'Tanggal ditambahkan (terlama)'; + + @override + String get studyRecentlyUpdated => 'Baru saja diperbarui'; + + @override + String get studyMostPopular => 'Paling populer'; + + @override + String get studyAlphabetical => 'Menurut abjad'; + + @override + String get studyAddNewChapter => 'Tambahkan bab baru'; + + @override + String get studyAddMembers => 'Tambahkan anggota'; + + @override + String get studyInviteToTheStudy => 'Ajak untuk studi'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Harap hanya mengundang orang yang Anda kenal, dan yang secara aktif ingin bergabung dengan studi ini.'; + + @override + String get studySearchByUsername => 'Cari berdasarkan nama pengguna'; + + @override + String get studySpectator => 'Penonton'; + + @override + String get studyContributor => 'Kontributor'; + + @override + String get studyKick => 'Diusir'; + + @override + String get studyLeaveTheStudy => 'Tinggalkan studi'; + + @override + String get studyYouAreNowAContributor => 'Sekarang Anda menjadi kontributor'; + + @override + String get studyYouAreNowASpectator => 'Sekarang Anda adalah penonton'; + + @override + String get studyPgnTags => 'Tagar PGN'; + + @override + String get studyLike => 'Suka'; + + @override + String get studyUnlike => 'Batal Suka'; + + @override + String get studyNewTag => 'Tagar baru'; + + @override + String get studyCommentThisPosition => 'Komentar di posisi ini'; + + @override + String get studyCommentThisMove => 'Komentari langkah ini'; + + @override + String get studyAnnotateWithGlyphs => 'Anotasikan dengan glif'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Bab ini terlalu pendek untuk di analisa.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Hanya kontributor yang dapat meminta analisa komputer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dapatkan analisis komputer penuh di pihak server dari jalur utama.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Pastikan bab ini selesai. Anda hanya dapat meminta analisis satu kali.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Semua anggota yang ter-sinkron tetap pada posisi yang sama'; + + @override + String get studyShareChanges => 'Bagikan perubahan dengan penonton dan simpan di server'; + + @override + String get studyPlaying => 'Memainkan'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Pertama'; + + @override + String get studyPrevious => 'Sebelumnya'; + + @override + String get studyNext => 'Berikutnya'; + + @override + String get studyLast => 'Terakhir'; + + @override + String get studyShareAndExport => 'Bagikan & ekspor'; + + @override + String get studyCloneStudy => 'Gandakan'; + + @override + String get studyStudyPgn => 'Studi PGN'; + + @override + String get studyDownloadAllGames => 'Unduh semua permainan'; + + @override + String get studyChapterPgn => 'Bab PGN'; + + @override + String get studyCopyChapterPgn => 'Salin PGN'; + + @override + String get studyDownloadGame => 'Unduh permainan'; + + @override + String get studyStudyUrl => 'URL studi'; + + @override + String get studyCurrentChapterUrl => 'URL Bab saat ini'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Anda dapat menempelkan ini di forum untuk disematkan'; + + @override + String get studyStartAtInitialPosition => 'Mulai saat posisi awal'; + + @override + String studyStartAtX(String param) { + return 'Mulai dari $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Sematkan di blog atau website Anda'; + + @override + String get studyReadMoreAboutEmbedding => 'Baca lebih tentang penyematan'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Hanya pelajaran publik yang dapat di sematkan!'; + + @override + String get studyOpen => 'Buka'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 dibawakan kepadamu dari $param2'; + } + + @override + String get studyStudyNotFound => 'Studi tidak ditemukan'; + + @override + String get studyEditChapter => 'Ubah bab'; + + @override + String get studyNewChapter => 'Bab baru'; + + @override + String studyImportFromChapterX(String param) { + return 'Impor dari $param'; + } + + @override + String get studyOrientation => 'Orientasi'; + + @override + String get studyAnalysisMode => 'Mode analisa'; + + @override + String get studyPinnedChapterComment => 'Sematkan komentar bagian bab'; + + @override + String get studySaveChapter => 'Simpan bab'; + + @override + String get studyClearAnnotations => 'Hapus anotasi'; + + @override + String get studyClearVariations => 'Hapus variasi'; + + @override + String get studyDeleteChapter => 'Hapus bab'; + + @override + String get studyDeleteThisChapter => 'Hapus bab ini? Ini tidak akan dapat mengulangkan kembali!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Hapus semua komentar dan bentuk di bab ini?'; + + @override + String get studyRightUnderTheBoard => 'Kanan dibawah papan'; + + @override + String get studyNoPinnedComment => 'Tidak ada'; + + @override + String get studyNormalAnalysis => 'Analisa biasa'; + + @override + String get studyHideNextMoves => 'Sembunyikan langkah selanjutnya'; + + @override + String get studyInteractiveLesson => 'Pelajaran interaktif'; + + @override + String studyChapterX(String param) { + return 'Bab $param'; + } + + @override + String get studyEmpty => 'Kosong'; + + @override + String get studyStartFromInitialPosition => 'Mulai dari posisi awal'; + + @override + String get studyEditor => 'Penyunting'; + + @override + String get studyStartFromCustomPosition => 'Mulai dari posisi yang disesuaikan'; + + @override + String get studyLoadAGameByUrl => 'Muat permainan dari URL'; + + @override + String get studyLoadAPositionFromFen => 'Muat posisi dari FEN'; + + @override + String get studyLoadAGameFromPgn => 'Muat permainan dari PGN'; + + @override + String get studyAutomatic => 'Otomatis'; + + @override + String get studyUrlOfTheGame => 'URL permainan'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Muat permainan dari $param1 atau $param2'; + } + + @override + String get studyCreateChapter => 'Buat bab'; + + @override + String get studyCreateStudy => 'Buat studi'; + + @override + String get studyEditStudy => 'Ubah studi'; + + @override + String get studyVisibility => 'Visibilitas'; + + @override + String get studyPublic => 'Publik'; + + @override + String get studyUnlisted => 'Tidak terdaftar'; + + @override + String get studyInviteOnly => 'Hanya yang diundang'; + + @override + String get studyAllowCloning => 'Perbolehkan kloning'; + + @override + String get studyNobody => 'Tidak ada seorangpun'; + + @override + String get studyOnlyMe => 'Hanya saya'; + + @override + String get studyContributors => 'Kontributor'; + + @override + String get studyMembers => 'Anggota'; + + @override + String get studyEveryone => 'Semua orang'; + + @override + String get studyEnableSync => 'Aktifkan sinkronisasi'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ya: atur semua orang dalam posisi yang sama'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Tidak: Bolehkan untuk menjelajah dengan bebas'; + + @override + String get studyPinnedStudyComment => 'Sematkan komentar studi'; + + @override + String get studyStart => 'Mulai'; + + @override + String get studySave => 'Simpan'; + + @override + String get studyClearChat => 'Bersihkan obrolan'; + + @override + String get studyDeleteTheStudyChatHistory => 'Hapus riwayat obrolan studi? Ini tidak akan dapat mengulangkan kembali!'; + + @override + String get studyDeleteStudy => 'Hapus studi'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Hapus seluruh studi? Tidak dapat kembal lagi! Tuliskan nama studi untuk konfirmasi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Dimana Anda ingin mempelajarinya?'; + + @override + String get studyGoodMove => 'Langkah bagus'; + + @override + String get studyMistake => 'Kesalahan'; + + @override + String get studyBrilliantMove => 'Langkah Brilian'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Langkah menarik'; + + @override + String get studyDubiousMove => 'Langkah meragukan'; + + @override + String get studyOnlyMove => 'Langkah satu-satunya'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posisi imbang'; + + @override + String get studyUnclearPosition => 'Posisi tidak jelas'; + + @override + String get studyWhiteIsSlightlyBetter => 'Putih sedikit lebih unggul'; + + @override + String get studyBlackIsSlightlyBetter => 'Hitam sedikit lebih unggul'; + + @override + String get studyWhiteIsBetter => 'Putih lebih unggul'; + + @override + String get studyBlackIsBetter => 'Hitam lebih unggul'; + + @override + String get studyWhiteIsWinning => 'Putih menang telak'; + + @override + String get studyBlackIsWinning => 'Hitam menang telak'; + + @override + String get studyNovelty => 'Langkah baru'; + + @override + String get studyDevelopment => 'Pengembangan'; + + @override + String get studyInitiative => 'Inisiatif'; + + @override + String get studyAttack => 'Serangan'; + + @override + String get studyCounterplay => 'Serangan balik'; + + @override + String get studyTimeTrouble => 'Tekanan waktu'; + + @override + String get studyWithCompensation => 'Dengan kompensasi'; + + @override + String get studyWithTheIdea => 'Dengan ide'; + + @override + String get studyNextChapter => 'Bab selanjutnya'; + + @override + String get studyPrevChapter => 'Bab sebelumnya'; + + @override + String get studyStudyActions => 'Pembelajaran'; + + @override + String get studyTopics => 'Topik'; + + @override + String get studyMyTopics => 'Topik saya'; + + @override + String get studyPopularTopics => 'Topik populer'; + + @override + String get studyManageTopics => 'Kelola topik'; + + @override + String get studyBack => 'Kembali'; + + @override + String get studyPlayAgain => 'Main lagi'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Selamat. Anda telah menyelesaikan pelajaran ini.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Bab', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Permainan', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Anggota', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Tempelkan PGN kamu disini, lebih dari $count permainan', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'baru saja'; + + @override + String get timeagoRightNow => 'sekarang'; + + @override + String get timeagoCompleted => 'telah selesai'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count detik', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count menit', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count jam', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count hari', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count minggu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count bulan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count tahun', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count menit yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jam yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hari yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minggu yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bulan yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tahun yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count menit tersisa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jam tersisa', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index 84551045ec..62e049c87b 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsIt extends AppLocalizations { AppLocalizationsIt([String locale = 'it']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Tutte le partite'; @override - String get mobilePuzzlesTab => 'Tattiche'; + String get mobileAreYouSure => 'Sei sicuro?'; @override - String get mobileToolsTab => 'Strumenti'; + String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; @override - String get mobileWatchTab => 'Guarda'; + String get mobileClearButton => 'Elimina'; @override - String get mobileSettingsTab => 'Settaggi'; + String get mobileCorrespondenceClearSavedMove => 'Cancella mossa salvata'; @override - String get mobileMustBeLoggedIn => 'Devi aver effettuato l\'accesso per visualizzare questa pagina.'; + String get mobileCustomGameJoinAGame => 'Unisciti a una partita'; @override - String get mobileSystemColors => 'Tema app'; + String get mobileFeedbackButton => 'Suggerimenti'; @override - String get mobileFeedbackButton => 'Suggerimenti'; + String mobileGreeting(String param) { + return 'Ciao, $param'; + } @override - String get mobileOkButton => 'Ok'; + String get mobileGreetingWithoutName => 'Ciao'; @override - String get mobileSettingsHapticFeedback => 'Feedback tattile'; + String get mobileHideVariation => 'Nascondi variante'; @override - String get mobileSettingsImmersiveMode => 'Modalità immersiva'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.'; + String get mobileLiveStreamers => 'Streamer in diretta'; @override - String get mobileNotFollowingAnyUser => 'Non stai seguendo nessun utente.'; + String get mobileMustBeLoggedIn => 'Devi aver effettuato l\'accesso per visualizzare questa pagina.'; @override - String get mobileAllGames => 'Tutte le partite'; + String get mobileNoSearchResults => 'Nessun risultato'; @override - String get mobileRecentSearches => 'Ricerche recenti'; + String get mobileNotFollowingAnyUser => 'Non stai seguendo nessun utente.'; @override - String get mobileClearButton => 'Elimina'; + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nessun risultato'; + String get mobilePrefMagnifyDraggedPiece => 'Ingrandisci il pezzo trascinato'; @override - String get mobileAreYouSure => 'Sei sicuro?'; + String get mobilePuzzleStormConfirmEndRun => 'Vuoi terminare questa serie?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderai la tua serie corrente e il tuo punteggio verrà salvato.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nessun risultato, per favore modifica i filtri'; @override String get mobilePuzzleStormNothingToShow => 'Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Condividi questa tattica'; + String get mobilePuzzleStormSubtitle => 'Risolvi il maggior numero di puzzle in tre minuti.'; @override - String get mobileShareGameURL => 'Condividi URL della partita'; + String get mobilePuzzleStreakAbortWarning => 'Perderai la tua serie corrente e il tuo punteggio verrà salvato.'; @override - String get mobileShareGamePGN => 'Condividi PGN'; + String get mobilePuzzleThemesSubtitle => '.'; @override - String get mobileSharePositionAsFEN => 'Condividi posizione come FEN'; + String get mobilePuzzlesTab => 'Tattiche'; @override - String get mobileShowVariations => 'Mostra varianti'; + String get mobileRecentSearches => 'Ricerche recenti'; @override - String get mobileHideVariation => 'Nascondi variante'; + String get mobileSettingsHapticFeedback => 'Feedback tattile'; @override - String get mobileShowComments => 'Mostra commenti'; + String get mobileSettingsImmersiveMode => 'Modalità immersiva'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vuoi terminare questa serie?'; + String get mobileSettingsImmersiveModeSubtitle => 'Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nessun risultato, per favore modifica i filtri'; + String get mobileSettingsTab => 'Settaggi'; @override - String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; + String get mobileShareGamePGN => 'Condividi PGN'; @override - String get mobileCancelDrawOffer => 'Annulla richiesta di patta'; + String get mobileShareGameURL => 'Condividi URL della partita'; @override - String get mobileWaitingForOpponentToJoin => 'In attesa dell\'avversario...'; + String get mobileSharePositionAsFEN => 'Condividi posizione come FEN'; @override - String get mobileBlindfoldMode => 'Alla cieca'; + String get mobileSharePuzzle => 'Condividi questa tattica'; @override - String get mobileLiveStreamers => 'Streamer in diretta'; + String get mobileShowComments => 'Mostra commenti'; @override - String get mobileCustomGameJoinAGame => 'Unisciti a una partita'; + String get mobileShowResult => 'Mostra il risultato'; @override - String get mobileCorrespondenceClearSavedMove => 'Cancella mossa salvata'; + String get mobileShowVariations => 'Mostra varianti'; @override String get mobileSomethingWentWrong => 'Si è verificato un errore.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Tema app'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Strumenti'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'In attesa dell\'avversario...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Guarda'; @override String get activityActivity => 'Attività'; @@ -246,6 +243,17 @@ class AppLocalizationsIt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Partite $count $param2 per corrispondenza completate', + one: 'Partita $count $param2 per corrispondenza completata', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsIt extends AppLocalizations { @override String get broadcastBroadcasts => 'Dirette'; + @override + String get broadcastMyBroadcasts => 'Le mie trasmissioni'; + @override String get broadcastLiveBroadcasts => 'Tornei in diretta'; + @override + String get broadcastBroadcastCalendar => 'Calendario trasmissioni'; + + @override + String get broadcastNewBroadcast => 'Nuova diretta'; + + @override + String get broadcastSubscribedBroadcasts => 'Trasmissioni abbonate'; + + @override + String get broadcastAboutBroadcasts => 'Informazioni sulle trasmissioni'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Istruzioni delle trasmissioni Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Il nuovo turno avrà gli stessi membri e contributori del precedente.'; + + @override + String get broadcastAddRound => 'Aggiungi un turno'; + + @override + String get broadcastOngoing => 'In corso'; + + @override + String get broadcastUpcoming => 'Prossimamente'; + + @override + String get broadcastCompleted => 'Conclusa'; + + @override + String get broadcastCompletedHelp => 'Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine.'; + + @override + String get broadcastRoundName => 'Nome turno'; + + @override + String get broadcastRoundNumber => 'Turno numero'; + + @override + String get broadcastTournamentName => 'Nome del torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descrizione dell\'evento'; + + @override + String get broadcastFullDescription => 'Descrizione completa dell\'evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '(Facoltativo) Descrizione completa dell\'evento. $param1 è disponibile. La lunghezza deve essere inferiore a $param2 caratteri.'; + } + + @override + String get broadcastSourceSingleUrl => 'Sorgente URL PGN'; + + @override + String get broadcastSourceUrlHelp => 'L\'URL che Lichess utilizzerà per ottenere gli aggiornamenti dei PGN. Deve essere accessibile pubblicamente su Internet.'; + + @override + String get broadcastSourceGameIds => 'Fino a 64 ID di partite Lichess, separati da spazi.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data d\'inizio nel fuso orario locale del torneo: $param'; + } + + @override + String get broadcastStartDateHelp => 'Facoltativo, se sai quando inizia l\'evento'; + + @override + String get broadcastCurrentGameUrl => 'URL della partita corrente'; + + @override + String get broadcastDownloadAllRounds => 'Scarica tutti i round'; + + @override + String get broadcastResetRound => 'Reimposta questo turno'; + + @override + String get broadcastDeleteRound => 'Elimina questo turno'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Elimina definitivamente il turno e le sue partite.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Elimina tutte le partite di questo turno. L\'emittente dovrà essere attiva per poterli ricreare.'; + + @override + String get broadcastEditRoundStudy => 'Modifica lo studio del turno'; + + @override + String get broadcastDeleteTournament => 'Elimina questo torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina definitivamente l\'intero torneo, tutti i turni e tutte le partite.'; + + @override + String get broadcastShowScores => 'Mostra i punteggi dei giocatori in base ai risultati del gioco'; + + @override + String get broadcastReplacePlayerTags => 'Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli'; + + @override + String get broadcastFideFederations => 'Federazioni FIDE'; + + @override + String get broadcastTop10Rating => 'Migliori 10 punteggi'; + + @override + String get broadcastFidePlayers => 'Giocatori FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Giocatore FIDE non trovato'; + + @override + String get broadcastFideProfile => 'Profilo FIDE'; + + @override + String get broadcastFederation => 'Federazione'; + + @override + String get broadcastAgeThisYear => 'Età quest\'anno'; + + @override + String get broadcastUnrated => 'Non classificato'; + + @override + String get broadcastRecentTournaments => 'Tornei recenti'; + + @override + String get broadcastOpenLichess => 'Apri con Lichess'; + + @override + String get broadcastTeams => 'Squadre'; + + @override + String get broadcastBoards => 'Scacchiere'; + + @override + String get broadcastOverview => 'Panoramica'; + + @override + String get broadcastSubscribeTitle => 'Iscriviti per ricevere notifiche sull\'inizio di ogni round. Puoi attivare o disattivare la campanella o le notifiche push per le dirette nelle preferenze del tuo account.'; + + @override + String get broadcastUploadImage => 'Carica immagine del torneo'; + + @override + String get broadcastNoBoardsYet => 'Non sono ancora presenti scacchiere. Esse compariranno non appena i giochi saranno stati caricati.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Le scacchiere possono essere caricate con una sorgente o tramite $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Inizia tra $param'; + } + + @override + String get broadcastStartVerySoon => 'Questa trasmissione inizierà a breve.'; + + @override + String get broadcastNotYetStarted => 'Questa trasmissione non è ancora cominciata.'; + + @override + String get broadcastOfficialWebsite => 'Sito web ufficiale'; + + @override + String get broadcastStandings => 'Classifica'; + + @override + String get broadcastOfficialStandings => 'Classifica Ufficiale'; + + @override + String broadcastIframeHelp(String param) { + return 'Altre opzioni si trovano nella $param'; + } + + @override + String get broadcastWebmastersPage => 'pagina dei gestori web'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Una sorgente PGN pubblica per questo round. Viene offerta anche un\'$param per una sincronizzazione più rapida ed efficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incorpora questa trasmissione nel tuo sito web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incorpora $param nel tuo sito web'; + } + + @override + String get broadcastRatingDiff => 'Differenza di punteggio'; + + @override + String get broadcastGamesThisTournament => 'Partite in questo torneo'; + + @override + String get broadcastScore => 'Punteggio'; + + @override + String get broadcastAllTeams => 'Tutte le squadre'; + + @override + String get broadcastTournamentFormat => 'Formato del torneo'; + + @override + String get broadcastTournamentLocation => 'Luogo del Torneo'; + + @override + String get broadcastTopPlayers => 'Giocatori migliori'; + + @override + String get broadcastTimezone => 'Fuso orario'; + + @override + String get broadcastFideRatingCategory => 'Categoria di punteggio FIDE'; + + @override + String get broadcastOptionalDetails => 'Dettagli facoltativi'; + + @override + String get broadcastPastBroadcasts => 'Trasmissioni precedenti'; + + @override + String get broadcastAllBroadcastsByMonth => 'Visualizza tutte le trasmissioni per mese'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dirette', + one: '$count diretta', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Sfide: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get preferencesInGameOnly => 'Solamente durante la partita'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Orologio'; @@ -750,6 +1008,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Tono notifica'; + @override + String get preferencesBlindfold => 'Alla cieca'; + @override String get puzzlePuzzles => 'Problemi'; @@ -1390,10 +1651,10 @@ class AppLocalizationsIt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'L\'avversario è limitato nella sua scelta della mossa, e tutte le mosse possibili peggiorano la sua posizione.'; @override - String get puzzleThemeHealthyMix => 'Mix generale'; + String get puzzleThemeMix => 'Mix generale'; @override - String get puzzleThemeHealthyMixDescription => 'Un po\' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.'; + String get puzzleThemeMixDescription => 'Un po\' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.'; @override String get puzzleThemePlayerGames => 'Partite tra giocatori'; @@ -1767,9 +2028,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get byCPL => 'Per CPL'; - @override - String get openStudy => 'Apri studio'; - @override String get enable => 'Abilita'; @@ -1797,9 +2055,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get removesTheDepthLimit => 'Rimuove il limite di profondità di analisi, ma può surriscaldare il tuo computer'; - @override - String get engineManager => 'Gestore del motore'; - @override String get blunder => 'Errore grave'; @@ -1878,7 +2133,7 @@ class AppLocalizationsIt extends AppLocalizations { String get friends => 'Amici'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'altri giocatori'; @override String get discussions => 'Conversazioni'; @@ -2063,6 +2318,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get gamesPlayed => 'Partite giocate'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annulla'; @@ -2437,9 +2695,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get unblock => 'Sblocca'; - @override - String get followsYou => 'Ti segue'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ha iniziato a seguire $param2'; @@ -2712,10 +2967,10 @@ class AppLocalizationsIt extends AppLocalizations { String get yes => 'Sì'; @override - String get website => 'Website'; + String get website => 'Sito'; @override - String get mobile => 'Mobile'; + String get mobile => 'Cellulare'; @override String get help => 'Aiuto:'; @@ -2772,7 +3027,13 @@ class AppLocalizationsIt extends AppLocalizations { String get other => 'Altro'; @override - String get reportDescriptionHelp => 'Incolla il link della partita/e e spiega cosa non va con questo giocatore. Non dire soltanto \"ha imbrogliato\", ma specifica come sei arrivato a questa conclusione. Il tuo report verrà processato più velocemente se scritto in lingua inglese.'; + String get reportCheatBoostHelp => 'Incolla il link della partita(o partite) e spiega cosa non va sul comportamento di questo utente. Non dire solamente \"ha barato\", ma invece dici come sei arrivato a questa conclusione.'; + + @override + String get reportUsernameHelp => 'Spiegaci cosa vi è di offensivo in questo nome utente. Non dire solamente \"è offensivo/inappopriato\", ma invece dici come sei arrivato a questa conclusione, soprattutto se l\'insulto è offuscato, non in inglese, in linguaggio giovanile, oppure se è un riferimento storico/culturale.'; + + @override + String get reportProcessedFasterInEnglish => 'La tua segnalazione sarà processata più velocemente se scritta in Inglese.'; @override String get error_provideOneCheatedGameLink => 'Si prega di fornire almeno un collegamento link di una partita in cui il giocatore ha imbrogliato.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get nothingToSeeHere => 'Niente da vedere qui al momento.'; + @override + String get stats => 'Statistiche'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsIt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamer'; + @override + String get studyPrivate => 'Privato'; + + @override + String get studyMyStudies => 'I miei studi'; + + @override + String get studyStudiesIContributeTo => 'Studi a cui collaboro'; + + @override + String get studyMyPublicStudies => 'I miei studi pubblici'; + + @override + String get studyMyPrivateStudies => 'I miei studi privati'; + + @override + String get studyMyFavoriteStudies => 'I miei studi preferiti'; + + @override + String get studyWhatAreStudies => 'Cosa sono gli \"studi\"?'; + + @override + String get studyAllStudies => 'Tutti gli studi'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studi creati da $param'; + } + + @override + String get studyNoneYet => 'Vuoto.'; + + @override + String get studyHot => 'Hot'; + + @override + String get studyDateAddedNewest => 'Data di pubblicazione (dalla più recente)'; + + @override + String get studyDateAddedOldest => 'Data di pubblicazione (dalla meno recente)'; + + @override + String get studyRecentlyUpdated => 'Data di aggiornamento (dalla più recente)'; + + @override + String get studyMostPopular => 'Più popolari'; + + @override + String get studyAlphabetical => 'Alfabetico'; + + @override + String get studyAddNewChapter => 'Aggiungi un nuovo capitolo'; + + @override + String get studyAddMembers => 'Aggiungi membri'; + + @override + String get studyInviteToTheStudy => 'Invita allo studio'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Invita solo persone che conosci e che desiderano partecipare attivamente a questo studio.'; + + @override + String get studySearchByUsername => 'Cerca per nome utente'; + + @override + String get studySpectator => 'Spettatore'; + + @override + String get studyContributor => 'Partecipante'; + + @override + String get studyKick => 'Espelli'; + + @override + String get studyLeaveTheStudy => 'Abbandona lo studio'; + + @override + String get studyYouAreNowAContributor => 'Ora sei un partecipante'; + + @override + String get studyYouAreNowASpectator => 'Ora sei uno spettatore'; + + @override + String get studyPgnTags => 'Tag PGN'; + + @override + String get studyLike => 'Mi piace'; + + @override + String get studyUnlike => 'Non mi Piace'; + + @override + String get studyNewTag => 'Nuovo tag'; + + @override + String get studyCommentThisPosition => 'Commenta questa posizione'; + + @override + String get studyCommentThisMove => 'Commenta questa mossa'; + + @override + String get studyAnnotateWithGlyphs => 'Commenta con segni convenzionali'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Il capitolo è troppo breve per essere analizzato.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Solo i partecipanti allo studio possono richiedere un\'analisi del computer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Richiedi un\'analisi completa del computer della variante principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Assicurati che il capitolo sia completo. Puoi richiedere l\'analisi solo una volta.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tutti i membri in SYNC rimangono sulla stessa posizione'; + + @override + String get studyShareChanges => 'Condividi le modifiche con gli spettatori e salvale sul server'; + + @override + String get studyPlaying => 'In corso'; + + @override + String get studyShowEvalBar => 'Barre di valutazione'; + + @override + String get studyFirst => 'Primo'; + + @override + String get studyPrevious => 'Precedente'; + + @override + String get studyNext => 'Successivo'; + + @override + String get studyLast => 'Ultimo'; + @override String get studyShareAndExport => 'Condividi & esporta'; + @override + String get studyCloneStudy => 'Duplica'; + + @override + String get studyStudyPgn => 'PGN dello studio'; + + @override + String get studyDownloadAllGames => 'Scarica tutte le partite'; + + @override + String get studyChapterPgn => 'PGN del capitolo'; + + @override + String get studyCopyChapterPgn => 'Copia in PGN'; + + @override + String get studyDownloadGame => 'Scarica partita'; + + @override + String get studyStudyUrl => 'URL dello studio'; + + @override + String get studyCurrentChapterUrl => 'URL del capitolo corrente'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Puoi incollare questo URL nel forum per creare un rimando'; + + @override + String get studyStartAtInitialPosition => 'Inizia dalla prima mossa'; + + @override + String studyStartAtX(String param) { + return 'Inizia a: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Incorpora nel tuo sito Web o Blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Per saperne di più su come incorporare'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Solo gli studi pubblici possono essere incorporati!'; + + @override + String get studyOpen => 'Apri'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 fornito da $param2'; + } + + @override + String get studyStudyNotFound => 'Studio non trovato'; + + @override + String get studyEditChapter => 'Modifica il capitolo'; + + @override + String get studyNewChapter => 'Nuovo capitolo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importa da $param'; + } + + @override + String get studyOrientation => 'Orientamento'; + + @override + String get studyAnalysisMode => 'Modalità analisi'; + + @override + String get studyPinnedChapterComment => 'Commento del capitolo'; + + @override + String get studySaveChapter => 'Salva capitolo'; + + @override + String get studyClearAnnotations => 'Cancella annotazioni'; + + @override + String get studyClearVariations => 'Elimina le varianti'; + + @override + String get studyDeleteChapter => 'Elimina capitolo'; + + @override + String get studyDeleteThisChapter => 'Vuoi davvero eliminare questo capitolo? Sarà perso per sempre!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Cancellare tutti i commenti, le annotazioni e i disegni in questo capitolo?'; + + @override + String get studyRightUnderTheBoard => 'Sotto la scacchiera'; + + @override + String get studyNoPinnedComment => 'Nessun commento'; + + @override + String get studyNormalAnalysis => 'Analisi normale'; + + @override + String get studyHideNextMoves => 'Nascondi le mosse successive'; + + @override + String get studyInteractiveLesson => 'Lezione interattiva'; + + @override + String studyChapterX(String param) { + return 'Capitolo $param'; + } + + @override + String get studyEmpty => 'Semplice'; + + @override + String get studyStartFromInitialPosition => 'Parti dalla posizione iniziale'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Inizia da una posizione personalizzata'; + + @override + String get studyLoadAGameByUrl => 'Carica una partita da URL'; + + @override + String get studyLoadAPositionFromFen => 'Carica una posizione da FEN'; + + @override + String get studyLoadAGameFromPgn => 'Carica una partita da PGN'; + + @override + String get studyAutomatic => 'Automatica'; + + @override + String get studyUrlOfTheGame => 'URL della partita'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carica una partita da $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crea capitolo'; + + @override + String get studyCreateStudy => 'Crea studio'; + + @override + String get studyEditStudy => 'Modifica studio'; + + @override + String get studyVisibility => 'Visibilità'; + + @override + String get studyPublic => 'Pubblico'; + + @override + String get studyUnlisted => 'Non elencato'; + + @override + String get studyInviteOnly => 'Solo su invito'; + + @override + String get studyAllowCloning => 'Permetti la clonazione'; + + @override + String get studyNobody => 'Nessuno'; + + @override + String get studyOnlyMe => 'Solo io'; + + @override + String get studyContributors => 'Collaboratori'; + + @override + String get studyMembers => 'Membri'; + + @override + String get studyEveryone => 'Tutti'; + + @override + String get studyEnableSync => 'Abilita sincronizzazione'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sì: tutti vedranno la stessa posizione'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: ognuno potrà scorrere i capitoli indipendentemente'; + + @override + String get studyPinnedStudyComment => 'Commento dello studio'; + @override String get studyStart => 'Inizia'; + + @override + String get studySave => 'Salva'; + + @override + String get studyClearChat => 'Cancella chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vuoi davvero eliminare la cronologia della chat? Sarà persa per sempre!'; + + @override + String get studyDeleteStudy => 'Elimina studio'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Eliminare l\'intero studio? Non sarà possibile annullare l\'operazione! Digitare il nome dello studio per confermare: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Dove vuoi creare lo studio?'; + + @override + String get studyGoodMove => 'Bella mossa'; + + @override + String get studyMistake => 'Errore'; + + @override + String get studyBrilliantMove => 'Mossa geniale'; + + @override + String get studyBlunder => 'Errore grave'; + + @override + String get studyInterestingMove => 'Mossa interessante'; + + @override + String get studyDubiousMove => 'Mossa dubbia'; + + @override + String get studyOnlyMove => 'Unica mossa'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posizione equivalente'; + + @override + String get studyUnclearPosition => 'Posizione non chiara'; + + @override + String get studyWhiteIsSlightlyBetter => 'Il bianco è in lieve vantaggio'; + + @override + String get studyBlackIsSlightlyBetter => 'Il nero è in lieve vantaggio'; + + @override + String get studyWhiteIsBetter => 'Il bianco è in vantaggio'; + + @override + String get studyBlackIsBetter => 'Il nero è in vantaggio'; + + @override + String get studyWhiteIsWinning => 'Il bianco sta vincendo'; + + @override + String get studyBlackIsWinning => 'Il nero sta vincendo'; + + @override + String get studyNovelty => 'Novità'; + + @override + String get studyDevelopment => 'Sviluppo'; + + @override + String get studyInitiative => 'Iniziativa'; + + @override + String get studyAttack => 'Attacco'; + + @override + String get studyCounterplay => 'Contrattacco'; + + @override + String get studyTimeTrouble => 'Prolemi di tempo'; + + @override + String get studyWithCompensation => 'Con compenso'; + + @override + String get studyWithTheIdea => 'Con l\'idea'; + + @override + String get studyNextChapter => 'Prossimo capitolo'; + + @override + String get studyPrevChapter => 'Capitolo precedente'; + + @override + String get studyStudyActions => 'Studia azioni'; + + @override + String get studyTopics => 'Discussioni'; + + @override + String get studyMyTopics => 'Le mie discussioni'; + + @override + String get studyPopularTopics => 'Argomenti popolari'; + + @override + String get studyManageTopics => 'Gestisci discussioni'; + + @override + String get studyBack => 'Indietro'; + + @override + String get studyPlayAgain => 'Gioca di nuovo'; + + @override + String get studyWhatWouldYouPlay => 'Cosa giocheresti in questa posizione?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulazioni! Hai completato questa lezione.'; + + @override + String studyPerPage(String param) { + return '$param per pagina'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capitoli', + one: '$count capitolo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partite', + one: '$count partita', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membri', + one: '$count membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Incolla qui i testi PGN, massimo $count partite', + one: 'Incolla qui il testo PGN, massimo $count partita', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'adesso'; + + @override + String get timeagoRightNow => 'adesso'; + + @override + String get timeagoCompleted => 'completato'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count secondi', + one: 'tra $count secondo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count minuti', + one: 'tra $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count ore', + one: 'tra $count ora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count giorni', + one: 'tra $count giorno', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count settimane', + one: 'tra $count settimana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count mesi', + one: 'tra $count mese', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count anni', + one: 'tra $count anno', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti fa', + one: '$count minuto fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore fa', + one: '$count ora fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count giorni fa', + one: '$count giorno fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count settimane fa', + one: '$count settimana fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mesi fa', + one: '$count mese fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count anni fa', + one: '$count anno fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti rimanenti', + one: '$count minuto rimanente', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore rimanenti', + one: '$count ora rimanente', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index c64040946d..6f03ec4a3f 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); @override - String get mobileHomeTab => 'ホーム'; + String get mobileAllGames => 'すべて'; @override - String get mobilePuzzlesTab => '問題'; + String get mobileAreYouSure => '本当にいいですか?'; @override - String get mobileToolsTab => 'ツール'; + String get mobileCancelTakebackOffer => '待ったをキャンセル'; @override - String get mobileWatchTab => '見る'; + String get mobileClearButton => 'クリア'; @override - String get mobileSettingsTab => '設定'; + String get mobileCorrespondenceClearSavedMove => '保存した手を削除'; @override - String get mobileMustBeLoggedIn => 'このページを見るにはログインが必要です。'; + String get mobileCustomGameJoinAGame => 'ゲームに参加'; @override - String get mobileSystemColors => 'OS と同じ色設定'; + String get mobileFeedbackButton => 'フィードバック'; @override - String get mobileFeedbackButton => 'フィードバック'; + String mobileGreeting(String param) { + return 'こんにちは $param さん'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'こんにちは'; @override - String get mobileSettingsHapticFeedback => '振動フィードバック'; + String get mobileHideVariation => '変化手順を隠す'; @override - String get mobileSettingsImmersiveMode => '没入モード'; + String get mobileHomeTab => 'ホーム'; @override - String get mobileSettingsImmersiveModeSubtitle => '対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。'; + String get mobileLiveStreamers => 'ライブ配信者'; @override - String get mobileNotFollowingAnyUser => '誰もフォローしていません。'; + String get mobileMustBeLoggedIn => 'このページを見るにはログインが必要です。'; @override - String get mobileAllGames => 'すべて'; + String get mobileNoSearchResults => '検索結果なし'; @override - String get mobileRecentSearches => '最近の検索'; + String get mobileNotFollowingAnyUser => '誰もフォローしていません。'; @override - String get mobileClearButton => 'クリア'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get mobileNoSearchResults => '検索結果なし'; + String get mobilePrefMagnifyDraggedPiece => 'ドラッグ中の駒を拡大'; @override - String get mobileAreYouSure => '本当にいいですか?'; + String get mobilePuzzleStormConfirmEndRun => 'このストームを終了しますか?'; @override - String get mobilePuzzleStreakAbortWarning => '現在の連続正解が終わり、スコアが保存されます。'; + String get mobilePuzzleStormFilterNothingToShow => '条件に合う問題がありません。フィルターを変更してください'; @override String get mobilePuzzleStormNothingToShow => 'データがありません。まず問題ストームをプレイして。'; @override - String get mobileSharePuzzle => 'この問題を共有する'; + String get mobilePuzzleStormSubtitle => '3 分間でできるだけ多くの問題を解いてください。'; @override - String get mobileShareGameURL => 'ゲーム URLを共有'; + String get mobilePuzzleStreakAbortWarning => '現在の連続正解が終わり、スコアが保存されます。'; @override - String get mobileShareGamePGN => 'PGN を共有'; + String get mobilePuzzleThemesSubtitle => 'お気に入りのオープニングやテーマの問題が選べます。'; @override - String get mobileSharePositionAsFEN => '局面を FEN で共有'; + String get mobilePuzzlesTab => '問題'; @override - String get mobileShowVariations => '変化手順を表示'; + String get mobileRecentSearches => '最近の検索'; @override - String get mobileHideVariation => '変化手順を隠す'; + String get mobileSettingsHapticFeedback => '振動フィードバック'; @override - String get mobileShowComments => 'コメントを表示'; + String get mobileSettingsImmersiveMode => '没入モード'; @override - String get mobilePuzzleStormConfirmEndRun => 'このストームを終了しますか?'; + String get mobileSettingsImmersiveModeSubtitle => '対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。'; @override - String get mobilePuzzleStormFilterNothingToShow => '条件に合う問題がありません。フィルターを変更してください'; + String get mobileSettingsTab => '設定'; @override - String get mobileCancelTakebackOffer => '待ったをキャンセル'; + String get mobileShareGamePGN => 'PGN を共有'; @override - String get mobileCancelDrawOffer => 'ドロー提案をキャンセル'; + String get mobileShareGameURL => 'ゲーム URLを共有'; @override - String get mobileWaitingForOpponentToJoin => '対戦相手の参加を待っています…'; + String get mobileSharePositionAsFEN => '局面を FEN で共有'; @override - String get mobileBlindfoldMode => 'めかくしモード'; + String get mobileSharePuzzle => 'この問題を共有する'; @override - String get mobileLiveStreamers => 'ライブ配信者'; + String get mobileShowComments => 'コメントを表示'; @override - String get mobileCustomGameJoinAGame => 'ゲームに参加'; + String get mobileShowResult => '結果を表示'; @override - String get mobileCorrespondenceClearSavedMove => '保存した手を削除'; + String get mobileShowVariations => '変化手順を表示'; @override String get mobileSomethingWentWrong => '問題が発生しました。'; @override - String get mobileShowResult => '結果を表示'; - - @override - String get mobilePuzzleThemesSubtitle => 'お気に入りのオープニングやテーマの問題が選べます。'; + String get mobileSystemColors => 'OS と同じ色設定'; @override - String get mobilePuzzleStormSubtitle => '3 分間でできるだけ多くの問題を解いてください。'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'こんにちは $param さん'; - } + String get mobileToolsTab => 'ツール'; @override - String get mobileGreetingWithoutName => 'こんにちは'; + String get mobileWaitingForOpponentToJoin => '対戦相手の参加を待っています…'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => '見る'; @override String get activityActivity => '活動'; @@ -238,6 +235,16 @@ class AppLocalizationsJa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 局の $param2 通信戦を完了しました', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +338,255 @@ class AppLocalizationsJa extends AppLocalizations { @override String get broadcastBroadcasts => 'イベント中継'; + @override + String get broadcastMyBroadcasts => '自分の配信'; + @override String get broadcastLiveBroadcasts => '実戦トーナメントのライブ中継'; + @override + String get broadcastBroadcastCalendar => '中継カレンダー'; + + @override + String get broadcastNewBroadcast => '新しいライブ中継'; + + @override + String get broadcastSubscribedBroadcasts => '登録した配信'; + + @override + String get broadcastAboutBroadcasts => '中継について'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Lichess 中継の使い方。'; + + @override + String get broadcastTheNewRoundHelp => '新ラウンドには前回と同じメンバーと投稿者が参加します。'; + + @override + String get broadcastAddRound => 'ラウンドを追加'; + + @override + String get broadcastOngoing => '配信中'; + + @override + String get broadcastUpcoming => '予定'; + + @override + String get broadcastCompleted => '終了'; + + @override + String get broadcastCompletedHelp => 'Lichess は元になる対局に基づいてラウンド終了を検出します。元になる対局がない時はこのトグルを使ってください。'; + + @override + String get broadcastRoundName => 'ラウンド名'; + + @override + String get broadcastRoundNumber => 'ラウンド'; + + @override + String get broadcastTournamentName => '大会名'; + + @override + String get broadcastTournamentDescription => '大会の短い説明'; + + @override + String get broadcastFullDescription => '長い説明'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '内容の詳しい説明(オプション)。$param1 が利用できます。長さは [欧文換算で] $param2 字まで。'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN のソース URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess が PGN を取得するための URL。インターネット上に公表されているもののみ。'; + + @override + String get broadcastSourceGameIds => 'Lichess ゲーム ID、半角スペースで区切って最大 64 個まで。'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'イベント開始時刻(オプション)'; + + @override + String get broadcastCurrentGameUrl => '現在のゲームの URL'; + + @override + String get broadcastDownloadAllRounds => '全ラウンドをダウンロード'; + + @override + String get broadcastResetRound => 'このラウンドをリセット'; + + @override + String get broadcastDeleteRound => 'このラウンドを削除'; + + @override + String get broadcastDefinitivelyDeleteRound => 'このラウンドのゲームをすべて削除する。'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'このラウンドのすべてのゲームを削除します。復活させるには情報源がアクティブでなくてはなりません。'; + + @override + String get broadcastEditRoundStudy => 'ラウンドの研究を編集'; + + @override + String get broadcastDeleteTournament => 'このトーナメントを削除'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'トーナメント全体(全ラウンド、全ゲーム)を削除する。'; + + @override + String get broadcastShowScores => 'ゲーム結果に応じてプレイヤーのスコアを表示'; + + @override + String get broadcastReplacePlayerTags => 'オプション:プレイヤーの名前、レーティング、タイトルの変更'; + + @override + String get broadcastFideFederations => 'FIDE 加盟協会'; + + @override + String get broadcastTop10Rating => 'レーティング トップ10'; + + @override + String get broadcastFidePlayers => 'FIDE 選手'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE 選手が見つかりません'; + + @override + String get broadcastFideProfile => 'FIDE プロフィール'; + + @override + String get broadcastFederation => '所属協会'; + + @override + String get broadcastAgeThisYear => '今年時点の年齢'; + + @override + String get broadcastUnrated => 'レーティングなし'; + + @override + String get broadcastRecentTournaments => '最近のトーナメント'; + + @override + String get broadcastOpenLichess => 'Lichess で開く'; + + @override + String get broadcastTeams => 'チーム'; + + @override + String get broadcastBoards => 'ボード'; + + @override + String get broadcastOverview => '概要'; + + @override + String get broadcastSubscribeTitle => '登録しておくと各ラウンドの開始時に通知が来ます。アカウント設定でベルやプッシュ通知の切り替えができます。'; + + @override + String get broadcastUploadImage => 'トーナメントの画像をアップロード'; + + @override + String get broadcastNoBoardsYet => 'ボードはまだありません。棋譜がアップロードされると表示されます。'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'ボードはソースまたは $param 経由で読み込めます'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param 後に開始'; + } + + @override + String get broadcastStartVerySoon => '中継はまもなく始まります。'; + + @override + String get broadcastNotYetStarted => '中継はまだ始まっていません。'; + + @override + String get broadcastOfficialWebsite => '公式サイト'; + + @override + String get broadcastStandings => '順位'; + + @override + String get broadcastOfficialStandings => '公式順位'; + + @override + String broadcastIframeHelp(String param) { + return '他のオプションは $param にあります'; + } + + @override + String get broadcastWebmastersPage => 'ウェブ管理者のページ'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'このラウンドについて公表されたリアルタイムの PGN です。$param も利用でき、高速かつ高効率の同期が行なえます。'; + } + + @override + String get broadcastEmbedThisBroadcast => 'この中継をウェブサイトに埋め込む'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param をウェブサイトに埋め込む'; + } + + @override + String get broadcastRatingDiff => 'レーティングの差'; + + @override + String get broadcastGamesThisTournament => 'このトーナメントの対局'; + + @override + String get broadcastScore => 'スコア'; + + @override + String get broadcastAllTeams => 'すべてのチーム'; + + @override + String get broadcastTournamentFormat => 'トーナメント形式'; + + @override + String get broadcastTournamentLocation => '開催地'; + + @override + String get broadcastTopPlayers => 'トッププレイヤー'; + + @override + String get broadcastTimezone => 'タイムゾーン'; + + @override + String get broadcastFideRatingCategory => 'FIDE レーティング カテゴリー'; + + @override + String get broadcastOptionalDetails => 'その他詳細(オプション)'; + + @override + String get broadcastPastBroadcasts => '過去の中継'; + + @override + String get broadcastAllBroadcastsByMonth => 'すべての中継を月別に表示'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ブロードキャスト', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'チャレンジ:$param1'; @@ -592,6 +845,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get preferencesInGameOnly => '対局中のみ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '時間表示'; @@ -733,6 +989,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'ベル通知の音'; + @override + String get preferencesBlindfold => 'めかくしモード'; + @override String get puzzlePuzzles => 'タクティクス問題'; @@ -1368,10 +1627,10 @@ class AppLocalizationsJa extends AppLocalizations { String get puzzleThemeZugzwangDescription => '相手の指せる手が、どれを選んでも局面を悪くしてしまう形。'; @override - String get puzzleThemeHealthyMix => '混合'; + String get puzzleThemeMix => '混合'; @override - String get puzzleThemeHealthyMixDescription => 'いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。'; + String get puzzleThemeMixDescription => 'いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。'; @override String get puzzleThemePlayerGames => 'プレイヤーの対局'; @@ -1745,9 +2004,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get byCPL => '評価値で'; - @override - String get openStudy => '研究を開く'; - @override String get enable => '解析する'; @@ -1775,9 +2031,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get removesTheDepthLimit => '探索手数の制限をなくし最大限の解析を行なう'; - @override - String get engineManager => '解析エンジンの管理'; - @override String get blunder => '大悪手'; @@ -2041,6 +2294,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get gamesPlayed => '対局数'; + @override + String get ok => 'OK'; + @override String get cancel => 'キャンセル'; @@ -2415,9 +2671,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get unblock => 'ブロックを外す'; - @override - String get followsYou => 'あなたをフォローしています'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 が $param2 のフォローを開始'; @@ -2750,7 +3003,13 @@ class AppLocalizationsJa extends AppLocalizations { String get other => 'その他'; @override - String get reportDescriptionHelp => '問題のゲームへのリンクを貼って、相手ユーザーの問題点を説明してください。ただ「イカサマだ」と言うのではなく、なぜそう思うか理由を書いてください。英語で書くと対応が早くできます。'; + String get reportCheatBoostHelp => 'ゲームへのリンクを張って、このユーザーの行動のどこが問題かを説明してください。ただ「チート」と言うのではなく、あなたがなぜそう思ったのか教えてください。'; + + @override + String get reportUsernameHelp => 'このユーザー名のどこが攻撃的かを説明してください。ただ「攻撃的」「不適切」と言うのではなく、あなたがなぜそう思ったのか教えてください。中でも綴りの変更、英語以外の言語、俗語、歴史・文化的要因に関係した場合は特に説明が必要です。'; + + @override + String get reportProcessedFasterInEnglish => '英語で書いていただくと通報への対応が早くなります。'; @override String get error_provideOneCheatedGameLink => '不正のあった対局 1 局以上へのリンクを添えてください。'; @@ -4055,6 +4314,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get nothingToSeeHere => '今は何もありません。'; + @override + String get stats => '統計'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4657,9 +4919,674 @@ class AppLocalizationsJa extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess 配信者'; + @override + String get studyPrivate => '非公開'; + + @override + String get studyMyStudies => '自分の研究'; + + @override + String get studyStudiesIContributeTo => '参加した研究'; + + @override + String get studyMyPublicStudies => '自分の公開研究'; + + @override + String get studyMyPrivateStudies => '自分の非公開研究'; + + @override + String get studyMyFavoriteStudies => 'お気に入りの研究'; + + @override + String get studyWhatAreStudies => '研究(study)とは?'; + + @override + String get studyAllStudies => 'すべての研究'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param による研究'; + } + + @override + String get studyNoneYet => 'まだなし'; + + @override + String get studyHot => '注目'; + + @override + String get studyDateAddedNewest => '投稿日(新しい順)'; + + @override + String get studyDateAddedOldest => '投稿日(古い順)'; + + @override + String get studyRecentlyUpdated => '更新順'; + + @override + String get studyMostPopular => '人気順'; + + @override + String get studyAlphabetical => 'アルファベット順'; + + @override + String get studyAddNewChapter => '新たな章を追加'; + + @override + String get studyAddMembers => 'メンバーを追加する'; + + @override + String get studyInviteToTheStudy => 'この研究に招待する'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '招待する相手は、あなたが知っていて参加したい人だけにしてください。'; + + @override + String get studySearchByUsername => 'ユーザー名で検索'; + + @override + String get studySpectator => '観戦者'; + + @override + String get studyContributor => '投稿参加者'; + + @override + String get studyKick => '追放'; + + @override + String get studyLeaveTheStudy => 'この研究から出る'; + + @override + String get studyYouAreNowAContributor => '投稿参加者になりました'; + + @override + String get studyYouAreNowASpectator => '観戦者になりました'; + + @override + String get studyPgnTags => 'PGN タグ'; + + @override + String get studyLike => 'いいね'; + + @override + String get studyUnlike => 'いいね解除'; + + @override + String get studyNewTag => '新しいタグ'; + + @override + String get studyCommentThisPosition => 'この局面にコメントする'; + + @override + String get studyCommentThisMove => 'この手にコメント'; + + @override + String get studyAnnotateWithGlyphs => '解説記号を入れる'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '章が短すぎて解析できません。'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'コンピュータ解析を要請できるのは投稿参加者だけです。'; + + @override + String get studyGetAFullComputerAnalysis => '主手順についてサーバ上でのコンピュータ解析を行なう。'; + + @override + String get studyMakeSureTheChapterIsComplete => '章が完成したか確認してください。解析の要請は 1 回だけです。'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '同期したメンバーは同じ局面に留まります'; + + @override + String get studyShareChanges => '変更を観戦者と共有し、サーバに保存する'; + + @override + String get studyPlaying => 'プレイ中'; + + @override + String get studyShowEvalBar => '評価値バー'; + + @override + String get studyFirst => '最初'; + + @override + String get studyPrevious => '前'; + + @override + String get studyNext => '次'; + + @override + String get studyLast => '最後'; + @override String get studyShareAndExport => '共有とエクスポート'; + @override + String get studyCloneStudy => '研究をコピー'; + + @override + String get studyStudyPgn => '研究の PGN'; + + @override + String get studyDownloadAllGames => '全局をダウンロード'; + + @override + String get studyChapterPgn => '章の PGN'; + + @override + String get studyCopyChapterPgn => 'PGN をコピー'; + + @override + String get studyDownloadGame => '1 局をダウンロード'; + + @override + String get studyStudyUrl => '研究の URL'; + + @override + String get studyCurrentChapterUrl => '現在の章の URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'これをフォーラムにペーストすれば埋め込み表示できます'; + + @override + String get studyStartAtInitialPosition => '開始局面から'; + + @override + String studyStartAtX(String param) { + return '$param に開始'; + } + + @override + String get studyEmbedInYourWebsite => '自分のウェブサイト/ブログに埋め込む'; + + @override + String get studyReadMoreAboutEmbedding => '埋め込み(embedding)の説明'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '埋め込みできるのは公開研究だけです!'; + + @override + String get studyOpen => '開く'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 を $param2 がお届けします'; + } + + @override + String get studyStudyNotFound => '研究が見つかりません'; + + @override + String get studyEditChapter => '章を編集'; + + @override + String get studyNewChapter => '新しい章'; + + @override + String studyImportFromChapterX(String param) { + return '$param からインポート'; + } + + @override + String get studyOrientation => '盤の上下'; + + @override + String get studyAnalysisMode => '解析モード'; + + @override + String get studyPinnedChapterComment => '章の優先表示コメント'; + + @override + String get studySaveChapter => '章を保存'; + + @override + String get studyClearAnnotations => '注釈をクリア'; + + @override + String get studyClearVariations => '手順をクリア'; + + @override + String get studyDeleteChapter => '章を削除'; + + @override + String get studyDeleteThisChapter => 'ほんとうに削除しますか? 戻せませんよ!'; + + @override + String get studyClearAllCommentsInThisChapter => 'この章のコメントと図形をすべて削除しますか?'; + + @override + String get studyRightUnderTheBoard => '盤のすぐ下に'; + + @override + String get studyNoPinnedComment => 'なし'; + + @override + String get studyNormalAnalysis => '通常解析'; + + @override + String get studyHideNextMoves => '次の手順をかくす'; + + @override + String get studyInteractiveLesson => '対話形式のレッスン'; + + @override + String studyChapterX(String param) { + return '章 $param'; + } + + @override + String get studyEmpty => '空白'; + + @override + String get studyStartFromInitialPosition => '開始局面から'; + + @override + String get studyEditor => 'エディタ'; + + @override + String get studyStartFromCustomPosition => '指定した局面から'; + + @override + String get studyLoadAGameByUrl => '棋譜を URL で読み込み'; + + @override + String get studyLoadAPositionFromFen => '局面を FEN で読み込み'; + + @override + String get studyLoadAGameFromPgn => '棋譜を PGN で読み込み'; + + @override + String get studyAutomatic => '自動'; + + @override + String get studyUrlOfTheGame => '棋譜の URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 か $param2 から棋譜を読み込み'; + } + + @override + String get studyCreateChapter => '章を作成'; + + @override + String get studyCreateStudy => '研究を作成'; + + @override + String get studyEditStudy => '研究を編集'; + + @override + String get studyVisibility => '公開範囲'; + + @override + String get studyPublic => '公開'; + + @override + String get studyUnlisted => '非公開'; + + @override + String get studyInviteOnly => '招待のみ'; + + @override + String get studyAllowCloning => 'コピーの許可'; + + @override + String get studyNobody => '不許可'; + + @override + String get studyOnlyMe => '自分のみ'; + + @override + String get studyContributors => '参加者のみ'; + + @override + String get studyMembers => 'メンバー'; + + @override + String get studyEveryone => '全員'; + + @override + String get studyEnableSync => '同期'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => '同期する=全員が同じ局面を見る'; + + @override + String get studyNoLetPeopleBrowseFreely => '同期しない=各人が自由に閲覧'; + + @override + String get studyPinnedStudyComment => '優先表示コメント'; + @override String get studyStart => '開始'; + + @override + String get studySave => '保存'; + + @override + String get studyClearChat => 'チャットを消去'; + + @override + String get studyDeleteTheStudyChatHistory => 'ほんとうに削除しますか? 戻せませんよ!'; + + @override + String get studyDeleteStudy => '研究を削除'; + + @override + String studyConfirmDeleteStudy(String param) { + return '研究全体を削除しますか? 戻せませんよ! 削除なら研究の名称を入力: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'どこで研究しますか?'; + + @override + String get studyGoodMove => '好手'; + + @override + String get studyMistake => '悪手'; + + @override + String get studyBrilliantMove => '妙手'; + + @override + String get studyBlunder => '大悪手'; + + @override + String get studyInterestingMove => '面白い手'; + + @override + String get studyDubiousMove => '疑問手'; + + @override + String get studyOnlyMove => '絶対手'; + + @override + String get studyZugzwang => 'ツークツワンク'; + + @override + String get studyEqualPosition => '互角'; + + @override + String get studyUnclearPosition => '形勢不明'; + + @override + String get studyWhiteIsSlightlyBetter => '白やや優勢'; + + @override + String get studyBlackIsSlightlyBetter => '黒やや優勢'; + + @override + String get studyWhiteIsBetter => '白優勢'; + + @override + String get studyBlackIsBetter => '黒優勢'; + + @override + String get studyWhiteIsWinning => '白勝勢'; + + @override + String get studyBlackIsWinning => '黒勝勢'; + + @override + String get studyNovelty => '新手'; + + @override + String get studyDevelopment => '展開'; + + @override + String get studyInitiative => '主導権'; + + @override + String get studyAttack => '攻撃'; + + @override + String get studyCounterplay => '反撃'; + + @override + String get studyTimeTrouble => '時間切迫'; + + @override + String get studyWithCompensation => '駒損だが代償あり'; + + @override + String get studyWithTheIdea => '狙い'; + + @override + String get studyNextChapter => '次の章'; + + @override + String get studyPrevChapter => '前の章'; + + @override + String get studyStudyActions => '研究の操作'; + + @override + String get studyTopics => 'トピック'; + + @override + String get studyMyTopics => '自分のトピック'; + + @override + String get studyPopularTopics => '人気のトピック'; + + @override + String get studyManageTopics => 'トピックの管理'; + + @override + String get studyBack => '戻る'; + + @override + String get studyPlayAgain => 'もう一度プレイ'; + + @override + String get studyWhatWouldYouPlay => 'この局面、あなたならどう指す?'; + + @override + String get studyYouCompletedThisLesson => 'おめでとう ! このレッスンを修了しました。'; + + @override + String studyPerPage(String param) { + return '$param 件/ページ'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 章', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 局', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count メンバー', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ここに PGN をペースト($count 局まで)', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'たった今'; + + @override + String get timeagoRightNow => 'たった今'; + + @override + String get timeagoCompleted => '完了'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 秒後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 時間後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 日後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 週後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count か月後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年後', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分前', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 時間前', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 日前', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 週前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count か月前', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '残り $count 分', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '残り $count 時間', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index 8dbeda53d5..a3fb2fa76f 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsKk extends AppLocalizations { AppLocalizationsKk([String locale = 'kk']) : super(locale); @override - String get mobileHomeTab => 'Үйге'; + String get mobileAllGames => 'Барлық ойындар'; @override - String get mobilePuzzlesTab => 'Жұмбақ'; + String get mobileAreYouSure => 'Растайсыз ба?'; @override - String get mobileToolsTab => 'Құрал'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Бақылау'; + String get mobileClearButton => 'Өшіру'; @override - String get mobileSettingsTab => 'Баптау'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'Бұл бетті көру үшін тіркелгіге кіріңіз.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'Жүйе түстері'; + String get mobileFeedbackButton => 'Пікір айту'; @override - String get mobileFeedbackButton => 'Пікір айту'; + String mobileGreeting(String param) { + return 'Ассаламу ғалейкүм, $param'; + } @override - String get mobileOkButton => 'Иә'; + String get mobileGreetingWithoutName => 'Ассаламу ғалейкүм'; @override - String get mobileSettingsHapticFeedback => 'Дірілмен білдіру'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Оқшау көрініс'; + String get mobileHomeTab => 'Үйге'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'Сіз әзір ешкіге серік емессіз.'; + String get mobileMustBeLoggedIn => 'Бұл бетті көру үшін тіркелгіге кіріңіз.'; @override - String get mobileAllGames => 'Барлық ойындар'; + String get mobileNoSearchResults => 'Нәтиже жоқ'; @override - String get mobileRecentSearches => 'Кейінгі іздеулер'; + String get mobileNotFollowingAnyUser => 'Сіз әзір ешкіге серік емессіз.'; @override - String get mobileClearButton => 'Өшіру'; + String get mobileOkButton => 'Иә'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsKk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Нәтиже жоқ'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Растайсыз ба?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'Қазіргі тізбектен айрыласыз, нәтиже сақталады.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.'; @override - String get mobileSharePuzzle => 'Бұл жұмбақты тарату'; + String get mobilePuzzleStormSubtitle => '3 минутта барынша көп жұмбақ шешіп көр.'; @override - String get mobileShareGameURL => 'Ойын сілтемесін тарату'; + String get mobilePuzzleStreakAbortWarning => 'Қазіргі тізбектен айрыласыз, нәтиже сақталады.'; @override - String get mobileShareGamePGN => 'PGN тарату'; + String get mobilePuzzleThemesSubtitle => 'Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Жұмбақ'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Кейінгі іздеулер'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Дірілмен білдіру'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Оқшау көрініс'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Баптау'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'PGN тарату'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Ойын сілтемесін тарату'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Бұл жұмбақты тарату'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Нәтижесін көру'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Нәтижесін көру'; - - @override - String get mobilePuzzleThemesSubtitle => 'Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.'; + String get mobileSystemColors => 'Жүйе түстері'; @override - String get mobilePuzzleStormSubtitle => '3 минутта барынша көп жұмбақ шешіп көр.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ассаламу ғалейкүм, $param'; - } + String get mobileToolsTab => 'Құрал'; @override - String get mobileGreetingWithoutName => 'Ассаламу ғалейкүм'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Бақылау'; @override String get activityActivity => 'Белсенділігі'; @@ -246,6 +243,17 @@ class AppLocalizationsKk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsKk extends AppLocalizations { @override String get broadcastBroadcasts => 'Көрсетілімдер'; + @override + String get broadcastMyBroadcasts => 'Менің көрсетілімдерім'; + @override String get broadcastLiveBroadcasts => 'Жарыстың тікелей көрсетілімдері'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Жаңа тікелей көрсетілім'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Айналым қосу'; + + @override + String get broadcastOngoing => 'Болып жатқан'; + + @override + String get broadcastUpcoming => 'Келе жатқан'; + + @override + String get broadcastCompleted => 'Аяқталған'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Айналым атауы'; + + @override + String get broadcastRoundNumber => 'Раунд нөмірі'; + + @override + String get broadcastTournamentName => 'Жарыс атауы'; + + @override + String get broadcastTournamentDescription => 'Жарыстың қысқа сипаттамасы'; + + @override + String get broadcastFullDescription => 'Оқиғаның толық сипаттамасы'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Көрсетілімнің қосымша үлкен сипаттамасы. $param1 қолданысқа ашық. Ұзындығы $param2 таңбадан кем болуы керек.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'PGN жаңартуларын алу үшін Личес тексеретін сілтеме. Ол интернетте баршалыққа ашық болуы керек.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Міндетті емес, егер күнін біліп тұрсаңыз'; + + @override + String get broadcastCurrentGameUrl => 'Қазіргі ойын сілтемесі'; + + @override + String get broadcastDownloadAllRounds => 'Барлық айналымдарды жүктеп алу'; + + @override + String get broadcastResetRound => 'Бұл айналымды жаңарту'; + + @override + String get broadcastDeleteRound => 'Бұл айналымды жою'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Айналым мен оның ойындарын толығымен жою.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Айналымның бүкіл ойындарын жою. Оларды қайта құру үшін қайнар көзі белсенді болуы керек.'; + + @override + String get broadcastEditRoundStudy => 'Айналымның зертханасын өзгерту'; + + @override + String get broadcastDeleteTournament => 'Бұл жарысты жою'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Жарысты айналым мен ойындарымен бірге толығымен жою.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count көрсетілім', + one: '$count көрсетілім', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Шақырулар: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get preferencesInGameOnly => 'Ойында ғана'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахмат сағаты'; @@ -750,6 +1008,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Қоңыраулы ескерту'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Жұмбақтар'; @@ -1390,10 +1651,10 @@ class AppLocalizationsKk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Жүрісі шектелген тастардың әр жүрісі жалпы жағдайдың нашарлауына әкеп соқтыратын кез.'; @override - String get puzzleThemeHealthyMix => 'Аралас дастархан'; + String get puzzleThemeMix => 'Аралас дастархан'; @override - String get puzzleThemeHealthyMixDescription => 'Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!'; + String get puzzleThemeMixDescription => 'Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!'; @override String get puzzleThemePlayerGames => 'Ойыншылардан'; @@ -1767,9 +2028,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get byCPL => 'CPL сәйкес'; - @override - String get openStudy => 'Зерттеуді ашу'; - @override String get enable => 'Қосу'; @@ -1797,9 +2055,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get removesTheDepthLimit => 'Тереңдік шектеулерін жояды, әрі компьютеріңізді қыздырады'; - @override - String get engineManager => 'Есептеуіш басқарушысы'; - @override String get blunder => 'Өрескел қателік'; @@ -2063,6 +2318,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get gamesPlayed => 'Ойындар саны'; + @override + String get ok => 'OK'; + @override String get cancel => 'Болдыртпау'; @@ -2437,9 +2695,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get unblock => 'Бұғаттан шығару'; - @override - String get followsYou => 'Сізге серік'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 серігі болды'; @@ -2772,7 +3027,13 @@ class AppLocalizationsKk extends AppLocalizations { String get other => 'Басқа'; @override - String get reportDescriptionHelp => 'Ойынның (-дардың) сілтемесін қойып, осы ойыншының қай әрекеті орынсыз болғанын түсіндіріп беріңіз. Жай ғана \"ол алдап ойнады\" деп жаза салмай, осы ойға қалай келгеніңізді айтып беріңіз. Сіздің шағымыңыз ағылшын тілінде жазылса, тезірек тексеруден өтеді.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Кемі бір ойынның сілтемесін беруіңізді сұраймыз.'; @@ -3388,10 +3649,10 @@ class AppLocalizationsKk extends AppLocalizations { String get asFreeAsLichess => 'Личес-тей тегін'; @override - String get builtForTheLoveOfChessNotMoney => 'Ақша қуып емес, шахматты қызыққаннан жасап отырмыз'; + String get builtForTheLoveOfChessNotMoney => 'Ақша қуып емес, шахматты жақсы көргеннен жасап отырмыз'; @override - String get everybodyGetsAllFeaturesForFree => 'Барлық құралдары бүкіл адам үшін тегін'; + String get everybodyGetsAllFeaturesForFree => 'Барлық құралдары барлық адам үшін тегін'; @override String get zeroAdvertisement => 'Еш жарнамасыз'; @@ -4077,6 +4338,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4501,7 +4765,7 @@ class AppLocalizationsKk extends AppLocalizations { count, locale: localeName, other: '$count тіліндегі нұсқасы бар!', - one: '$count тіліндегі нұсқасы бар!', + one: '$count тілде нұсқасы бар!', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsKk extends AppLocalizations { @override String get streamerLichessStreamers => 'Личес стримерлері'; + @override + String get studyPrivate => 'Жеке'; + + @override + String get studyMyStudies => 'Менің зерттеулерім'; + + @override + String get studyStudiesIContributeTo => 'Қолдауымдағы зерттеулер'; + + @override + String get studyMyPublicStudies => 'Жалпыға ашық зерттеулерім'; + + @override + String get studyMyPrivateStudies => 'Жеке зерттеулерім'; + + @override + String get studyMyFavoriteStudies => 'Қалаулы зерттеулерім'; + + @override + String get studyWhatAreStudies => 'Зерттеулер деген не?'; + + @override + String get studyAllStudies => 'Бүкіл зерттеулер'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param жасаған зерттеулер'; + } + + @override + String get studyNoneYet => 'Әзірге жоқ.'; + + @override + String get studyHot => 'Тренд'; + + @override + String get studyDateAddedNewest => 'Құрылған күні (жаңадан)'; + + @override + String get studyDateAddedOldest => 'Құрылған күні (ескіден)'; + + @override + String get studyRecentlyUpdated => 'Жақында құрылған'; + + @override + String get studyMostPopular => 'Ең танымалдары'; + + @override + String get studyAlphabetical => 'Әліппе ретімен'; + + @override + String get studyAddNewChapter => 'Жаңа бөлім құру'; + + @override + String get studyAddMembers => 'Мүшелерді қосу'; + + @override + String get studyInviteToTheStudy => 'Зерттеуге шақыру'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Тек таныстарды әрі зерттеуге қосылуға шын ниетті адамдарды ғана шықырыңыз.'; + + @override + String get studySearchByUsername => 'Тіркеулі атымен іздеу'; + + @override + String get studySpectator => 'Көрермен'; + + @override + String get studyContributor => 'Қолдаушы'; + + @override + String get studyKick => 'Шығару'; + + @override + String get studyLeaveTheStudy => 'Зерттеуден шығу'; + + @override + String get studyYouAreNowAContributor => 'Сіз енді қолдаушысыз'; + + @override + String get studyYouAreNowASpectator => 'Сіз енді көрерменсіз'; + + @override + String get studyPgnTags => 'PGN тэгтері'; + + @override + String get studyLike => 'Ұнату'; + + @override + String get studyUnlike => 'Ұнатпаймын'; + + @override + String get studyNewTag => 'Жаңа тэг'; + + @override + String get studyCommentThisPosition => 'Осы тақта күйі туралы пікір қалдыру'; + + @override + String get studyCommentThisMove => 'Осы жүріс туралы пікір қалдыру'; + + @override + String get studyAnnotateWithGlyphs => 'Глифтермен түсіндірме жазуу'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Бөлім талдауға жарамды болу үшін тым қысқа.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Зерттеу қолдаушылары ғана компьютерлік талдауды сұрай алады.'; + + @override + String get studyGetAFullComputerAnalysis => 'Сервер-жақты компьютер осы негізгі жолға толық талдау жасайтын болады.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Талдауды бір рет қана сұрай аласыз, сондықтан бөлімді аяқтауды ұмытпаңыз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Барлық үйлескен мүшелер өз күйінде қалады'; + + @override + String get studyShareChanges => 'Көрермендермен өзгертулерді бөлісіңіз әрі серверде сақтап қойыңыз'; + + @override + String get studyPlaying => 'Қазір ойында'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Бірінші'; + + @override + String get studyPrevious => 'Алдыңғы'; + + @override + String get studyNext => 'Келесі'; + + @override + String get studyLast => 'Соңғы'; + @override String get studyShareAndExport => 'Бөлісу мен Жүктеп алу'; + @override + String get studyCloneStudy => 'Көшірме'; + + @override + String get studyStudyPgn => 'Зерттеудің PGN'; + + @override + String get studyDownloadAllGames => 'Барлық ойындарды жүктеп алу'; + + @override + String get studyChapterPgn => 'Бөлімнің PGN'; + + @override + String get studyCopyChapterPgn => 'PGN-ді көшіру'; + + @override + String get studyDownloadGame => 'Ойынды жүктеп алу'; + + @override + String get studyStudyUrl => 'Зерттеудің сілтемесі'; + + @override + String get studyCurrentChapterUrl => 'Қазіргі бөлімнің сілтемесі'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Сіз бұны форумға не Личес блогыңызға қоя аласыз'; + + @override + String get studyStartAtInitialPosition => 'Басталуы: бастапқы күйден'; + + @override + String studyStartAtX(String param) { + return 'Басталуы: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Сіздің сайт не блогыңызға арналған енгізу сілтемесі'; + + @override + String get studyReadMoreAboutEmbedding => 'Енгізу туралы оқыңыз'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Тек жалпыға ашық зерттеулер енгізуге жарамды!'; + + @override + String get studyOpen => 'Ашу'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, оны сізге $param2 ұсынды'; + } + + @override + String get studyStudyNotFound => 'Зерттеу табылмады'; + + @override + String get studyEditChapter => 'Бөлімді өңдеу'; + + @override + String get studyNewChapter => 'Жаңа бөлім'; + + @override + String studyImportFromChapterX(String param) { + return '$param-нан жүктеп алу'; + } + + @override + String get studyOrientation => 'Бағыты'; + + @override + String get studyAnalysisMode => 'Талдау нұсқасы'; + + @override + String get studyPinnedChapterComment => 'Қадаулы бөлім пікірі'; + + @override + String get studySaveChapter => 'Бөлімді сақтау'; + + @override + String get studyClearAnnotations => 'Түсіндірмені өшіру'; + + @override + String get studyClearVariations => 'Тармақты өшіру'; + + @override + String get studyDeleteChapter => 'Бөлімді жою'; + + @override + String get studyDeleteThisChapter => 'Бөлімді жоясыз ба? Кері жол жоқ!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Бөлімдегі бүкіл пікір, глиф пен сызбаларды өшіресіз бе?'; + + @override + String get studyRightUnderTheBoard => 'Тура тақтаның астына'; + + @override + String get studyNoPinnedComment => 'Жоқ'; + + @override + String get studyNormalAnalysis => 'Қалыпты талдау'; + + @override + String get studyHideNextMoves => 'Келесі жүрістерді жасыру'; + + @override + String get studyInteractiveLesson => 'Интерактивті сабақ'; + + @override + String studyChapterX(String param) { + return '$param-ші бөлім'; + } + + @override + String get studyEmpty => 'Бос'; + + @override + String get studyStartFromInitialPosition => 'Басталуы: бастапқы күйден'; + + @override + String get studyEditor => 'Өңдеуші'; + + @override + String get studyStartFromCustomPosition => 'Басталуы: белгілі күйден'; + + @override + String get studyLoadAGameByUrl => 'Сілтеме арқылы ойындарды жүктеп салу'; + + @override + String get studyLoadAPositionFromFen => 'FEN арқылы ойындарды жүктеп салу'; + + @override + String get studyLoadAGameFromPgn => 'PGN арқылы ойындарды жүктеп салу'; + + @override + String get studyAutomatic => 'Автоматты түрде'; + + @override + String get studyUrlOfTheGame => 'Ойындардың сілтемесі, әр жолға бір-бірден'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 не $param2 ойындарын жүктеп салу'; + } + + @override + String get studyCreateChapter => 'Бөлім құру'; + + @override + String get studyCreateStudy => 'Зерттеуді құру'; + + @override + String get studyEditStudy => 'Зерттеуді өңдеу'; + + @override + String get studyVisibility => 'Көрінуі'; + + @override + String get studyPublic => 'Жалпыға ашық'; + + @override + String get studyUnlisted => 'Жасырын'; + + @override + String get studyInviteOnly => 'Шақырумен ғана'; + + @override + String get studyAllowCloning => 'Көшірмеге рұқсат беру'; + + @override + String get studyNobody => 'Ешкім'; + + @override + String get studyOnlyMe => 'Өзім ғана'; + + @override + String get studyContributors => 'Қолдаушылар'; + + @override + String get studyMembers => 'Мүшелер'; + + @override + String get studyEveryone => 'Барлығы'; + + @override + String get studyEnableSync => 'Үйлесуді қосу'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Иә: бәрі бірдей күйде болады'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Жоқ: бәріне еркін шолуға рұқсат ету'; + + @override + String get studyPinnedStudyComment => 'Қадаулы зерттеу пікірі'; + @override String get studyStart => 'Бастау'; + + @override + String get studySave => 'Сақтау'; + + @override + String get studyClearChat => 'Чатты өшіру'; + + @override + String get studyDeleteTheStudyChatHistory => 'Зерттеудің чат тарихын өшіресіз бе? Кері жол жоқ!'; + + @override + String get studyDeleteStudy => 'Зерттеуді жою'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Бүкіл зерттеуді жоясыз ба? Қайтар жол жоқ. Растау үшін зерттеу атауын жазыңыз: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Бұл күйдің зерттеуін қай жерде бастайсыз?'; + + @override + String get studyGoodMove => 'Жақсы жүріс'; + + @override + String get studyMistake => 'Қате'; + + @override + String get studyBrilliantMove => 'Әдемі жүріс'; + + @override + String get studyBlunder => 'Өрескел қателік'; + + @override + String get studyInterestingMove => 'Қызық жүріс'; + + @override + String get studyDubiousMove => 'Күмәнді жүріс'; + + @override + String get studyOnlyMove => 'Жалғыз жүріс'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Күйлері шамалас'; + + @override + String get studyUnclearPosition => 'Күйі анық емес'; + + @override + String get studyWhiteIsSlightlyBetter => 'Ақ сәл күштірек'; + + @override + String get studyBlackIsSlightlyBetter => 'Қара сәл күштірек'; + + @override + String get studyWhiteIsBetter => 'Ақтың жағдайы жақсы'; + + @override + String get studyBlackIsBetter => 'Қараның жағдайы жақсы'; + + @override + String get studyWhiteIsWinning => 'Ақ жеңеді'; + + @override + String get studyBlackIsWinning => 'Қара жеңеді'; + + @override + String get studyNovelty => 'Жаңашылдық'; + + @override + String get studyDevelopment => 'Дамыту'; + + @override + String get studyInitiative => 'Белсенді'; + + @override + String get studyAttack => 'Шабуыл'; + + @override + String get studyCounterplay => 'Қарсы шабуыл'; + + @override + String get studyTimeTrouble => 'Уақыт қаупі'; + + @override + String get studyWithCompensation => 'Өтеумен'; + + @override + String get studyWithTheIdea => 'Бір оймен'; + + @override + String get studyNextChapter => 'Келесі бөлім'; + + @override + String get studyPrevChapter => 'Алдыңғы бөлім'; + + @override + String get studyStudyActions => 'Зерттеу әрекеттері'; + + @override + String get studyTopics => 'Тақырыптар'; + + @override + String get studyMyTopics => 'Менің тақырыптарым'; + + @override + String get studyPopularTopics => 'Белгілі тақырыптар'; + + @override + String get studyManageTopics => 'Тақырыптарды басқару'; + + @override + String get studyBack => 'Кері қайту'; + + @override + String get studyPlayAgain => 'Қайта ойнау'; + + @override + String get studyWhatWouldYouPlay => 'Осы күйде не ойнамақсыз?'; + + @override + String get studyYouCompletedThisLesson => 'Құтты болсын! Сіз бұл сабақты бітірдіңіз.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count бөлім', + one: '$count бөлім', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ойын', + one: '$count ойын', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count мүше', + one: '$count мүше', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN мәтінін осында қойыңыз, $count ойынға дейін', + one: 'PGN мәтінін осында қойыңыз, $count ойын ғана', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'жаңа ғана'; + + @override + String get timeagoRightNow => 'дәл қазір'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count секундта', + one: '$count секундта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минутта', + one: '$count минутта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count сағатта', + one: '$count сағатта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count күннен кейін', + one: '$count күннен кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count аптадан кейін', + one: '$count аптадан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count айдан кейін', + one: '$count айдан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count жылдан кейін', + one: '$count жылдан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минут бұрын', + one: '$count минут бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count сағат бұрын', + one: '$count сағат бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count күн бұрын', + one: '$count күн бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count апта бұрын', + one: '$count апта бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ай бұрын', + one: '$count ай бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count жыл бұрын', + one: '$count жыл бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index df44e81b5d..fe23b91549 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); @override - String get mobileHomeTab => '홈'; + String get mobileAllGames => '모든 대국'; @override - String get mobilePuzzlesTab => '퍼즐'; + String get mobileAreYouSure => '확실하십니까?'; @override - String get mobileToolsTab => '도구'; + String get mobileCancelTakebackOffer => '무르기 요청 취소'; @override - String get mobileWatchTab => '관람'; + String get mobileClearButton => '지우기'; @override - String get mobileSettingsTab => '설정'; + String get mobileCorrespondenceClearSavedMove => '저장된 수 삭제'; @override - String get mobileMustBeLoggedIn => '이 페이지를 보려면 로그인해야 합니다.'; + String get mobileCustomGameJoinAGame => '게임 참가'; @override - String get mobileSystemColors => '시스템 색상'; + String get mobileFeedbackButton => '피드백'; @override - String get mobileFeedbackButton => '피드백'; + String mobileGreeting(String param) { + return '안녕하세요, $param'; + } @override - String get mobileOkButton => '확인'; + String get mobileGreetingWithoutName => '안녕하세요'; @override - String get mobileSettingsHapticFeedback => '터치 시 진동'; + String get mobileHideVariation => '바리에이션 숨기기'; @override - String get mobileSettingsImmersiveMode => '전체 화면 모드'; + String get mobileHomeTab => '홈'; @override - String get mobileSettingsImmersiveModeSubtitle => '플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.'; + String get mobileLiveStreamers => '방송 중인 스트리머'; @override - String get mobileNotFollowingAnyUser => '팔로우한 사용자가 없습니다.'; + String get mobileMustBeLoggedIn => '이 페이지를 보려면 로그인해야 합니다.'; @override - String get mobileAllGames => '모든 대국'; + String get mobileNoSearchResults => '결과 없음'; @override - String get mobileRecentSearches => '최근 검색어'; + String get mobileNotFollowingAnyUser => '팔로우한 사용자가 없습니다.'; @override - String get mobileClearButton => '지우기'; + String get mobileOkButton => '확인'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get mobileNoSearchResults => '결과 없음'; + String get mobilePrefMagnifyDraggedPiece => '드래그한 기물 확대하기'; @override - String get mobileAreYouSure => '확실하십니까?'; + String get mobilePuzzleStormConfirmEndRun => '이 도전을 종료하시겠습니까?'; @override - String get mobilePuzzleStreakAbortWarning => '현재 연속 해결 기록을 잃고 점수는 저장됩니다.'; + String get mobilePuzzleStormFilterNothingToShow => '표시할 것이 없습니다. 필터를 변경해 주세요'; @override String get mobilePuzzleStormNothingToShow => '표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.'; @override - String get mobileSharePuzzle => '이 퍼즐 공유'; + String get mobilePuzzleStormSubtitle => '3분 이내에 최대한 많은 퍼즐을 해결하십시오.'; @override - String get mobileShareGameURL => '게임 URL 공유'; + String get mobilePuzzleStreakAbortWarning => '현재 연속 해결 기록을 잃고 점수는 저장됩니다.'; @override - String get mobileShareGamePGN => 'PGN 공유'; + String get mobilePuzzleThemesSubtitle => '당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.'; @override - String get mobileSharePositionAsFEN => 'FEN으로 공유'; + String get mobilePuzzlesTab => '퍼즐'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => '최근 검색어'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => '터치 시 진동'; @override - String get mobileShowComments => '댓글 보기'; + String get mobileSettingsImmersiveMode => '전체 화면 모드'; @override - String get mobilePuzzleStormConfirmEndRun => '이 도전을 종료하시겠습니까?'; + String get mobileSettingsImmersiveModeSubtitle => '플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.'; @override - String get mobilePuzzleStormFilterNothingToShow => '표시할 것이 없습니다. 필터를 변경해 주세요'; + String get mobileSettingsTab => '설정'; @override - String get mobileCancelTakebackOffer => '무르기 요청 취소'; + String get mobileShareGamePGN => 'PGN 공유'; @override - String get mobileCancelDrawOffer => '무승부 요청 취소'; + String get mobileShareGameURL => '게임 URL 공유'; @override - String get mobileWaitingForOpponentToJoin => '상대 참가를 기다리는 중...'; + String get mobileSharePositionAsFEN => 'FEN으로 공유'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => '이 퍼즐 공유'; @override - String get mobileLiveStreamers => '방송 중인 스트리머'; + String get mobileShowComments => '댓글 보기'; @override - String get mobileCustomGameJoinAGame => '게임 참가'; + String get mobileShowResult => '결과 표시'; @override - String get mobileCorrespondenceClearSavedMove => '저장된 수 삭제'; + String get mobileShowVariations => '바리에이션 보이기'; @override String get mobileSomethingWentWrong => '문제가 발생했습니다.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => '시스템 색상'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => '테마'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => '도구'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => '상대 참가를 기다리는 중...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => '관람'; @override String get activityActivity => '활동'; @@ -163,7 +160,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 개월 동안 $param2 에서 lichess.org 을 후원하였습니다.', + other: '$count개월 동안 $param2으로 lichess.org를 후원함', ); return '$_temp0'; } @@ -173,7 +170,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$param2 에서 총 $count 개의 포지션을 연습하였습니다.', + other: '$param2에서 총 $count개의 포지션을 연습함', ); return '$_temp0'; } @@ -183,7 +180,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '전술 문제 $count 개를 해결하였습니다.', + other: '전술 문제 $count개를 해결함', ); return '$_temp0'; } @@ -213,7 +210,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 수를 둠', + other: '수 $count개를 둠', ); return '$_temp0'; } @@ -223,7 +220,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 통신전에서', + other: '$count개의 통신 대국에서', ); return '$_temp0'; } @@ -233,7 +230,17 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 번의 통신전을 완료하셨습니다.', + other: '$count번의 통신 대국을 완료함', + ); + return '$_temp0'; + } + + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count번의 $param2 통신 대국을 완료함', ); return '$_temp0'; } @@ -283,7 +290,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 건의 연구를 작성함', + other: '새 연구 $count개 작성함', ); return '$_temp0'; } @@ -331,9 +338,255 @@ class AppLocalizationsKo extends AppLocalizations { @override String get broadcastBroadcasts => '방송'; + @override + String get broadcastMyBroadcasts => '내 방송'; + @override String get broadcastLiveBroadcasts => '실시간 대회 방송'; + @override + String get broadcastBroadcastCalendar => '방송 달력'; + + @override + String get broadcastNewBroadcast => '새 실시간 방송'; + + @override + String get broadcastSubscribedBroadcasts => '구독 중인 방송'; + + @override + String get broadcastAboutBroadcasts => '방송에 대해서'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Lichess 방송을 사용하는 방법.'; + + @override + String get broadcastTheNewRoundHelp => '새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.'; + + @override + String get broadcastAddRound => '라운드 추가'; + + @override + String get broadcastOngoing => '진행중'; + + @override + String get broadcastUpcoming => '방영 예정'; + + @override + String get broadcastCompleted => '종료됨'; + + @override + String get broadcastCompletedHelp => 'Lichess는 경기 완료를 감지하지만, 잘못될 때가 있을 수 있습니다. 수동으로 설정하기 위해 이걸 사용하세요.'; + + @override + String get broadcastRoundName => '라운드 이름'; + + @override + String get broadcastRoundNumber => '라운드 숫자'; + + @override + String get broadcastTournamentName => '토너먼트 이름'; + + @override + String get broadcastTournamentDescription => '짧은 토너먼트 설명'; + + @override + String get broadcastFullDescription => '전체 이벤트 설명'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '(선택) 방송에 대한 긴 설명입니다. $param1 사용이 가능합니다. 길이는 $param2 글자보다 짧아야 합니다.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess가 PGN 업데이트를 받기 위해 확인할 URL입니다. 인터넷에서 공개적으로 액세스 할 수 있어야 합니다.'; + + @override + String get broadcastSourceGameIds => '공간으로 나눠진 64개까지의 Lichess 경기 ID.'; + + @override + String broadcastStartDateTimeZone(String param) { + return '내 시간대의 토너먼트 시작 날짜: $param'; + } + + @override + String get broadcastStartDateHelp => '선택 사항, 언제 이벤트가 시작되는지 알고 있는 경우'; + + @override + String get broadcastCurrentGameUrl => '현재 게임 URL'; + + @override + String get broadcastDownloadAllRounds => '모든 라운드 다운로드받기'; + + @override + String get broadcastResetRound => '라운드 초기화'; + + @override + String get broadcastDeleteRound => '라운드 삭제'; + + @override + String get broadcastDefinitivelyDeleteRound => '라운드와 해당 게임을 완전히 삭제합니다.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => '이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.'; + + @override + String get broadcastEditRoundStudy => '경기 연구 편집'; + + @override + String get broadcastDeleteTournament => '이 토너먼트 삭제'; + + @override + String get broadcastDefinitivelyDeleteTournament => '토너먼트 전체의 모든 라운드와 게임을 완전히 삭제합니다.'; + + @override + String get broadcastShowScores => '게임 결과에 따라 플레이어 점수 표시'; + + @override + String get broadcastReplacePlayerTags => '선택 사항: 플레이어 이름, 레이팅 및 타이틀 바꾸기'; + + @override + String get broadcastFideFederations => 'FIDE 연맹'; + + @override + String get broadcastTop10Rating => 'Top 10 레이팅'; + + @override + String get broadcastFidePlayers => 'FIDE 선수들'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE 선수 찾지 못함'; + + @override + String get broadcastFideProfile => 'FIDE 프로필'; + + @override + String get broadcastFederation => '연맹'; + + @override + String get broadcastAgeThisYear => '올해 나이'; + + @override + String get broadcastUnrated => '비레이팅'; + + @override + String get broadcastRecentTournaments => '최근 토너먼트'; + + @override + String get broadcastOpenLichess => 'Lichess에서 열기'; + + @override + String get broadcastTeams => '팀'; + + @override + String get broadcastBoards => '보드'; + + @override + String get broadcastOverview => '개요'; + + @override + String get broadcastSubscribeTitle => '라운드가 시작될 때 알림을 받으려면 구독하세요. 계정 설정에서 방송을 위한 벨이나 알림 푸시를 토글할 수 있습니다.'; + + @override + String get broadcastUploadImage => '토너먼트 사진 업로드'; + + @override + String get broadcastNoBoardsYet => '아직 보드가 없습니다. 게임들이 업로드되면 나타납니다.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return '보드들은 소스나 $param(으)로 로드될 수 있습니다'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param 후 시작'; + } + + @override + String get broadcastStartVerySoon => '방송이 곧 시작됩니다.'; + + @override + String get broadcastNotYetStarted => '아직 방송이 시작을 하지 않았습니다.'; + + @override + String get broadcastOfficialWebsite => '공식 웹사이트'; + + @override + String get broadcastStandings => '순위'; + + @override + String get broadcastOfficialStandings => '공식 순위'; + + @override + String broadcastIframeHelp(String param) { + return '$param에서 더 많은 정보를 확인하실 수 있습니다'; + } + + @override + String get broadcastWebmastersPage => '웹마스터 페이지'; + + @override + String broadcastPgnSourceHelp(String param) { + return '이 라운드의 공개된, 실시간 PGN 소스 입니다. 보다 더 빠르고 효율적인 동기화를 위해 $param도 제공됩니다.'; + } + + @override + String get broadcastEmbedThisBroadcast => '이 방송을 웹사이트에 삽입하세요'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param을(를) 웹사이트에 삽입하세요'; + } + + @override + String get broadcastRatingDiff => '레이팅 차이'; + + @override + String get broadcastGamesThisTournament => '이 토너먼트의 게임들'; + + @override + String get broadcastScore => '점수'; + + @override + String get broadcastAllTeams => '모든 팀'; + + @override + String get broadcastTournamentFormat => '토너먼트 형식'; + + @override + String get broadcastTournamentLocation => '토너먼트 장소'; + + @override + String get broadcastTopPlayers => '상위 플레이어들'; + + @override + String get broadcastTimezone => '시간대'; + + @override + String get broadcastFideRatingCategory => 'FIDE 레이팅 범주'; + + @override + String get broadcastOptionalDetails => '선택적 세부 정보'; + + @override + String get broadcastPastBroadcasts => '과거 방송들'; + + @override + String get broadcastAllBroadcastsByMonth => '월별 방송들 모두 보기'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 방송', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '도전: $param1'; @@ -386,13 +639,13 @@ class AppLocalizationsKo extends AppLocalizations { String get challengeDeclineLater => '시간이 맞지 않습니다. 나중에 다시 요청해주세요.'; @override - String get challengeDeclineTooFast => '시간이 너무 짧습니다. 더 긴 게임으로 신청해주세요.'; + String get challengeDeclineTooFast => '시간이 너무 짧습니다. 더 느린 게임으로 신청해주세요.'; @override - String get challengeDeclineTooSlow => '시간이 너무 깁니다. 더 빠른 게임으로 신청해주세요.'; + String get challengeDeclineTooSlow => '시간이 너무 깁니다. 더 빠른 게임으로 다시 신청해주세요.'; @override - String get challengeDeclineTimeControl => '이 시간으로는 도전을 받지 않습니다.'; + String get challengeDeclineTimeControl => '이 시간 제한으로는 도전을 받지 않겠습니다.'; @override String get challengeDeclineRated => '대신 레이팅 대전을 신청해주세요.'; @@ -457,7 +710,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get perfStatTotalGames => '총 게임'; + String get perfStatTotalGames => '총 대국'; @override String get perfStatRatedGames => '레이팅 게임'; @@ -472,13 +725,13 @@ class AppLocalizationsKo extends AppLocalizations { String get perfStatTimeSpentPlaying => '플레이한 시간'; @override - String get perfStatAverageOpponent => '상대의 평균 레이팅'; + String get perfStatAverageOpponent => '상대 평균'; @override - String get perfStatVictories => '승리'; + String get perfStatVictories => '승'; @override - String get perfStatDefeats => '패배'; + String get perfStatDefeats => '패'; @override String get perfStatDisconnections => '연결 끊김'; @@ -498,7 +751,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String perfStatFromXToY(String param1, String param2) { - return '$param1에서 $param2'; + return '$param1에서 $param2까지'; } @override @@ -518,10 +771,10 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get perfStatBestRated => '승리한 최고 레이팅'; + String get perfStatBestRated => '최고 레이팅 승리'; @override - String get perfStatGamesInARow => '연속 게임 플레이'; + String get perfStatGamesInARow => '연속 대국'; @override String get perfStatLessThanOneHour => '게임 사이가 1시간 미만인 경우'; @@ -539,10 +792,10 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesDisplay => '화면'; @override - String get preferencesPrivacy => '프라이버시'; + String get preferencesPrivacy => '보안'; @override - String get preferencesNotifications => '공지 사항'; + String get preferencesNotifications => '알림'; @override String get preferencesPieceAnimation => '기물 움직임 애니메이션'; @@ -560,13 +813,13 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesBoardCoordinates => '보드 좌표 (A-H, 1-8)'; @override - String get preferencesMoveListWhilePlaying => '피스 움직임 기록'; + String get preferencesMoveListWhilePlaying => '기물 움직임 기록'; @override String get preferencesPgnPieceNotation => 'PGN 기물표기방식'; @override - String get preferencesChessPieceSymbol => '체스 말 기호'; + String get preferencesChessPieceSymbol => '체스 기물 기호'; @override String get preferencesPgnLetter => '알파벳 (K, Q, R, B, N)'; @@ -578,7 +831,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesShowPlayerRatings => '플레이어 레이팅 보기'; @override - String get preferencesShowFlairs => '플레이어 레이팅 보기'; + String get preferencesShowFlairs => '플레이어 아이콘 보기'; @override String get preferencesExplainShowPlayerRatings => '체스에 집중할 수 있도록 웹사이트에서 레이팅을 모두 숨깁니다. 경기는 여전히 레이팅에 반영될 것이며, 눈으로 보이는 정보에만 영향을 줍니다.'; @@ -592,6 +845,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get preferencesInGameOnly => '게임 도중에만 적용'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '체스 시계'; @@ -626,7 +882,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesBothClicksAndDrag => '아무 방법으로'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => '미리두기 (상대 턴일 때 수를 두기)'; + String get preferencesPremovesPlayingDuringOpponentTurn => '미리두기 (상대 차례일 때 수를 두기)'; @override String get preferencesTakebacksWithOpponentApproval => '무르기 (상대 승인과 함께)'; @@ -641,7 +897,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesExplainPromoteToQueenAutomatically => '일시적으로 자동 승진을 끄기 위해 승진하는 동안 를 누르세요'; @override - String get preferencesWhenPremoving => '미리둘 때만'; + String get preferencesWhenPremoving => '미리두기 때만'; @override String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '3회 동형반복시 자동으로 무승부 요청'; @@ -650,16 +906,16 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesWhenTimeRemainingLessThanThirtySeconds => '남은 시간이 30초 미만일 때만'; @override - String get preferencesMoveConfirmation => '피스를 움직이기 전에 물음'; + String get preferencesMoveConfirmation => '수 확인'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; + String get preferencesExplainCanThenBeTemporarilyDisabled => '경기 도중 보드 메뉴에서 비활성화될 수 있습니다.'; @override - String get preferencesInCorrespondenceGames => '긴 대국에서만'; + String get preferencesInCorrespondenceGames => '통신 대국'; @override - String get preferencesCorrespondenceAndUnlimited => '긴 대국과 무제한'; + String get preferencesCorrespondenceAndUnlimited => '통신 대국과 무제한'; @override String get preferencesConfirmResignationAndDrawOffers => '기권 또는 무승부 제안시 물음'; @@ -668,7 +924,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '캐슬링 방법'; @override - String get preferencesCastleByMovingTwoSquares => '왕을 2칸 옮기기'; + String get preferencesCastleByMovingTwoSquares => '킹을 2칸 옮기기'; @override String get preferencesCastleByMovingOntoTheRook => '킹을 룩한테 이동'; @@ -704,10 +960,10 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyForumMention => '포럼 댓글에서 당신이 언급됨'; @override - String get preferencesNotifyInvitedStudy => '스터디 초대'; + String get preferencesNotifyInvitedStudy => '연구 초대'; @override - String get preferencesNotifyGameEvent => '긴 대국 업데이트'; + String get preferencesNotifyGameEvent => '통신 대국 업데이트'; @override String get preferencesNotifyChallenge => '도전 과제'; @@ -716,13 +972,13 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyTournamentSoon => '곧 토너먼트 시작할 때'; @override - String get preferencesNotifyTimeAlarm => '긴 대국 시간 초과'; + String get preferencesNotifyTimeAlarm => '통신 대국 시간 곧 만료됨'; @override - String get preferencesNotifyBell => '리체스 내에서 벨 알림'; + String get preferencesNotifyBell => 'Lichess 내에서 벨 알림'; @override - String get preferencesNotifyPush => '리체스를 사용하지 않을 때 기기 알림'; + String get preferencesNotifyPush => 'Lichess를 사용하지 않을 때 기기 알림'; @override String get preferencesNotifyWeb => '브라우저'; @@ -733,6 +989,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get preferencesBellNotificationSound => '벨 알림 음'; + @override + String get preferencesBlindfold => '기물 가리기'; + @override String get puzzlePuzzles => '퍼즐'; @@ -779,13 +1038,13 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleDownVote => '퍼즐 비추천'; @override - String get puzzleYourPuzzleRatingWillNotChange => '당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 스킬에 맞는 퍼즐을 선택하도록 돕습니다.'; + String get puzzleYourPuzzleRatingWillNotChange => '당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.'; @override - String get puzzleFindTheBestMoveForWhite => '백의 최고의 수를 찾아보세요.'; + String get puzzleFindTheBestMoveForWhite => '백의 최선 수를 찾아보세요.'; @override - String get puzzleFindTheBestMoveForBlack => '흑의 최고의 수를 찾아보세요.'; + String get puzzleFindTheBestMoveForBlack => '흑의 최선 수를 찾아보세요.'; @override String get puzzleToGetPersonalizedPuzzles => '개인화된 퍼즐을 위해선:'; @@ -835,7 +1094,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleUseCtrlF => 'Ctrl+f를 사용해서 가장 좋아하는 오프닝을 찾으세요!'; @override - String get puzzleNotTheMove => '답이 아닙니다!'; + String get puzzleNotTheMove => '그 수가 아닙니다!'; @override String get puzzleTrySomethingElse => '다른 것 시도하기'; @@ -1119,7 +1378,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleThemeDefensiveMoveDescription => '기물을 잃거나 다른 손실을 피하기 위해 필요한 정확한 수입니다.'; @override - String get puzzleThemeDeflection => '유인'; + String get puzzleThemeDeflection => '굴절'; @override String get puzzleThemeDeflectionDescription => '중요한 칸을 수비하는 등 다른 역할을 수행하는 상대 기물의 주의를 분산시키는 수입니다. \"과부하\"라고도 불립니다.'; @@ -1368,10 +1627,10 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleThemeZugzwangDescription => '상대가 둘 수 있는 수는 제한되어 있으며, 모든 수가 포지션을 악화시킵니다.'; @override - String get puzzleThemeHealthyMix => '골고루 섞기'; + String get puzzleThemeMix => '골고루 섞기'; @override - String get puzzleThemeHealthyMixDescription => '전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.'; + String get puzzleThemeMixDescription => '전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.'; @override String get puzzleThemePlayerGames => '플레이어 게임'; @@ -1415,13 +1674,13 @@ class AppLocalizationsKo extends AppLocalizations { String get playWithAFriend => '친구와 게임하기'; @override - String get playWithTheMachine => '체스 엔진과 게임하기'; + String get playWithTheMachine => '체스 엔진과 붙기'; @override String get toInviteSomeoneToPlayGiveThisUrl => '이 URL로 친구를 초대하세요'; @override - String get gameOver => '게임 종료'; + String get gameOver => '게임 오버'; @override String get waitingForOpponent => '상대를 기다리는 중'; @@ -1462,10 +1721,10 @@ class AppLocalizationsKo extends AppLocalizations { String get stalemate => '스테일메이트'; @override - String get white => '백색'; + String get white => '백'; @override - String get black => '흑색'; + String get black => '흑'; @override String get asWhite => '백일때'; @@ -1516,22 +1775,22 @@ class AppLocalizationsKo extends AppLocalizations { String get yourOpponentWantsToPlayANewGameWithYou => '상대가 재대결을 원합니다'; @override - String get joinTheGame => '게임 참가'; + String get joinTheGame => '대국 참가'; @override - String get whitePlays => '백색 차례'; + String get whitePlays => '백 차례'; @override - String get blackPlays => '흑색 차례'; + String get blackPlays => '흑 차례'; @override String get opponentLeftChoices => '당신의 상대가 게임을 나갔습니다. 상대를 기다리거나 승리 또는 무승부 처리할 수 있습니다.'; @override - String get forceResignation => '승리 처리'; + String get forceResignation => '승리 취하기'; @override - String get forceDraw => '무승부 처리'; + String get forceDraw => '무승부 선언'; @override String get talkInChat => '건전한 채팅을 해주세요!'; @@ -1540,34 +1799,34 @@ class AppLocalizationsKo extends AppLocalizations { String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.'; @override - String get whiteResigned => '백색 기권'; + String get whiteResigned => '백이 기권하였습니다'; @override - String get blackResigned => '흑색 기권'; + String get blackResigned => '흑이 기권하였습니다'; @override - String get whiteLeftTheGame => '백색이 게임을 나갔습니다'; + String get whiteLeftTheGame => '백이 게임을 나갔습니다'; @override - String get blackLeftTheGame => '흑색이 게임을 나갔습니다'; + String get blackLeftTheGame => '흑이 게임을 나갔습니다'; @override - String get whiteDidntMove => '백이 두지 않음'; + String get whiteDidntMove => '백이 수를 두지 않음'; @override - String get blackDidntMove => '흑이 두지 않음'; + String get blackDidntMove => '흑이 수를 두지 않음'; @override - String get requestAComputerAnalysis => '컴퓨터 분석 요청'; + String get requestAComputerAnalysis => '컴퓨터 분석 요청하기'; @override String get computerAnalysis => '컴퓨터 분석'; @override - String get computerAnalysisAvailable => '컴퓨터 분석이 가능합니다.'; + String get computerAnalysisAvailable => '컴퓨터 분석 가능'; @override - String get computerAnalysisDisabled => '컴퓨터 분석 꺼짐'; + String get computerAnalysisDisabled => '컴퓨터 분석 비활성화됨'; @override String get analysis => '분석'; @@ -1581,13 +1840,13 @@ class AppLocalizationsKo extends AppLocalizations { String get usingServerAnalysis => '서버 분석 사용하기'; @override - String get loadingEngine => '엔진 로드 중 ...'; + String get loadingEngine => '엔진 로드 중...'; @override String get calculatingMoves => '수 계산 중...'; @override - String get engineFailed => '엔진 로딩 에러'; + String get engineFailed => '엔진 불러오는 도중 오류 발생'; @override String get cloudAnalysis => '클라우드 분석'; @@ -1596,16 +1855,16 @@ class AppLocalizationsKo extends AppLocalizations { String get goDeeper => '더 깊게 분석하기'; @override - String get showThreat => '위험요소 표시하기'; + String get showThreat => '위험요소 표시'; @override String get inLocalBrowser => '브라우저에서'; @override - String get toggleLocalEvaluation => '개인 컴퓨터에서 분석하기'; + String get toggleLocalEvaluation => '로컬 분석 전환'; @override - String get promoteVariation => '게임 분석 후에 어떤 수에 대한 예상결과들을 확인하고 싶다면'; + String get promoteVariation => '바리에이션 승격하기'; @override String get makeMainLine => '주 라인으로 하기'; @@ -1614,16 +1873,16 @@ class AppLocalizationsKo extends AppLocalizations { String get deleteFromHere => '여기서부터 삭제'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => '바리에이션 축소하기'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => '바리에이션 확장하기'; @override - String get forceVariation => '변화 강제하기'; + String get forceVariation => '바리에이션 강제하기'; @override - String get copyVariationPgn => '변동 PGN 복사'; + String get copyVariationPgn => '바리에이션 PGN 복사'; @override String get move => '수'; @@ -1635,13 +1894,13 @@ class AppLocalizationsKo extends AppLocalizations { String get variantWin => '변형 체스에서 승리'; @override - String get insufficientMaterial => '기물 부족으로 무승부입니다.'; + String get insufficientMaterial => '기물 부족'; @override String get pawnMove => '폰 이동'; @override - String get capture => 'Capture'; + String get capture => '기물 잡기'; @override String get close => '닫기'; @@ -1662,7 +1921,7 @@ class AppLocalizationsKo extends AppLocalizations { String get database => '데이터베이스'; @override - String get whiteDrawBlack => '백 : 무승부 : 흑'; + String get whiteDrawBlack => '백 / 무승부 / 흑'; @override String averageRatingX(String param) { @@ -1684,7 +1943,7 @@ class AppLocalizationsKo extends AppLocalizations { String get dtzWithRounding => '다음 포획 혹은 폰 수까지 남은 반수를 반올림후 나타낸 DTZ50\" 수치'; @override - String get noGameFound => '게임을 찾을 수 없습니다.'; + String get noGameFound => '게임을 찾을 수 없습니다'; @override String get maxDepthReached => '최대 깊이 도달!'; @@ -1710,10 +1969,10 @@ class AppLocalizationsKo extends AppLocalizations { String get playFirstOpeningEndgameExplorerMove => '첫 번째 오프닝/엔드게임 탐색기 수 두기'; @override - String get winPreventedBy50MoveRule => '50수 규칙에 의하여 승리가 불가능합니다.'; + String get winPreventedBy50MoveRule => '50수 규칙에 의하여 승리가 불가능합니다'; @override - String get lossSavedBy50MoveRule => '50수 규칙에 의하여 패배가 불가능합니다.'; + String get lossSavedBy50MoveRule => '50수 규칙에 의하여 패배가 불가능합니다'; @override String get winOr50MovesByPriorMistake => '승리 혹은 이전의 실수로 인한 50수 규칙 무승부'; @@ -1725,7 +1984,7 @@ class AppLocalizationsKo extends AppLocalizations { String get unknownDueToRounding => 'DTZ 수치의 반올림 때문에 추천된 테이블베이스 라인을 따라야만 승리 및 패배가 보장됩니다.'; @override - String get allSet => '모든 설정 완료!'; + String get allSet => '모두 완료!'; @override String get importPgn => 'PGN 가져오기'; @@ -1743,10 +2002,7 @@ class AppLocalizationsKo extends AppLocalizations { String get realtimeReplay => '실시간'; @override - String get byCPL => '평가치변화'; - - @override - String get openStudy => '연구를 시작하기'; + String get byCPL => '센티폰 손실'; @override String get enable => '활성화'; @@ -1758,10 +2014,10 @@ class AppLocalizationsKo extends AppLocalizations { String get showVariationArrows => '바리에이션 화살표 표시하기'; @override - String get evaluationGauge => '평가치 게이지'; + String get evaluationGauge => '평가 게이지'; @override - String get multipleLines => '다중 분석 수'; + String get multipleLines => '다중 라인 수'; @override String get cpus => 'CPU 수'; @@ -1776,16 +2032,13 @@ class AppLocalizationsKo extends AppLocalizations { String get removesTheDepthLimit => '탐색 깊이 제한을 없애고 컴퓨터를 따뜻하게 해줍니다'; @override - String get engineManager => '엔진 매니저'; - - @override - String get blunder => '심각한 실수'; + String get blunder => '블런더'; @override String get mistake => '실수'; @override - String get inaccuracy => '사소한 실수'; + String get inaccuracy => '부정확한 수'; @override String get moveTimes => '이동 시간'; @@ -1809,10 +2062,10 @@ class AppLocalizationsKo extends AppLocalizations { String get drawByMutualAgreement => '상호 동의에 의한 무승부'; @override - String get fiftyMovesWithoutProgress => '진전이 없이 50수 소모'; + String get fiftyMovesWithoutProgress => '진전 없이 50수 소모'; @override - String get currentGames => '진행 중인 게임'; + String get currentGames => '진행 중인 게임들'; @override String get viewInFullSize => '크게 보기'; @@ -1827,13 +2080,13 @@ class AppLocalizationsKo extends AppLocalizations { String get rememberMe => '로그인 유지'; @override - String get youNeedAnAccountToDoThat => '회원만이 접근할 수 있습니다.'; + String get youNeedAnAccountToDoThat => '회원이어야 가능합니다'; @override String get signUp => '회원 가입'; @override - String get computersAreNotAllowedToPlay => '컴퓨터나 컴퓨터의 도움을 받는 플레이어는 대국이 금지되어 있습니다. 대국할 때 체스 엔진이나 관련 자료, 또는 주변 플레이어로부터 도움을 받지 마십시오. 또한, 다중 계정 사용은 권장하지 않으며 지나치게 많은 다중 계정을 사용할 시 계정이 차단될 수 있습니다.'; + String get computersAreNotAllowedToPlay => '컴퓨터나 컴퓨터 지원을 받는 플레이어들은 게임 참가가 금지되어 있습니다. 게임 중 체스 엔진이나, 데이터베이스나, 주변 플레이어들로부터 도움을 받지 마십시오. 이와 더불어 다중 계정 소유는 권장하지 않으며 지나치게 많은 계정들을 사용할 시 계정들이 차단될 수 있습니다.'; @override String get games => '게임'; @@ -1843,7 +2096,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String xPostedInForumY(String param1, String param2) { - return '$param1(이)가 $param2 쓰레드에 글을 씀'; + return '$param1(이)가 $param2 주제에 글을 씀'; } @override @@ -1856,10 +2109,10 @@ class AppLocalizationsKo extends AppLocalizations { String get friends => '친구들'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => '다른 플레이어들'; @override - String get discussions => '토론'; + String get discussions => '대화'; @override String get today => '오늘'; @@ -1868,7 +2121,7 @@ class AppLocalizationsKo extends AppLocalizations { String get yesterday => '어제'; @override - String get minutesPerSide => '주어진 시간(분)'; + String get minutesPerSide => '제한 시간(분)'; @override String get variant => '게임 종류'; @@ -1880,13 +2133,13 @@ class AppLocalizationsKo extends AppLocalizations { String get timeControl => '시간 제한'; @override - String get realTime => '짧은 대국'; + String get realTime => '실시간'; @override - String get correspondence => '긴 대국'; + String get correspondence => '통신 대국'; @override - String get daysPerTurn => '한 수에 걸리는 일수'; + String get daysPerTurn => '수당 일수'; @override String get oneDay => '1일'; @@ -1901,19 +2154,19 @@ class AppLocalizationsKo extends AppLocalizations { String get ratingStats => '레이팅 통계'; @override - String get username => '아이디'; + String get username => '사용자 이름'; @override String get usernameOrEmail => '사용자 이름이나 이메일 주소'; @override - String get changeUsername => '사용자명 변경'; + String get changeUsername => '사용자 이름 변경'; @override - String get changeUsernameNotSame => '글자의 대소문자 변경만 가능합니다 예: \"johndoe\" to \"JohnDoe\".'; + String get changeUsernameNotSame => '글자의 대소문자 변경만 가능합니다 예: \"chulsoo\"에서 \"ChulSoo\"로.'; @override - String get changeUsernameDescription => '닉네임 변경하기: 대/소문자의 변경만이 허용되며, 단 한번만 가능한 작업입니다.'; + String get changeUsernameDescription => '사용자명 변경하기: 대/소문자만의 변경이 허용되며, 단 한번만 가능합니다.'; @override String get signupUsernameHint => '사용자 이름이 어린이를 포함해 모두에게 적절한지 확인하세요. 나중에 변경할 수 없으며 부적절한 사용자 이름을 가진 계정은 폐쇄됩니다!'; @@ -1928,10 +2181,10 @@ class AppLocalizationsKo extends AppLocalizations { String get changePassword => '비밀번호 변경'; @override - String get changeEmail => '메일 주소 변경'; + String get changeEmail => '이메일 주소 변경'; @override - String get email => '메일'; + String get email => '이메일'; @override String get passwordReset => '비밀번호 초기화'; @@ -1940,23 +2193,23 @@ class AppLocalizationsKo extends AppLocalizations { String get forgotPassword => '비밀번호를 잊어버리셨나요?'; @override - String get error_weakPassword => '이 비밀번호는 매우 일반적이고 추측하기 쉽습니다.'; + String get error_weakPassword => '이 비밀번호는 매우 흔하며 추측하기 쉽습니다.'; @override - String get error_namePassword => '사용자 아이디를 비밀번호로 사용하지 마세요.'; + String get error_namePassword => '사용자 이름을 비밀번호로 사용하지 마세요.'; @override - String get blankedPassword => '다른 사이트에서 동일한 비밀번호를 사용했으며 해당 사이트가 유출된 경우. 라이선스 계정의 안전을 위해 새 비밀번호를 설정해 주셔야 합니다. 양해해 주셔서 감사합니다.'; + String get blankedPassword => '당신이 다른 사이트에서 동일한 비밀번호를 사용했으며, 해당 사이트가 유출되었습니다. Lichess 계정의 안전을 위해 새 비밀번호를 설정해 주세요. 양해해 주셔서 감사합니다.'; @override - String get youAreLeavingLichess => '리체스에서 나갑니다'; + String get youAreLeavingLichess => 'Lichess에서 나갑니다'; @override - String get neverTypeYourPassword => '다른 사이트에서는 절대로 리체스 비밀번호를 입력하지 마세요!'; + String get neverTypeYourPassword => '다른 사이트에서는 절대로 Lichess 비밀번호를 입력하지 마세요!'; @override String proceedToX(String param) { - return '$param 진행'; + return '$param로 진행'; } @override @@ -1984,24 +2237,24 @@ class AppLocalizationsKo extends AppLocalizations { @override String emailSent(String param) { - return '$param로 이메일을 전송했습니다.'; + return '$param(으)로 이메일을 전송했습니다.'; } @override - String get emailCanTakeSomeTime => '도착하는데 시간이 걸릴 수 있습니다.'; + String get emailCanTakeSomeTime => '이메일이 도착하는데 시간이 좀 걸릴 수 있습니다.'; @override - String get refreshInboxAfterFiveMinutes => '5분 가량 기다린 후 이메일 수신함을 새로고침하세요.'; + String get refreshInboxAfterFiveMinutes => '5분 가량 기다린 후 이메일 수신함을 새로고침 해주세요.'; @override - String get checkSpamFolder => '또한 스펨메일함을 확인해주시고 스펨을 해제해주세요.'; + String get checkSpamFolder => '또한 스팸 메일함에 들어가 있을 수 있습니다. 만약 그런 경우, 스팸이 아님으로 표시해 두세요.'; @override - String get emailForSignupHelp => '모두 실패했다면 이곳으로 메일을 보내주세요:'; + String get emailForSignupHelp => '모두 실패했다면, 이곳으로 메일을 보내주세요:'; @override String copyTextToEmail(String param) { - return '위의 텍스트를 복사해서 $param로 보내주세요.'; + return '위의 텍스트를 복사해서 $param(으)로 보내주세요'; } @override @@ -2009,12 +2262,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String accountConfirmed(String param) { - return '$param 사용자가 성공적으로 확인되었습니다.'; + return '유저 $param(이)가 성공적으로 확인되었습니다.'; } @override String accountCanLogin(String param) { - return '이제 $param로 로그인할 수 있습니다.'; + return '이제 $param(으)로 로그인할 수 있습니다.'; } @override @@ -2022,7 +2275,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String accountClosed(String param) { - return '$param 계정은 폐쇄되었습니다.'; + return '계정 $param(은)는 폐쇄되었습니다.'; } @override @@ -2041,53 +2294,56 @@ class AppLocalizationsKo extends AppLocalizations { @override String get gamesPlayed => '게임'; + @override + String get ok => '확인'; + @override String get cancel => '취소'; @override - String get whiteTimeOut => '백색 시간 초과'; + String get whiteTimeOut => '백 시간 초과'; @override - String get blackTimeOut => '흑색 시간 초과'; + String get blackTimeOut => '흑 시간 초과'; @override - String get drawOfferSent => '무승부를 요청했습니다'; + String get drawOfferSent => '무승부 요청함'; @override - String get drawOfferAccepted => '무승부 요청이 승낙됐습니다'; + String get drawOfferAccepted => '무승부 요청 수락됨'; @override - String get drawOfferCanceled => '무승부 요청을 취소했습니다'; + String get drawOfferCanceled => '무승부 요청 취소함'; @override - String get whiteOffersDraw => '백이 무승부를 제안했습니다'; + String get whiteOffersDraw => '백이 무승부를 제안합니다'; @override - String get blackOffersDraw => '흑이 무승부를 제안했습니다'; + String get blackOffersDraw => '흑이 무승부를 제안합니다'; @override - String get whiteDeclinesDraw => '백이 무승부 제안을 거절했습니다'; + String get whiteDeclinesDraw => '백이 무승부 제안을 거절하였습니다'; @override - String get blackDeclinesDraw => '흑이 무승부 제안을 거절했습니다'; + String get blackDeclinesDraw => '흑이 무승부 제안을 거절하였습니다'; @override - String get yourOpponentOffersADraw => '상대가 무승부를 요청했습니다'; + String get yourOpponentOffersADraw => '상대가 무승부를 요청합니다'; @override - String get accept => '승낙'; + String get accept => '수락'; @override String get decline => '거절'; @override - String get playingRightNow => '대국 중'; + String get playingRightNow => '지금 대국 중'; @override String get eventInProgress => '지금 대국 중'; @override - String get finished => '종료'; + String get finished => '종료됨'; @override String get abortGame => '게임 중단'; @@ -2096,10 +2352,10 @@ class AppLocalizationsKo extends AppLocalizations { String get gameAborted => '게임 중단됨'; @override - String get standard => '표준'; + String get standard => '스탠다드'; @override - String get customPosition => '사용자 지정 포지션'; + String get customPosition => '커스텀 포지션'; @override String get unlimited => '무제한'; @@ -2114,7 +2370,7 @@ class AppLocalizationsKo extends AppLocalizations { String get rated => '레이팅'; @override - String get casualTournament => '일반'; + String get casualTournament => '캐주얼'; @override String get ratedTournament => '레이팅'; @@ -2126,13 +2382,13 @@ class AppLocalizationsKo extends AppLocalizations { String get rematch => '재대결'; @override - String get rematchOfferSent => '재대결 요청을 보냈습니다'; + String get rematchOfferSent => '재대결 요청 전송됨'; @override - String get rematchOfferAccepted => '재대결 요청이 승낙됐습니다'; + String get rematchOfferAccepted => '재대결 요청 승낙됨'; @override - String get rematchOfferCanceled => '재대결 요청이 취소됐습니다'; + String get rematchOfferCanceled => '재대결 요청 취소됨'; @override String get rematchOfferDeclined => '재대결 요청이 거절됐습니다'; @@ -2141,7 +2397,7 @@ class AppLocalizationsKo extends AppLocalizations { String get cancelRematchOffer => '재대결 요청 취소'; @override - String get viewRematch => '재대결 보러 가기'; + String get viewRematch => '재대결 보기'; @override String get confirmMove => '수 확인'; @@ -2150,16 +2406,16 @@ class AppLocalizationsKo extends AppLocalizations { String get play => '플레이'; @override - String get inbox => '받은편지함'; + String get inbox => '편지함'; @override String get chatRoom => '채팅'; @override - String get loginToChat => '채팅에 로그인하기'; + String get loginToChat => '채팅하려면 로그인하세요'; @override - String get youHaveBeenTimedOut => '채팅에서 로그아웃 되었습니다.'; + String get youHaveBeenTimedOut => '채팅에서 타임아웃 되었습니다.'; @override String get spectatorRoom => '관전자 채팅'; @@ -2174,7 +2430,7 @@ class AppLocalizationsKo extends AppLocalizations { String get send => '전송'; @override - String get incrementInSeconds => '턴 당 추가 시간(초)'; + String get incrementInSeconds => '수 당 추가 시간(초)'; @override String get freeOnlineChess => '무료 온라인 체스'; @@ -2183,10 +2439,10 @@ class AppLocalizationsKo extends AppLocalizations { String get exportGames => '게임 내보내기'; @override - String get ratingRange => 'ELO 범위'; + String get ratingRange => '레이팅 범위'; @override - String get thisAccountViolatedTos => '이 계정은 Lichess 이용 약관을 위반했습니다.'; + String get thisAccountViolatedTos => '이 계정은 Lichess 이용 약관을 위반하였습니다'; @override String get openingExplorerAndTablebase => '오프닝 탐색 & 테이블베이스'; @@ -2195,19 +2451,19 @@ class AppLocalizationsKo extends AppLocalizations { String get takeback => '무르기'; @override - String get proposeATakeback => '무르기를 요청합니다'; + String get proposeATakeback => '무르기 요청'; @override - String get takebackPropositionSent => '무르기 요청을 보냈습니다'; + String get takebackPropositionSent => '무르기 요청 전송됨'; @override - String get takebackPropositionDeclined => '무르기 요청이 거절됐습니다'; + String get takebackPropositionDeclined => '무르기 요청 거절됨'; @override - String get takebackPropositionAccepted => '무르기 요청이 승낙됐습니다'; + String get takebackPropositionAccepted => '무르기 요청 승낙됨'; @override - String get takebackPropositionCanceled => '무르기 요청이 취소됐습니다'; + String get takebackPropositionCanceled => '무르기 요청 취소됨'; @override String get yourOpponentProposesATakeback => '상대가 무르기를 요청합니다'; @@ -2304,16 +2560,16 @@ class AppLocalizationsKo extends AppLocalizations { String get averageElo => '평균 레이팅'; @override - String get location => '주소'; + String get location => '위치'; @override - String get filterGames => '필터'; + String get filterGames => '대국 필터'; @override String get reset => '초기화'; @override - String get apply => '적용'; + String get apply => '저장'; @override String get save => '저장하기'; @@ -2352,7 +2608,7 @@ class AppLocalizationsKo extends AppLocalizations { String get importGameExplanation => '게임의 PGN 을 붙여넣으면, 브라우저에서의 리플레이, 컴퓨터 해석, 게임챗, 공유가능 URL을 얻습니다.'; @override - String get importGameCaveat => '변형은 지워집니다. 변형을 유지하려면 스터디를 통해 PGN을 가져오세요.'; + String get importGameCaveat => '변형은 지워집니다. 변형을 유지하려면 연구를 통해 PGN을 가져오세요.'; @override String get importGameDataPrivacyWarning => '이 PGN은 모두가 볼 수 있게 됩니다. 비공개로 게임을 불러오려면, 연구 기능을 이용하세요.'; @@ -2379,7 +2635,7 @@ class AppLocalizationsKo extends AppLocalizations { String get retry => '재시도'; @override - String get reconnecting => '연결 재시도 중'; + String get reconnecting => '연결 중'; @override String get noNetwork => '오프라인'; @@ -2415,9 +2671,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get unblock => '차단 해제'; - @override - String get followsYou => '팔로워'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1(이)가 $param2(을)를 팔로우했습니다'; @@ -2524,7 +2777,7 @@ class AppLocalizationsKo extends AppLocalizations { String get winRate => '승률'; @override - String get berserkRate => '버서크율'; + String get berserkRate => '버서크 비율'; @override String get performance => '퍼포먼스 레이팅'; @@ -2600,7 +2853,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get ifNoneLeaveEmpty => '없으면 무시하세요'; + String get ifNoneLeaveEmpty => '없다면 비워두세요'; @override String get profile => '프로필'; @@ -2609,22 +2862,22 @@ class AppLocalizationsKo extends AppLocalizations { String get editProfile => '프로필 수정'; @override - String get realName => '본명'; + String get realName => '실명'; @override - String get setFlair => 'Set your flair'; + String get setFlair => '아이콘을 선택하세요'; @override - String get flair => 'Flair'; + String get flair => '아이콘'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => '전체 사이트에서 모든 유저의 아이콘을 숨기는 설정이 있습니다.'; @override String get biography => '소개'; @override - String get countryRegion => '국가/지역'; + String get countryRegion => '국가 또는 지역'; @override String get thankYou => '감사합니다!'; @@ -2633,13 +2886,13 @@ class AppLocalizationsKo extends AppLocalizations { String get socialMediaLinks => '소셜 미디어 링크'; @override - String get oneUrlPerLine => '한 줄에 1개 URL'; + String get oneUrlPerLine => '한 줄에 당 URL 1개'; @override - String get inlineNotation => '기보를 가로쓰기'; + String get inlineNotation => '기보법 가로쓰기'; @override - String get makeAStudy => '안전하게 보관하고 공유하려면 스터디를 만들어 보세요.'; + String get makeAStudy => '안전하게 보관하고 공유하려면 연구를 만들어 보세요.'; @override String get clearSavedMoves => '저장된 움직임 삭제'; @@ -2669,7 +2922,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzles => '퍼즐'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => '온라인 봇'; @override String get name => '이름'; @@ -2690,19 +2943,19 @@ class AppLocalizationsKo extends AppLocalizations { String get yes => '예'; @override - String get website => 'Website'; + String get website => '웹사이트'; @override - String get mobile => 'Mobile'; + String get mobile => '모바일'; @override String get help => '힌트:'; @override - String get createANewTopic => '새 토픽'; + String get createANewTopic => '새 주제 만들기'; @override - String get topics => '토픽'; + String get topics => '주제'; @override String get posts => '글'; @@ -2726,7 +2979,7 @@ class AppLocalizationsKo extends AppLocalizations { String get message => '내용'; @override - String get createTheTopic => '새 토픽 생성'; + String get createTheTopic => '새 주제 생성'; @override String get reportAUser => '사용자 신고'; @@ -2750,7 +3003,13 @@ class AppLocalizationsKo extends AppLocalizations { String get other => '기타'; @override - String get reportDescriptionHelp => '게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요.'; + String get reportCheatBoostHelp => '게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요. 그냥 \"그들이 부정행위를 했어요\" 라고만 말하지 말고, 어떻게 당신이 이 결론에 도달하게 됐는지 알려주세요.'; + + @override + String get reportUsernameHelp => '왜 이 사용자의 이름이 불쾌한지 설명해주세요. 그저 \"불쾌해요/부적절해요\"라고만 말하지 마세요, 대신 왜 이런 결론에 도달했는지 말씀해 주세요. 단어가 난해하거나, 영어가 아니거나, 은어이거나, 문화적/역사적 배경이 있는 경우 특히 중요합니다.'; + + @override + String get reportProcessedFasterInEnglish => '귀하의 신고가 영어로 적혀있을 경우 빠르게 처리될 것입니다.'; @override String get error_provideOneCheatedGameLink => '부정행위가 존재하는 게임의 링크를 적어도 하나는 적어주세요.'; @@ -2766,7 +3025,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get thisTopicIsNowClosed => '이 토픽은 닫혔습니다.'; + String get thisTopicIsNowClosed => '이 주제는 닫혔습니다.'; @override String get blog => '블로그'; @@ -2814,7 +3073,7 @@ class AppLocalizationsKo extends AppLocalizations { String get clockInitialTime => '기본 시간'; @override - String get clockIncrement => '한 수당 증가하는 시간'; + String get clockIncrement => '수 당 추가 시간'; @override String get privacy => '보안'; @@ -2829,7 +3088,7 @@ class AppLocalizationsKo extends AppLocalizations { String get letOtherPlayersChallengeYou => '다른 사람이 나에게 도전할 수 있게 함'; @override - String get letOtherPlayersInviteYouToStudy => '다른 플레이어들이 나를 학습에 초대할 수 있음'; + String get letOtherPlayersInviteYouToStudy => '다른 플레이어들이 나를 연구에 초대할 수 있음'; @override String get sound => '소리'; @@ -2853,7 +3112,7 @@ class AppLocalizationsKo extends AppLocalizations { String get outsideTheBoard => '보드 바깥쪽에'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => '보드의 모든 칸'; @override String get onSlowGames => '느린 게임에서만'; @@ -2909,10 +3168,10 @@ class AppLocalizationsKo extends AppLocalizations { String get human => '인간'; @override - String get computer => '인공지능'; + String get computer => '컴퓨터'; @override - String get side => '진영'; + String get side => '색'; @override String get clock => '시계'; @@ -2936,7 +3195,7 @@ class AppLocalizationsKo extends AppLocalizations { String get tools => '도구'; @override - String get increment => '시간 증가'; + String get increment => '추가 시간'; @override String get error_unknown => '잘못된 값'; @@ -2948,7 +3207,7 @@ class AppLocalizationsKo extends AppLocalizations { String get error_email => '이메일 주소가 유효하지 않습니다'; @override - String get error_email_acceptable => '이 이메일 주소는 받을 수 없습니다. 다시 확인후 시도해주세요.'; + String get error_email_acceptable => '이 이메일 주소는 수용 불가합니다. 확인후 다시 시도해주세요.'; @override String get error_email_unique => '이메일 주소가 유효하지 않거나 이미 등록되었습니다'; @@ -3056,51 +3315,51 @@ class AppLocalizationsKo extends AppLocalizations { String get sourceCode => '소스 코드'; @override - String get simultaneousExhibitions => '동시대국'; + String get simultaneousExhibitions => '다면기'; @override - String get host => '호스트'; + String get host => '주최자'; @override String hostColorX(String param) { - return '호스트의 색: $param'; + return '주최자 색: $param'; } @override - String get yourPendingSimuls => '대기 중인 동시대국'; + String get yourPendingSimuls => '대기 중인 다면기'; @override - String get createdSimuls => '새롭게 생성된 동시대국'; + String get createdSimuls => '새롭게 생성된 다면기'; @override - String get hostANewSimul => '새 동시대국을 생성하기'; + String get hostANewSimul => '새 다면기 주최하기'; @override - String get signUpToHostOrJoinASimul => '동시대국을 생성/참가하려면 로그인하세요'; + String get signUpToHostOrJoinASimul => '다면기를 주최/참가하려면 로그인하세요'; @override - String get noSimulFound => '동시대국을 찾을 수 없습니다'; + String get noSimulFound => '다면기를 찾을 수 없습니다'; @override - String get noSimulExplanation => '존재하지 않는 동시대국입니다.'; + String get noSimulExplanation => '존재하지 않는 다면기입니다.'; @override - String get returnToSimulHomepage => '동시대국 홈으로 돌아가기'; + String get returnToSimulHomepage => '다면기 홈으로 돌아가기'; @override - String get aboutSimul => '동시대국에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.'; + String get aboutSimul => '다면기에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.'; @override - String get aboutSimulImage => '50명의 상대 중, 피셔는 47국을 승리하였고, 2국은 무승부였으며 1국만을 패배하였습니다.'; + String get aboutSimulImage => '피셔는 50명의 상대 중, 47국을 승리하였고, 2국은 무승부였으며 1국만 패하였습니다.'; @override - String get aboutSimulRealLife => '이 동시대국의 개념은 실제 동시대국과 동일합니다. 실제로 1인 플레이어는 테이블을 넘기며 한 수씩 둡니다.'; + String get aboutSimulRealLife => '이 컨셉은 실제 이벤트들을 본딴 것입니다. 실제 경기에서는 다면기 주최자가 테이블을 돌아다니며 한 수씩 둡니다.'; @override - String get aboutSimulRules => '동시대국이 시작되면 모든 플레이어가 호스트와 게임을 합니다. 동시대국은 모든 플레이어와 게임이 끝나면 종료됩니다.'; + String get aboutSimulRules => '다면기가 시작되면, 모든 플레이어가 주최자와 대국을 합니다. 다면기는 모든 플레이어와 게임이 끝나면 종료됩니다.'; @override - String get aboutSimulSettings => '동시대국은 캐주얼 전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.'; + String get aboutSimulSettings => '다면기는 항상 캐주얼전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.'; @override String get create => '생성'; @@ -3151,7 +3410,7 @@ class AppLocalizationsKo extends AppLocalizations { String get keyGoToStartOrEnd => '처음/끝으로 가기'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get keyCycleSelectedVariation => '선택된 바리에이션 순환하기'; @override String get keyShowOrHideComments => '댓글 표시/숨기기'; @@ -3175,22 +3434,22 @@ class AppLocalizationsKo extends AppLocalizations { String get keyNextInaccuracy => '다음 부정확한 수'; @override - String get keyPreviousBranch => 'Previous branch'; + String get keyPreviousBranch => '이전 부'; @override - String get keyNextBranch => 'Next branch'; + String get keyNextBranch => '다음 부'; @override - String get toggleVariationArrows => 'Toggle variation arrows'; + String get toggleVariationArrows => '바리에이션 화살표 표시하기'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => '이전/다음 바리에이션 순환하기'; @override - String get toggleGlyphAnnotations => 'Toggle move annotations'; + String get toggleGlyphAnnotations => '이동 주석 토글하기'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => '위치 주석 토글하기'; @override String get variationArrowsInfo => '변형 화살표를 사용하면 이동 목록을 사용하지 않고 탐색이 가능합니다.'; @@ -3202,7 +3461,7 @@ class AppLocalizationsKo extends AppLocalizations { String get newTournament => '새로운 토너먼트'; @override - String get tournamentHomeTitle => '다양한 제한시간과 게임방식을 지원하는 체스 토너먼트'; + String get tournamentHomeTitle => '다양한 시간 제한과 변형을 지원하는 체스 토너먼트'; @override String get tournamentHomeDescription => '빠른 체스 토너먼트를 즐겨 보세요! 공식 일정이 잡힌 토너먼트에 참가할 수도, 당신만의 토너먼트를 만들 수도 있습니다. 불릿, 블리츠, 클래식, 체스960, 언덕의 왕, 3체크를 비롯하여 다양한 게임방식을 즐길 수 있습니다.'; @@ -3363,7 +3622,7 @@ class AppLocalizationsKo extends AppLocalizations { String get playChessEverywhere => '어디에서나 체스를 즐기세요'; @override - String get asFreeAsLichess => 'lichess처럼 무료입니다'; + String get asFreeAsLichess => 'Lichess처럼 무료예요'; @override String get builtForTheLoveOfChessNotMoney => '오직 체스에 대한 열정으로 만들어졌습니다'; @@ -3384,13 +3643,13 @@ class AppLocalizationsKo extends AppLocalizations { String get bulletBlitzClassical => '불릿, 블리츠, 클래식 방식 지원'; @override - String get correspondenceChess => '우편 체스 지원'; + String get correspondenceChess => '통신 체스'; @override String get onlineAndOfflinePlay => '온라인/오프라인 게임 모두 지원'; @override - String get viewTheSolution => '해답 보기'; + String get viewTheSolution => '정답 보기'; @override String get followAndChallengeFriends => '친구를 팔로우하고 도전하기'; @@ -3491,7 +3750,7 @@ class AppLocalizationsKo extends AppLocalizations { String get playChessInStyle => '스타일리시하게 체스하기'; @override - String get chessBasics => '체스 기본'; + String get chessBasics => '체스의 기본'; @override String get coaches => '코치'; @@ -3594,10 +3853,10 @@ class AppLocalizationsKo extends AppLocalizations { String get youCanDoBetter => '더 잘할 수 있어요'; @override - String get tryAnotherMoveForWhite => '흰색에게 또 다른 수를 찾아보세요'; + String get tryAnotherMoveForWhite => '백의 또 다른 수를 찾아보세요'; @override - String get tryAnotherMoveForBlack => '검은색에게 또 다른 수를 찾아보세요'; + String get tryAnotherMoveForBlack => '흑의 또 다른 수를 찾아보세요'; @override String get solution => '해답'; @@ -3606,28 +3865,28 @@ class AppLocalizationsKo extends AppLocalizations { String get waitingForAnalysis => '분석을 기다리는 중'; @override - String get noMistakesFoundForWhite => '백에게 악수는 없었습니다'; + String get noMistakesFoundForWhite => '백에게 실수는 없었습니다'; @override - String get noMistakesFoundForBlack => '흑에게 악수는 없었습니다'; + String get noMistakesFoundForBlack => '흑에게 실수는 없었습니다'; @override - String get doneReviewingWhiteMistakes => '백의 악수 체크가 종료됨'; + String get doneReviewingWhiteMistakes => '백의 실수 탐색이 종료됨'; @override - String get doneReviewingBlackMistakes => '흑의 악수 체크가 종료됨'; + String get doneReviewingBlackMistakes => '흑의 실수 탐색이 종료됨'; @override String get doItAgain => '다시 하기'; @override - String get reviewWhiteMistakes => '백의 악수를 체크'; + String get reviewWhiteMistakes => '백의 실수 탐색하기'; @override - String get reviewBlackMistakes => '흑의 악수를 체크'; + String get reviewBlackMistakes => '흑의 실수 탐색하기'; @override - String get advantage => '이득'; + String get advantage => '이점'; @override String get opening => '오프닝'; @@ -3656,7 +3915,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get showUnreadLichessMessage => '리체스로부터 비공개 메시지를 받았습니다.'; + String get showUnreadLichessMessage => 'Lichess로부터 비공개 메시지를 받았습니다.'; @override String get clickHereToReadIt => '클릭하여 읽기'; @@ -3754,7 +4013,7 @@ class AppLocalizationsKo extends AppLocalizations { String get classicalDesc => '클래시컬 게임: 25분 이상'; @override - String get correspondenceDesc => '통신전: 한 수당 하루 또는 수 일'; + String get correspondenceDesc => '통신 대국: 한 수당 하루 또는 며칠'; @override String get puzzleDesc => '체스 전술 트레이너'; @@ -3780,7 +4039,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String toRequestSupport(String param1) { - return '$param1에서 리체스에 문의하실 수 있습니다.'; + return '$param1에서 문의하실 수 있습니다.'; } @override @@ -3869,7 +4128,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get timeAlmostUp => '시간이 거의 다 되었습니다!'; + String get timeAlmostUp => '시간이 거의 다 되었어요!'; @override String get clickToRevealEmailAddress => '[이메일 주소를 보려면 클릭]'; @@ -3884,7 +4143,7 @@ class AppLocalizationsKo extends AppLocalizations { String get streamerManager => '스트리머 설정'; @override - String get cancelTournament => '토너먼트 취소'; + String get cancelTournament => '토너먼트 취소하기'; @override String get tournDescription => '토너먼트 설명'; @@ -3896,7 +4155,7 @@ class AppLocalizationsKo extends AppLocalizations { String get ratedFormHelp => '레이팅 게임을 합니다\n플레이어 레이팅에 영향을 줍니다'; @override - String get onlyMembersOfTeam => '팀 멤버만'; + String get onlyMembersOfTeam => '팀 멤버들만'; @override String get noRestriction => '제한 없음'; @@ -3946,7 +4205,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get embedsAvailable => '포함할 게임 URL 또는 스터디 챕터 URL을 붙여넣으세요.'; + String get embedsAvailable => '포함할 게임 URL 또는 연구 챕터 URL을 붙여넣으세요.'; @override String get inYourLocalTimezone => '본인의 현지 시간대 기준'; @@ -3964,7 +4223,7 @@ class AppLocalizationsKo extends AppLocalizations { String get onlyTeamMembers => '팀 멤버만'; @override - String get navigateMoveTree => '수 탐색'; + String get navigateMoveTree => '수의 나무 탐색'; @override String get mouseTricks => '마우스 기능'; @@ -3988,7 +4247,7 @@ class AppLocalizationsKo extends AppLocalizations { String get showHelpDialog => '이 도움말 보기'; @override - String get reopenYourAccount => '계정 다시 활성화'; + String get reopenYourAccount => '계정 재활성화'; @override String get closedAccountChangedMind => '계정을 폐쇄한 후 마음이 바뀌었다면, 계정을 다시 활성화할 수 있는 기회가 한 번 있습니다.'; @@ -4009,11 +4268,11 @@ class AppLocalizationsKo extends AppLocalizations { String get tournamentEntryCode => '토너먼트 입장 코드'; @override - String get hangOn => '잠깐!'; + String get hangOn => '잠깐만요!'; @override String gameInProgress(String param) { - return '$param와 진행중인 게임이 있습니다.'; + return '$param와(과) 진행중인 대국이 있습니다.'; } @override @@ -4032,7 +4291,7 @@ class AppLocalizationsKo extends AppLocalizations { String get until => '까지'; @override - String get lichessDbExplanation => '모든 리체스 플레이어의 레이팅 게임 샘플'; + String get lichessDbExplanation => '모든 Lichess 플레이어의 레이팅 게임 샘플'; @override String get switchSides => '색 바꾸기'; @@ -4055,12 +4314,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get nothingToSeeHere => '지금은 여기에 볼 것이 없습니다.'; + @override + String get stats => '통계'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '당신의 상대가 게임을 나갔습니다. $count 초 후에 승리를 주장할 수 있습니다.', + other: '상대방이 게임을 나갔습니다. $count초 후에 승리를 취할 수 있습니다.', ); return '$_temp0'; } @@ -4070,7 +4332,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count반수만에 체크메이트', + other: '$count개의 반수 후 체크메이트', ); return '$_temp0'; } @@ -4100,7 +4362,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 사소한 실수', + other: '$count 부정확한 수', ); return '$_temp0'; } @@ -4110,7 +4372,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count명의 플레이어', + other: '플레이어 $count명', ); return '$_temp0'; } @@ -4120,7 +4382,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 게임', + other: '게임 $count개', ); return '$_temp0'; } @@ -4180,7 +4442,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '순위는 매 $count분마다 갱신됩니다.', + other: '순위는 매 $count분마다 갱신됩니다', ); return '$_temp0'; } @@ -4210,7 +4472,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count번의 레이팅 대국', + other: '$count번의 레이팅 게임', ); return '$_temp0'; } @@ -4270,7 +4532,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 토너먼트 포인트', + other: '$count 토너먼트 점수', ); return '$_temp0'; } @@ -4290,7 +4552,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 동시대국', + other: '$count 다면기', ); return '$_temp0'; } @@ -4440,7 +4702,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 언어 지원!', + other: '$count개의 언어를 지원합니다!', ); return '$_temp0'; } @@ -4657,9 +4919,674 @@ class AppLocalizationsKo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess 스트리머'; + @override + String get studyPrivate => '비공개'; + + @override + String get studyMyStudies => '내 연구'; + + @override + String get studyStudiesIContributeTo => '내가 기여한 연구'; + + @override + String get studyMyPublicStudies => '내 공개 연구'; + + @override + String get studyMyPrivateStudies => '내 비공개 연구'; + + @override + String get studyMyFavoriteStudies => '내가 즐겨찾는 연구'; + + @override + String get studyWhatAreStudies => '연구란 무엇인가요?'; + + @override + String get studyAllStudies => '모든 연구'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param이(가) 만든 연구'; + } + + @override + String get studyNoneYet => '아직 없음'; + + @override + String get studyHot => '인기있는'; + + @override + String get studyDateAddedNewest => '추가된 날짜(새로운 순)'; + + @override + String get studyDateAddedOldest => '추가된 날짜(오래된 순)'; + + @override + String get studyRecentlyUpdated => '최근에 업데이트된 순'; + + @override + String get studyMostPopular => '인기 많은 순'; + + @override + String get studyAlphabetical => '알파벳 순'; + + @override + String get studyAddNewChapter => '새 챕터 추가하기'; + + @override + String get studyAddMembers => '멤버 추가'; + + @override + String get studyInviteToTheStudy => '연구에 초대'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '당신이 아는 사람들이나 연구에 적극적으로 참여하고 싶은 사람들만 초대하세요.'; + + @override + String get studySearchByUsername => '사용자 이름으로 검색'; + + @override + String get studySpectator => '관전자'; + + @override + String get studyContributor => '기여자'; + + @override + String get studyKick => '강제 퇴장'; + + @override + String get studyLeaveTheStudy => '연구 나가기'; + + @override + String get studyYouAreNowAContributor => '당신은 이제 기여자입니다'; + + @override + String get studyYouAreNowASpectator => '당신은 이제 관전자입니다'; + + @override + String get studyPgnTags => 'PGN 태그'; + + @override + String get studyLike => '좋아요'; + + @override + String get studyUnlike => '좋아요 취소'; + + @override + String get studyNewTag => '새 태그'; + + @override + String get studyCommentThisPosition => '이 포지션에 댓글 달기'; + + @override + String get studyCommentThisMove => '이 수에 댓글 달기'; + + @override + String get studyAnnotateWithGlyphs => '기호로 주석 달기'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '분석되기 너무 짧은 챕터입니다.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => '연구 기여자만이 컴퓨터 분석을 요청할 수 있습니다.'; + + @override + String get studyGetAFullComputerAnalysis => '메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.'; + + @override + String get studyMakeSureTheChapterIsComplete => '챕터가 완료되었는지 확인하세요. 분석은 한번만 요청할 수 있습니다.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '동기화된 모든 멤버들은 같은 포지션을 공유합니다'; + + @override + String get studyShareChanges => '관전자와 변경 사항을 공유하고 서버에 저장'; + + @override + String get studyPlaying => '대국 중'; + + @override + String get studyShowEvalBar => '평가 막대'; + + @override + String get studyFirst => '처음'; + + @override + String get studyPrevious => '이전'; + + @override + String get studyNext => '다음'; + + @override + String get studyLast => '마지막'; + @override String get studyShareAndExport => '공유 및 내보내기'; + @override + String get studyCloneStudy => '복제'; + + @override + String get studyStudyPgn => '연구 PGN'; + + @override + String get studyDownloadAllGames => '모든 게임 다운로드'; + + @override + String get studyChapterPgn => '챕터 PGN'; + + @override + String get studyCopyChapterPgn => 'PGN 복사'; + + @override + String get studyDownloadGame => '게임 다운로드'; + + @override + String get studyStudyUrl => '연구 URL'; + + @override + String get studyCurrentChapterUrl => '현재 챕터 URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => '포럼에 공유하려면 이 주소를 붙여넣으세요'; + + @override + String get studyStartAtInitialPosition => '처음 포지션에서 시작'; + + @override + String studyStartAtX(String param) { + return '$param에서 시작'; + } + + @override + String get studyEmbedInYourWebsite => '웹사이트 또는 블로그에 공유하기'; + + @override + String get studyReadMoreAboutEmbedding => '공유에 대한 상세 정보'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '공개 연구만 공유할 수 있습니다!'; + + @override + String get studyOpen => '열기'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1. $param2에서 가져옴'; + } + + @override + String get studyStudyNotFound => '연구를 찾을 수 없음'; + + @override + String get studyEditChapter => '챕터 편집하기'; + + @override + String get studyNewChapter => '새 챕터'; + + @override + String studyImportFromChapterX(String param) { + return '$param에서 가져오기'; + } + + @override + String get studyOrientation => '방향'; + + @override + String get studyAnalysisMode => '분석 모드'; + + @override + String get studyPinnedChapterComment => '챕터 댓글 고정하기'; + + @override + String get studySaveChapter => '챕터 저장'; + + @override + String get studyClearAnnotations => '주석 지우기'; + + @override + String get studyClearVariations => '바리에이션 초기화'; + + @override + String get studyDeleteChapter => '챕터 지우기'; + + @override + String get studyDeleteThisChapter => '이 챕터를 지울까요? 되돌릴 수 없습니다!'; + + @override + String get studyClearAllCommentsInThisChapter => '이 챕터의 모든 코멘트와 기호를 지울까요?'; + + @override + String get studyRightUnderTheBoard => '보드 우하단에'; + + @override + String get studyNoPinnedComment => '없음'; + + @override + String get studyNormalAnalysis => '일반 분석'; + + @override + String get studyHideNextMoves => '다음 수 숨기기'; + + @override + String get studyInteractiveLesson => '상호 대화형 레슨'; + + @override + String studyChapterX(String param) { + return '챕터 $param'; + } + + @override + String get studyEmpty => '비어있음'; + + @override + String get studyStartFromInitialPosition => '초기 포지션에서 시작'; + + @override + String get studyEditor => '편집기'; + + @override + String get studyStartFromCustomPosition => '커스텀 포지션에서 시작'; + + @override + String get studyLoadAGameByUrl => 'URL로 게임 가져오기'; + + @override + String get studyLoadAPositionFromFen => 'FEN으로 포지션 가져오기'; + + @override + String get studyLoadAGameFromPgn => 'PGN으로 게임 가져오기'; + + @override + String get studyAutomatic => '자동'; + + @override + String get studyUrlOfTheGame => '한 줄에 하나씩, 게임의 URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 또는 $param2에서 게임 로드'; + } + + @override + String get studyCreateChapter => '챕터 만들기'; + + @override + String get studyCreateStudy => '연구 만들기'; + + @override + String get studyEditStudy => '연구 편집하기'; + + @override + String get studyVisibility => '공개 설정'; + + @override + String get studyPublic => '공개'; + + @override + String get studyUnlisted => '비공개'; + + @override + String get studyInviteOnly => '초대만'; + + @override + String get studyAllowCloning => '복제 허용'; + + @override + String get studyNobody => '아무도'; + + @override + String get studyOnlyMe => '나만'; + + @override + String get studyContributors => '기여자만'; + + @override + String get studyMembers => '멤버만'; + + @override + String get studyEveryone => '모두'; + + @override + String get studyEnableSync => '동기화 사용'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => '예: 모두가 같은 위치를 봅니다'; + + @override + String get studyNoLetPeopleBrowseFreely => '아니요: 사람들이 자유롭게 이동할 수 있습니다'; + + @override + String get studyPinnedStudyComment => '고정된 댓글'; + @override String get studyStart => '시작'; + + @override + String get studySave => '저장'; + + @override + String get studyClearChat => '채팅 기록 지우기'; + + @override + String get studyDeleteTheStudyChatHistory => '연구 채팅 기록을 삭제할까요? 되돌릴 수 없습니다!'; + + @override + String get studyDeleteStudy => '연구 삭제'; + + @override + String studyConfirmDeleteStudy(String param) { + return '모든 연구를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 연구의 이름을 입력하세요: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => '어디에서 연구를 시작하시겠습니까?'; + + @override + String get studyGoodMove => '좋은 수'; + + @override + String get studyMistake => '실수'; + + @override + String get studyBrilliantMove => '매우 좋은 수'; + + @override + String get studyBlunder => '블런더'; + + @override + String get studyInterestingMove => '흥미로운 수'; + + @override + String get studyDubiousMove => '애매한 수'; + + @override + String get studyOnlyMove => '유일한 수'; + + @override + String get studyZugzwang => '추크추방'; + + @override + String get studyEqualPosition => '동등한 포지션'; + + @override + String get studyUnclearPosition => '불확실한 포지션'; + + @override + String get studyWhiteIsSlightlyBetter => '백이 미세하게 좋음'; + + @override + String get studyBlackIsSlightlyBetter => '흑이 미세하게 좋음'; + + @override + String get studyWhiteIsBetter => '백이 유리함'; + + @override + String get studyBlackIsBetter => '흑이 유리함'; + + @override + String get studyWhiteIsWinning => '백이 이기고 있음'; + + @override + String get studyBlackIsWinning => '흑이 이기고 있음'; + + @override + String get studyNovelty => '새로운 수'; + + @override + String get studyDevelopment => '발전'; + + @override + String get studyInitiative => '주도권'; + + @override + String get studyAttack => '공격'; + + @override + String get studyCounterplay => '반격'; + + @override + String get studyTimeTrouble => '시간이 부족함'; + + @override + String get studyWithCompensation => '보상이 있음'; + + @override + String get studyWithTheIdea => '아이디어'; + + @override + String get studyNextChapter => '다음 챕터'; + + @override + String get studyPrevChapter => '이전 챕터'; + + @override + String get studyStudyActions => '연구 작업'; + + @override + String get studyTopics => '주제'; + + @override + String get studyMyTopics => '내 주제'; + + @override + String get studyPopularTopics => '인기 주제'; + + @override + String get studyManageTopics => '주제 관리'; + + @override + String get studyBack => '뒤로'; + + @override + String get studyPlayAgain => '다시 플레이'; + + @override + String get studyWhatWouldYouPlay => '이 포지션에서 무엇을 하시겠습니까?'; + + @override + String get studyYouCompletedThisLesson => '축하합니다! 이 레슨을 완료했습니다.'; + + @override + String studyPerPage(String param) { + return '페이지 당 $param개'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 챕터', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 게임', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '멤버 $count명', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN을 여기에 붙여넣으세요. 최대 $count 게임까지 가능합니다.', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => '방금'; + + @override + String get timeagoRightNow => '지금'; + + @override + String get timeagoCompleted => '종료됨'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count초 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count일 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count주 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count개월 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count년 후', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count일 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count주 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count개월 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count년 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 남음', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 남음', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index 26186fd760..1d8ae93639 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsLb extends AppLocalizations { AppLocalizationsLb([String locale = 'lb']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All Partien'; @override - String get mobilePuzzlesTab => 'Aufgaben'; + String get mobileAreYouSure => 'Bass de sécher?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'Du muss ageloggt si fir dës Säit ze gesinn.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'Systemsfaarwen'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Moien, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Moien'; @override - String get mobileSettingsHapticFeedback => 'Haptesche Feedback'; + String get mobileHideVariation => 'Variante verstoppen'; @override - String get mobileSettingsImmersiveMode => 'Immersive Modus'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'Du muss ageloggt si fir dës Säit ze gesinn.'; @override - String get mobileAllGames => 'All Partien'; + String get mobileNoSearchResults => 'Keng Resultater'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsLb extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Keng Resultater'; + String get mobilePrefMagnifyDraggedPiece => 'Gezunne Figur vergréisseren'; @override - String get mobileAreYouSure => 'Bass de sécher?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Dës Aufgab deelen'; + String get mobilePuzzleStormSubtitle => 'Léis sou vill Aufgabe wéi méiglech an 3 Minutten.'; @override - String get mobileShareGameURL => 'URL vun der Partie deelen'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'PGN deelen'; + String get mobilePuzzleThemesSubtitle => 'Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.'; @override - String get mobileSharePositionAsFEN => 'Stellung als FEN deelen'; + String get mobilePuzzlesTab => 'Aufgaben'; @override - String get mobileShowVariations => 'Variante weisen'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Variante verstoppen'; + String get mobileSettingsHapticFeedback => 'Haptesche Feedback'; @override - String get mobileShowComments => 'Kommentarer weisen'; + String get mobileSettingsImmersiveMode => 'Immersive Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'PGN deelen'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'URL vun der Partie deelen'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Stellung als FEN deelen'; @override - String get mobileBlindfoldMode => 'Blann'; + String get mobileSharePuzzle => 'Dës Aufgab deelen'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Kommentarer weisen'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Resultat weisen'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Variante weisen'; @override String get mobileSomethingWentWrong => 'Et ass eppes schifgaang.'; @override - String get mobileShowResult => 'Resultat weisen'; - - @override - String get mobilePuzzleThemesSubtitle => 'Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.'; + String get mobileSystemColors => 'Systemsfaarwen'; @override - String get mobilePuzzleStormSubtitle => 'Léis sou vill Aufgabe wéi méiglech an 3 Minutten.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Moien, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Moien'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Verlaf'; @@ -246,6 +243,17 @@ class AppLocalizationsLb extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Huet $count $param2-Fernschachpartië gespillt', + one: 'Huet $count $param2-Fernschachpartie gespillt', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsLb extends AppLocalizations { @override String get broadcastBroadcasts => 'Iwwerdroungen'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Live Turnéier Iwwerdroungen'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nei Live Iwwerdroung'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Ronn hinzufügen'; + + @override + String get broadcastOngoing => 'Am Gaang'; + + @override + String get broadcastUpcoming => 'Demnächst'; + + @override + String get broadcastCompleted => 'Eriwwer'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ronnennumm'; + + @override + String get broadcastRoundNumber => 'Ronnennummer'; + + @override + String get broadcastTournamentName => 'Turnéiernumm'; + + @override + String get broadcastTournamentDescription => 'Kuerz Turnéierbeschreiwung'; + + @override + String get broadcastFullDescription => 'Komplett Turnéierbeschreiwung'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional laang Beschreiwung vum Turnéier. $param1 ass disponibel. Längt muss manner wéi $param2 Buschtawen sinn.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL déi Lichess checkt fir PGN à jour ze halen. Muss ëffentlech iwwer Internet zougänglech sinn.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess-Partie-IDen, duerch Espacë getrennt.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum vum Turnéier an der lokaler Zäitzon: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, wann du wees wéini den Turnéier ufänkt'; + + @override + String get broadcastCurrentGameUrl => 'URL vun der aktueller Partie'; + + @override + String get broadcastDownloadAllRounds => 'All Ronnen eroflueden'; + + @override + String get broadcastResetRound => 'Ronn zerécksetzen'; + + @override + String get broadcastDeleteRound => 'Ronn läschen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dës Ronn an hir Partien endgülteg läschen.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'All Partien vun dëser Ronn läschen. D\'Quell muss aktiv sinn fir se ze rekreéieren.'; + + @override + String get broadcastEditRoundStudy => 'Ronnen-Etüd modifiéieren'; + + @override + String get broadcastDeleteTournament => 'Dësen Turnéier läschen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'De ganzen Turnéier definitiv läschen, all seng Ronnen an all seng Partien.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Spillernimm, Wäertungen an Titelen ersetzen'; + + @override + String get broadcastFideFederations => 'FIDE-Federatiounen'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-Spiller'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-Spiller net tfonnt'; + + @override + String get broadcastFideProfile => 'FIDE-Profil'; + + @override + String get broadcastFederation => 'Federatioun'; + + @override + String get broadcastAgeThisYear => 'Alter dëst Joer'; + + @override + String get broadcastUnrated => 'Ongewäert'; + + @override + String get broadcastRecentTournaments => 'Rezent Turnéieren'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Ekippen'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Iwwersiicht'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Turnéierbild eroplueden'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Fänkt no $param un'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Offiziell Websäit'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Offizielle Stand'; + + @override + String broadcastIframeHelp(String param) { + return 'Méi Optiounen op der $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster-Säit'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Partien an dësem Turnéier'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All Ekippen'; + + @override + String get broadcastTournamentFormat => 'Turnéierformat'; + + @override + String get broadcastTournamentLocation => 'Turnéierplaz'; + + @override + String get broadcastTopPlayers => 'Topspiller'; + + @override + String get broadcastTimezone => 'Zäitzon'; + + @override + String get broadcastFideRatingCategory => 'FIDE-Wäertungskategorie'; + + @override + String get broadcastOptionalDetails => 'Fakultativ Detailler'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Iwwerdroungen', + one: '$count Iwwerdroung', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Erausfuerderungen: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get preferencesInGameOnly => 'Nëmmen während enger Partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachauer'; @@ -750,6 +1008,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Glacken-Notifikatiounstoun'; + @override + String get preferencesBlindfold => 'Blann'; + @override String get puzzlePuzzles => 'Aufgaben'; @@ -1390,10 +1651,10 @@ class AppLocalizationsLb extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De Géigner huet eng begrenzten Unzuel un Zich an all Zuch verschlechtert seng Positioun.'; @override - String get puzzleThemeHealthyMix => 'Gesonde Mix'; + String get puzzleThemeMix => 'Gesonde Mix'; @override - String get puzzleThemeHealthyMixDescription => 'E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.'; + String get puzzleThemeMixDescription => 'E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.'; @override String get puzzleThemePlayerGames => 'Partie vu Spiller'; @@ -1767,9 +2028,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get byCPL => 'No CPL'; - @override - String get openStudy => 'Studie opmaachen'; - @override String get enable => 'Aktivéieren'; @@ -1797,9 +2055,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt d\'Déifenbegrenzung an hält däin Computer waarm'; - @override - String get engineManager => 'Engineverwaltung'; - @override String get blunder => 'Gaffe'; @@ -2063,6 +2318,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get gamesPlayed => 'Partien gespillt'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annuléieren'; @@ -2437,9 +2695,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get unblock => 'Spär ophiewen'; - @override - String get followsYou => 'Followt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 followt elo $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsLb extends AppLocalizations { String get other => 'Aner'; @override - String get reportDescriptionHelp => 'Post den Link vun Partie(n) and erklär wat den Problem mat dësem Benotzer sengem Verhalen ass. So net just \"Hien fuddelt\", mee so eis wéi du zu dëser Konklusioun komm bass. Däin Rapport gëtt méi schnell veraarbecht wann en op Englesch ass.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Wannechgelift gëff eis op mannst een Link zu enger Partie mat Bedruch.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get nothingToSeeHere => 'Fir de Moment gëtt et hei näischt ze gesinn.'; + @override + String get stats => 'Statistiken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsLb extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Meng Etüden'; + + @override + String get studyStudiesIContributeTo => 'Etüden, un deenen ech matwierken'; + + @override + String get studyMyPublicStudies => 'Meng öffentlech Etüden'; + + @override + String get studyMyPrivateStudies => 'Meng privat Etüden'; + + @override + String get studyMyFavoriteStudies => 'Meng Lieblingsetüden'; + + @override + String get studyWhatAreStudies => 'Wat sinn Etüden?'; + + @override + String get studyAllStudies => 'All Etüden'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Etüden kreéiert vun $param'; + } + + @override + String get studyNoneYet => 'Nach keng.'; + + @override + String get studyHot => 'Ugesot'; + + @override + String get studyDateAddedNewest => 'Veröffentlechungsdatum (am neisten)'; + + @override + String get studyDateAddedOldest => 'Veröffentlechungsdatum (am aalsten)'; + + @override + String get studyRecentlyUpdated => 'Rezent aktualiséiert'; + + @override + String get studyMostPopular => 'Am Beléiftsten'; + + @override + String get studyAlphabetical => 'Alphabetesch'; + + @override + String get studyAddNewChapter => 'Neit Kapitel bäifügen'; + + @override + String get studyAddMembers => 'Memberen hinzufügen'; + + @override + String get studyInviteToTheStudy => 'An d\'Etüd alueden'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Wannechgelift invitéier just Leit déi du kenns an déi aktiv un der Etüd matwierken wëllen.'; + + @override + String get studySearchByUsername => 'No Benotzernumm sichen'; + + @override + String get studySpectator => 'Zuschauer'; + + @override + String get studyContributor => 'Matwierkenden'; + + @override + String get studyKick => 'Rausgehéien'; + + @override + String get studyLeaveTheStudy => 'Etüd verloossen'; + + @override + String get studyYouAreNowAContributor => 'Du bass elo e Contributeur'; + + @override + String get studyYouAreNowASpectator => 'Du bass elo en Zuschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gefällt mir'; + + @override + String get studyUnlike => 'Gefällt mer net méi'; + + @override + String get studyNewTag => 'Néien Tag'; + + @override + String get studyCommentThisPosition => 'Kommentéier des Positioun'; + + @override + String get studyCommentThisMove => 'Kommentéier dësen Zuch'; + + @override + String get studyAnnotateWithGlyphs => 'Mat Symboler kommentéieren'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'D\'Kapitel ass ze kuerz fir analyséiert ze ginn.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Just Etüden Matwierkender kënnen eng Computer Analyse ufroen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Vollstänneg serversäiteg Computeranalyse vun der Haaptvariant erhalen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Stell sécher dass d\'Kapitel vollstänneg ass. Du kanns eng Analyse just eemol ufroen.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC Memberen gesinn déi selwecht Positioun'; + + @override + String get studyShareChanges => 'Deel Ännerungen mat den Zuschauer an späicher se um Server'; + + @override + String get studyPlaying => 'Lafend Partie'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Éischt Säit'; + + @override + String get studyPrevious => 'Zeréck'; + + @override + String get studyNext => 'Weider'; + + @override + String get studyLast => 'Lescht Säit'; + @override String get studyShareAndExport => 'Deelen & exportéieren'; + @override + String get studyCloneStudy => 'Klonen'; + + @override + String get studyStudyPgn => 'Etüden PGN'; + + @override + String get studyDownloadAllGames => 'All Partien eroflueden'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopéieren'; + + @override + String get studyDownloadGame => 'Partie eroflueden'; + + @override + String get studyStudyUrl => 'Etüden URL'; + + @override + String get studyCurrentChapterUrl => 'Aktuellt Kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Zum Anbetten an een Forum oder Blog afügen'; + + @override + String get studyStartAtInitialPosition => 'Mat Startpositioun ufänken'; + + @override + String studyStartAtX(String param) { + return 'Bei $param ufänken'; + } + + @override + String get studyEmbedInYourWebsite => 'An Websäit anbetten'; + + @override + String get studyReadMoreAboutEmbedding => 'Méi iwwer Anbetten liesen'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Just ëffentlech Etüden kënnen angebett ginn!'; + + @override + String get studyOpen => 'Opmaachen'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, presentéiert vum $param2'; + } + + @override + String get studyStudyNotFound => 'Etüd net fonnt'; + + @override + String get studyEditChapter => 'Kapitel editéieren'; + + @override + String get studyNewChapter => 'Neit Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importéieren aus $param'; + } + + @override + String get studyOrientation => 'Orientatioun'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Ugepinnten Kapitelkommentar'; + + @override + String get studySaveChapter => 'Kapitel späicheren'; + + @override + String get studyClearAnnotations => 'Annotatiounen läschen'; + + @override + String get studyClearVariations => 'Variante läschen'; + + @override + String get studyDeleteChapter => 'Kapitel läschen'; + + @override + String get studyDeleteThisChapter => 'Kapitel läschen? Et gëtt keen zeréck!'; + + @override + String get studyClearAllCommentsInThisChapter => 'All Kommentarer, Symboler an Zeechnungsformen an dësem Kapitel läschen?'; + + @override + String get studyRightUnderTheBoard => 'Direkt ënnert dem Briet'; + + @override + String get studyNoPinnedComment => 'Keng'; + + @override + String get studyNormalAnalysis => 'Normal Analyse'; + + @override + String get studyHideNextMoves => 'Nächst Zich verstoppen'; + + @override + String get studyInteractiveLesson => 'Interaktiv Übung'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Eidel'; + + @override + String get studyStartFromInitialPosition => 'Aus Startpositioun ufänken'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Aus benotzerdefinéierter Positioun ufänken'; + + @override + String get studyLoadAGameByUrl => 'Partien mat URL lueden'; + + @override + String get studyLoadAPositionFromFen => 'Positioun aus FEN lueden'; + + @override + String get studyLoadAGameFromPgn => 'Partien aus PGN lueden'; + + @override + String get studyAutomatic => 'Automatesch'; + + @override + String get studyUrlOfTheGame => 'URL vun den Partien, eng pro Zeil'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Partien vun $param1 oder $param2 lueden'; + } + + @override + String get studyCreateChapter => 'Kapitel kréieren'; + + @override + String get studyCreateStudy => 'Etüd kreéieren'; + + @override + String get studyEditStudy => 'Etüd änneren'; + + @override + String get studyVisibility => 'Visibilitéit'; + + @override + String get studyPublic => 'Ëffentlech'; + + @override + String get studyUnlisted => 'Ongelëscht'; + + @override + String get studyInviteOnly => 'Just mat Invitatioun'; + + @override + String get studyAllowCloning => 'Klonen erlaaben'; + + @override + String get studyNobody => 'Keen'; + + @override + String get studyOnlyMe => 'Just ech'; + + @override + String get studyContributors => 'Matwierkendender'; + + @override + String get studyMembers => 'Memberen'; + + @override + String get studyEveryone => 'Jiddereen'; + + @override + String get studyEnableSync => 'Synchronisatioun aktivéieren'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jo: Jiddereen op der selwechter Positioun halen'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: Leit individuell browsen loossen'; + + @override + String get studyPinnedStudyComment => 'Ugepinnten Etüdenkommentar'; + @override String get studyStart => 'Lass'; + + @override + String get studySave => 'Späicheren'; + + @override + String get studyClearChat => 'Chat läschen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Etüdenchat läschen? Et gëtt keen zeréck!'; + + @override + String get studyDeleteStudy => 'Etüd läschen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Komplett Etüd läschen? Et gëett keen zeréck! Tipp den Numm vun der Etüd an fir ze konfirméieren: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Wéieng Etüd wëlls du benotzen?'; + + @override + String get studyGoodMove => 'Gudden Zuch'; + + @override + String get studyMistake => 'Feeler'; + + @override + String get studyBrilliantMove => 'Brillianten Zuch'; + + @override + String get studyBlunder => 'Gaffe'; + + @override + String get studyInterestingMove => 'Interessanten Zuch'; + + @override + String get studyDubiousMove => 'Dubiosen Zuch'; + + @override + String get studyOnlyMove => 'Eenzegen Zuch'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Ausgeglach Positioun'; + + @override + String get studyUnclearPosition => 'Onkloer Positioun'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wäiss steet liicht besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwaarz steet liicht besser'; + + @override + String get studyWhiteIsBetter => 'Wäiss ass besser'; + + @override + String get studyBlackIsBetter => 'Schwaarz ass besser'; + + @override + String get studyWhiteIsWinning => 'Wéiss steet op Gewënn'; + + @override + String get studyBlackIsWinning => 'Schwaarz steet op Gewënn'; + + @override + String get studyNovelty => 'Neiheet'; + + @override + String get studyDevelopment => 'Entwécklung'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Ugrëff'; + + @override + String get studyCounterplay => 'Géigespill'; + + @override + String get studyTimeTrouble => 'Zäitdrock'; + + @override + String get studyWithCompensation => 'Mat Kompensatioun'; + + @override + String get studyWithTheIdea => 'Mat der Iddi'; + + @override + String get studyNextChapter => 'Nächst Kapitel'; + + @override + String get studyPrevChapter => 'Kapitel virdrun'; + + @override + String get studyStudyActions => 'Etüden-Aktiounen'; + + @override + String get studyTopics => 'Themen'; + + @override + String get studyMyTopics => 'Meng Themen'; + + @override + String get studyPopularTopics => 'Beléift Themen'; + + @override + String get studyManageTopics => 'Themen managen'; + + @override + String get studyBack => 'Zeréck'; + + @override + String get studyPlayAgain => 'Nach eng Kéier spillen'; + + @override + String get studyWhatWouldYouPlay => 'Wat géifs du an dëser Positioun spillen?'; + + @override + String get studyYouCompletedThisLesson => 'Gudd gemaach! Du hues dës Übung ofgeschloss.'; + + @override + String studyPerPage(String param) { + return '$param pro Säit'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partien', + one: '$count Partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Memberen', + one: '$count Member', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN Text hei asetzen, bis zu $count Partien', + one: 'PGN Text hei asetzen, bis zu $count Partie', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'elo grad'; + + @override + String get timeagoRightNow => 'elo'; + + @override + String get timeagoCompleted => 'eriwwer'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Sekonnen', + one: 'an $count Sekonn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Minutten', + one: 'an $count Minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Stonnen', + one: 'an $count Stonn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Deeg', + one: 'an $count Dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Wochen', + one: 'an $count Woch', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Méint', + one: 'an $count Mount', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Joer', + one: 'an $count Joer', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Minutten', + one: 'virun $count Minutt', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Stonnen', + one: 'virun $count Stonn', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Deeg', + one: 'virun $count Dag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Wochen', + one: 'virun $count Woch', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Méint', + one: 'virun $count Mount', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Joer', + one: 'virun $count Joer', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minutten iwwereg', + one: '$count Minutt iwwereg', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Stonnen iwwereg', + one: '$count Stonn iwwereg', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index 37288a6162..be90c42bdb 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsLt extends AppLocalizations { AppLocalizationsLt([String locale = 'lt']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsLt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Veikla'; @@ -262,6 +259,17 @@ class AppLocalizationsLt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +390,258 @@ class AppLocalizationsLt extends AppLocalizations { @override String get broadcastBroadcasts => 'Transliacijos'; + @override + String get broadcastMyBroadcasts => 'Mano transliacijos'; + @override String get broadcastLiveBroadcasts => 'Vykstančios turnyrų transliacijos'; + @override + String get broadcastBroadcastCalendar => 'Transliavimo kalendorius'; + + @override + String get broadcastNewBroadcast => 'Nauja transliacija'; + + @override + String get broadcastSubscribedBroadcasts => 'Prenumeruojamos transliacijos'; + + @override + String get broadcastAboutBroadcasts => 'Apie transliacijas'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kaip naudotis Lichess transliacijomis.'; + + @override + String get broadcastTheNewRoundHelp => 'Naujajame ture bus tie patys nariai ir bendradarbiai, kaip ir ankstesniame.'; + + @override + String get broadcastAddRound => 'Pridėti raundą'; + + @override + String get broadcastOngoing => 'Vykstančios'; + + @override + String get broadcastUpcoming => 'Artėjančios'; + + @override + String get broadcastCompleted => 'Pasibaigę'; + + @override + String get broadcastCompletedHelp => 'Lichess aptiko turo užbaigimą, bet galimai klaidingai. Naudokite tai, norėdami nustatyti rankiniu būdu.'; + + @override + String get broadcastRoundName => 'Raundo pavadinimas'; + + @override + String get broadcastRoundNumber => 'Raundo numeris'; + + @override + String get broadcastTournamentName => 'Turnyro pavadinimas'; + + @override + String get broadcastTournamentDescription => 'Trumpas turnyro aprašymas'; + + @override + String get broadcastFullDescription => 'Pilnas renginio aprašymas'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neprivalomas pilnas transliacijos aprašymas. Galima naudoti $param1. Ilgis negali viršyti $param2 simbolių.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN šaltinio URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, į kurį „Lichess“ kreipsis gauti PGN atnaujinimus. Privalo būti viešai pasiekiamas internete.'; + + @override + String get broadcastSourceGameIds => 'Iki 64 Lichess žaidimo ID, atskirtų tarpais.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Turnyro pradžia vietos laiku: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neprivaloma; tik jeigu žinote, kada prasideda renginys'; + + @override + String get broadcastCurrentGameUrl => 'Dabartinio žaidimo adresas'; + + @override + String get broadcastDownloadAllRounds => 'Atsisiųsti visus raundus'; + + @override + String get broadcastResetRound => 'Atstatyti raundą'; + + @override + String get broadcastDeleteRound => 'Ištrinti raundą'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Užtikrintai ištrinti raundą ir jo partijas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Ištrinti visas partijas šiame raunde. Norint jas perkurti reikės aktyvaus šaltinio.'; + + @override + String get broadcastEditRoundStudy => 'Keisti raundo studiją'; + + @override + String get broadcastDeleteTournament => 'Ištrinti šį turnyrą'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Užtikrintai ištrinti visą turnyrą, visus raundus ir visas jų partijas.'; + + @override + String get broadcastShowScores => 'Rodyti žaidėjų balus pagal partijų rezultatus'; + + @override + String get broadcastReplacePlayerTags => 'Pasirenkama: pakeiskite žaidėjų vardus, reitingus ir titulus'; + + @override + String get broadcastFideFederations => 'FIDE federacijos'; + + @override + String get broadcastTop10Rating => '10 aukščiausių reitingų'; + + @override + String get broadcastFidePlayers => 'FIDE žaidėjai'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE žaidėjas nerastas'; + + @override + String get broadcastFideProfile => 'FIDE profilis'; + + @override + String get broadcastFederation => 'Federacija'; + + @override + String get broadcastAgeThisYear => 'Amžius šiemet'; + + @override + String get broadcastUnrated => 'Nereitinguota(s)'; + + @override + String get broadcastRecentTournaments => 'Neseniai sukurti turnyrai'; + + @override + String get broadcastOpenLichess => 'Atverti Lichess-e'; + + @override + String get broadcastTeams => 'Komandos'; + + @override + String get broadcastBoards => 'Lentos'; + + @override + String get broadcastOverview => 'Apžvalga'; + + @override + String get broadcastSubscribeTitle => 'Užsakykite pranešimą apie kiekvieno turo pradžią. Paskyros nustatymuose galite perjungti transliacijų skambėjimo signalą arba tiesioginius pranešimus.'; + + @override + String get broadcastUploadImage => 'Įkelkite turnyro paveikslėlį'; + + @override + String get broadcastNoBoardsYet => 'Dar nėra lentų. Jos bus rodomos, kai bus įkeltos partijos.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Lentas galima įkelti iš šaltinio arba per $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Pradedama po $param'; + } + + @override + String get broadcastStartVerySoon => 'Transliacija prasidės visai netrukus.'; + + @override + String get broadcastNotYetStarted => 'Transliacija dar neprasidėjo.'; + + @override + String get broadcastOfficialWebsite => 'Oficialus tinklapis'; + + @override + String get broadcastStandings => 'Rezultatai'; + + @override + String get broadcastOfficialStandings => 'Oficialūs rezultatai'; + + @override + String broadcastIframeHelp(String param) { + return 'Daugiau parinkčių $param'; + } + + @override + String get broadcastWebmastersPage => 'žiniatinklio valdytojų puslapis'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Viešas realaus laiko PGN šaltinis šiam turui. Taip pat siūlome $param greitesniam ir efektyvesniam sinchronizavimui.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Įterpkite šią transliaciją į savo svetainę'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Įterpkite $param į savo svetainę'; + } + + @override + String get broadcastRatingDiff => 'Reitingo skirtumas'; + + @override + String get broadcastGamesThisTournament => 'Partijos šiame turnyre'; + + @override + String get broadcastScore => 'Taškų skaičius'; + + @override + String get broadcastAllTeams => 'Visos komandos'; + + @override + String get broadcastTournamentFormat => 'Turnyro formatas'; + + @override + String get broadcastTournamentLocation => 'Turnyro vieta'; + + @override + String get broadcastTopPlayers => 'Geriausi žaidėjai'; + + @override + String get broadcastTimezone => 'Laiko juosta'; + + @override + String get broadcastFideRatingCategory => 'FIDE reitingo kategorija'; + + @override + String get broadcastOptionalDetails => 'Papildoma informacija'; + + @override + String get broadcastPastBroadcasts => 'Ankstesnės transliacijos'; + + @override + String get broadcastAllBroadcastsByMonth => 'Rodyti visas transliacijas pagal mėnesį'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transliacijų', + many: '$count transliacijos', + few: '$count transliacijos', + one: '$count transliacija', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Iššūkiai: $param1'; @@ -643,6 +900,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get preferencesInGameOnly => 'Tik žaidimo metu'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Žaidimo laikrodis'; @@ -784,6 +1044,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Pranešimų varpelio garsas'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Užduotys'; @@ -1434,10 +1697,10 @@ class AppLocalizationsLt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Priešininkas apribotas ėjimais, kuriuos gali padaryti, ir visi jo ėjimai tik pabloginą jo poziciją.'; @override - String get puzzleThemeHealthyMix => 'Visko po truputį'; + String get puzzleThemeMix => 'Visko po truputį'; @override - String get puzzleThemeHealthyMixDescription => 'Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.'; + String get puzzleThemeMixDescription => 'Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.'; @override String get puzzleThemePlayerGames => 'Žaidėjų žaidimai'; @@ -1493,7 +1756,7 @@ class AppLocalizationsLt extends AppLocalizations { String get waitingForOpponent => 'Laukiama varžovo'; @override - String get orLetYourOpponentScanQrCode => 'Arba leiskite priešininkui nuskanuoti šį QR kodą'; + String get orLetYourOpponentScanQrCode => 'Arba leiskite priešininkui nuskenuoti šį QR kodą'; @override String get waiting => 'Laukiama'; @@ -1510,7 +1773,7 @@ class AppLocalizationsLt extends AppLocalizations { String get level => 'Lygis'; @override - String get strength => 'Stiprumas'; + String get strength => 'Pasipriešinimo stiprumas'; @override String get toggleTheChat => 'Įjungti / išjungti pokalbį'; @@ -1534,10 +1797,10 @@ class AppLocalizationsLt extends AppLocalizations { String get black => 'Juodieji'; @override - String get asWhite => 'kaip baltieji'; + String get asWhite => 'už baltuosius'; @override - String get asBlack => 'kaip juodieji'; + String get asBlack => 'už juoduosius'; @override String get randomColor => 'Atsitiktinė spalva'; @@ -1552,10 +1815,10 @@ class AppLocalizationsLt extends AppLocalizations { String get blackIsVictorious => 'Juodieji laimėjo'; @override - String get youPlayTheWhitePieces => 'Žaidžiate baltomis figūromis'; + String get youPlayTheWhitePieces => 'Jūs žaidžiate baltosiomis figūromis'; @override - String get youPlayTheBlackPieces => 'Žaidžiate juodomis figūromis'; + String get youPlayTheBlackPieces => 'Jūs žaidžiate juodosiomis figūromis'; @override String get itsYourTurn => 'Jūsų ėjimas!'; @@ -1811,9 +2074,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get byCPL => 'Pagal įvertį'; - @override - String get openStudy => 'Atverti studiją'; - @override String get enable => 'Įjungti'; @@ -1841,9 +2101,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get removesTheDepthLimit => 'Panaikina gylio limitą ir neleidžia kompiuteriui atvėsti'; - @override - String get engineManager => 'Variklių valdymas'; - @override String get blunder => 'Šiurkšti klaida'; @@ -1922,7 +2179,7 @@ class AppLocalizationsLt extends AppLocalizations { String get friends => 'Draugai'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'kiti žaidėjai'; @override String get discussions => 'Diskusijos'; @@ -2107,6 +2364,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get gamesPlayed => 'sužaistos partijos'; + @override + String get ok => 'OK'; + @override String get cancel => 'Atšaukti'; @@ -2481,9 +2741,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get unblock => 'Atblokuoti'; - @override - String get followsYou => 'Seka jus'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 pradėjo sekti $param2'; @@ -2756,10 +3013,10 @@ class AppLocalizationsLt extends AppLocalizations { String get yes => 'Taip'; @override - String get website => 'Website'; + String get website => 'Tinklapis'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobilus'; @override String get help => 'Pagalba:'; @@ -2816,7 +3073,13 @@ class AppLocalizationsLt extends AppLocalizations { String get other => 'Kita'; @override - String get reportDescriptionHelp => 'Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.'; + String get reportCheatBoostHelp => 'Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.'; + + @override + String get reportUsernameHelp => 'Paaiškinkite, kuo šis vartotojo vardas yra įžeidžiantis. Nesakykite tiesiog „tai įžeidžia/netinkama“, bet papasakokite, kaip priėjote prie šios išvados, ypač jei įžeidimas yra užmaskuotas, ne anglų kalba, yra slengas arba yra istorinė / kultūrinė nuoroda.'; + + @override + String get reportProcessedFasterInEnglish => 'Jūsų pranešimas bus apdorotas greičiau, jei jis bus parašytas anglų kalba.'; @override String get error_provideOneCheatedGameLink => 'Pateikite bent vieną nuorodą į partiją, kurioje buvo sukčiauta.'; @@ -4121,6 +4384,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get nothingToSeeHere => 'Nieko naujo.'; + @override + String get stats => 'Statistika'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4129,7 +4395,7 @@ class AppLocalizationsLt extends AppLocalizations { other: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', many: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', few: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', - one: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundės.', + one: 'Jūsų varžovas paliko partiją. Galite reikalauti pergalės už $count sekundės.', ); return '$_temp0'; } @@ -4855,9 +5121,731 @@ class AppLocalizationsLt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess transliuotojai'; + @override + String get studyPrivate => 'Privati'; + + @override + String get studyMyStudies => 'Mano studijos'; + + @override + String get studyStudiesIContributeTo => 'Studijos, kuriose prisidedu'; + + @override + String get studyMyPublicStudies => 'Mano viešos studijos'; + + @override + String get studyMyPrivateStudies => 'Mano privačios studijos'; + + @override + String get studyMyFavoriteStudies => 'Mano mėgstamiausios studijos'; + + @override + String get studyWhatAreStudies => 'Kas yra studijos?'; + + @override + String get studyAllStudies => 'Visos studijos'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studijos, sukurtos $param'; + } + + @override + String get studyNoneYet => 'Dar nėra.'; + + @override + String get studyHot => 'Populiaru dabar'; + + @override + String get studyDateAddedNewest => 'Sukūrimo data (naujausios)'; + + @override + String get studyDateAddedOldest => 'Sukūrimo data (seniausios)'; + + @override + String get studyRecentlyUpdated => 'Neseniai atnaujintos'; + + @override + String get studyMostPopular => 'Populiariausios'; + + @override + String get studyAlphabetical => 'Abėcėlės tvarka'; + + @override + String get studyAddNewChapter => 'Pridėti naują skyrių'; + + @override + String get studyAddMembers => 'Pridėti narių'; + + @override + String get studyInviteToTheStudy => 'Pakviesti į studiją'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Kvieskite tik pažįstamus žmones, ir tik norinčius dalyvauti šioje studijoje.'; + + @override + String get studySearchByUsername => 'Ieškoti pagal naudotojo vardą'; + + @override + String get studySpectator => 'Žiūrovas'; + + @override + String get studyContributor => 'Talkininkas'; + + @override + String get studyKick => 'Išmesti'; + + @override + String get studyLeaveTheStudy => 'Palikti studiją'; + + @override + String get studyYouAreNowAContributor => 'Dabar esate talkininkas'; + + @override + String get studyYouAreNowASpectator => 'Dabar esate žiūrovas'; + + @override + String get studyPgnTags => 'PGN žymos'; + + @override + String get studyLike => 'Mėgti'; + + @override + String get studyUnlike => 'Nebemėgti'; + + @override + String get studyNewTag => 'Nauja žyma'; + + @override + String get studyCommentThisPosition => 'Komentuoti šią poziciją'; + + @override + String get studyCommentThisMove => 'Komentuoti šį ėjimą'; + + @override + String get studyAnnotateWithGlyphs => 'Komentuoti su glifais'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Skyrius yra per trumpas analizei.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tik studijos talkininkai gali prašyti kompiuterio analizės.'; + + @override + String get studyGetAFullComputerAnalysis => 'Gaukite pilną pagrindinės linijos kompiuterio analizę.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Įsitikinkite, kad skyrius užbaigtas. Analizės galite prašyti tik kartą.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Visi SYNC nariai lieka toje pačioje pozicijoje'; + + @override + String get studyShareChanges => 'Dalinkitės pakeitimais su žiūrovais ir saugokite juos serveryje'; + + @override + String get studyPlaying => 'Žaidžiama'; + + @override + String get studyShowEvalBar => 'Vertinimo skalė'; + + @override + String get studyFirst => 'Pirmas'; + + @override + String get studyPrevious => 'Ankstesnis'; + + @override + String get studyNext => 'Kitas'; + + @override + String get studyLast => 'Paskutinis'; + @override String get studyShareAndExport => 'Dalintis ir eksportuoti'; + @override + String get studyCloneStudy => 'Klonuoti'; + + @override + String get studyStudyPgn => 'Studijos PGN'; + + @override + String get studyDownloadAllGames => 'Atsisiųsti visus žaidimus'; + + @override + String get studyChapterPgn => 'Skyriaus PGN'; + + @override + String get studyCopyChapterPgn => 'Kopijuoti PGN'; + + @override + String get studyDownloadGame => 'Atsisiųsti žaidimą'; + + @override + String get studyStudyUrl => 'Studijos URL'; + + @override + String get studyCurrentChapterUrl => 'Dabartinio skyriaus URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Galite įklijuoti šį forume norėdami įterpti'; + + @override + String get studyStartAtInitialPosition => 'Pradėti pradinėje pozicijoje'; + + @override + String studyStartAtX(String param) { + return 'Pradėti nuo $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Įterpti savo svetainėje ar tinklaraštyje'; + + @override + String get studyReadMoreAboutEmbedding => 'Skaitykite daugiau apie įterpimą'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Gali būti įterptos tik viešos studijos!'; + + @override + String get studyOpen => 'Atverti'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 iš $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nerasta'; + + @override + String get studyEditChapter => 'Redaguoti skyrių'; + + @override + String get studyNewChapter => 'Naujas skyrius'; + + @override + String studyImportFromChapterX(String param) { + return 'Importuoti iš $param'; + } + + @override + String get studyOrientation => 'Kryptis'; + + @override + String get studyAnalysisMode => 'Analizės režimas'; + + @override + String get studyPinnedChapterComment => 'Prisegtas skyriaus komentaras'; + + @override + String get studySaveChapter => 'Išsaugoti skyrių'; + + @override + String get studyClearAnnotations => 'Pašalinti anotacijas'; + + @override + String get studyClearVariations => 'Išvalyti variacijas'; + + @override + String get studyDeleteChapter => 'Ištrinti skyrių'; + + @override + String get studyDeleteThisChapter => 'Ištrinti šį skyrių? Nėra kelio atgal!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Išvalyti visus komentarus, ženklus ir figūras šiame skyriuje?'; + + @override + String get studyRightUnderTheBoard => 'Iš karto po lenta'; + + @override + String get studyNoPinnedComment => 'Jokio'; + + @override + String get studyNormalAnalysis => 'Įprasta analizė'; + + @override + String get studyHideNextMoves => 'Slėpti kitus ėjimus'; + + @override + String get studyInteractiveLesson => 'Interaktyvi pamoka'; + + @override + String studyChapterX(String param) { + return 'Skyrius $param'; + } + + @override + String get studyEmpty => 'Tuščia'; + + @override + String get studyStartFromInitialPosition => 'Pradėti nuo pirminės pozicijos'; + + @override + String get studyEditor => 'Redaktorius'; + + @override + String get studyStartFromCustomPosition => 'Pradėti nuo tinkintos pozicijos'; + + @override + String get studyLoadAGameByUrl => 'Pakrauti partijas iš adresų'; + + @override + String get studyLoadAPositionFromFen => 'Pakrauti poziciją iš FEN'; + + @override + String get studyLoadAGameFromPgn => 'Pakrauti partijas iš PGN'; + + @override + String get studyAutomatic => 'Automatinis'; + + @override + String get studyUrlOfTheGame => 'Partijų adresai, vienas per eilutę'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Pakrauti partijas iš $param1 arba $param2'; + } + + @override + String get studyCreateChapter => 'Sukurti skyrių'; + + @override + String get studyCreateStudy => 'Sukurti studiją'; + + @override + String get studyEditStudy => 'Redaguoti studiją'; + + @override + String get studyVisibility => 'Matomumas'; + + @override + String get studyPublic => 'Viešas'; + + @override + String get studyUnlisted => 'Nėra sąraše'; + + @override + String get studyInviteOnly => 'Tik su pakvietimu'; + + @override + String get studyAllowCloning => 'Leisti kopijuoti'; + + @override + String get studyNobody => 'Niekam'; + + @override + String get studyOnlyMe => 'Tik man'; + + @override + String get studyContributors => 'Dalyviams'; + + @override + String get studyMembers => 'Nariams'; + + @override + String get studyEveryone => 'Visiems'; + + @override + String get studyEnableSync => 'Įgalinti sinchronizaciją'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Taip: visiems rodyti tą pačią poziciją'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: leisti žmonėms naršyti laisvai'; + + @override + String get studyPinnedStudyComment => 'Prisegtas studijos komentaras'; + @override String get studyStart => 'Pradėti'; + + @override + String get studySave => 'Išsaugoti'; + + @override + String get studyClearChat => 'Išvalyti pokalbį'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ištrinti studijos pokalbių istoriją? Nėra kelio atgal!'; + + @override + String get studyDeleteStudy => 'Ištrinti studiją'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ištrinti visą studiją? Ištrynimas negrįžtamas. Norėdami tęsti įrašykite studijos pavadinimą: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kur norite tai studijuoti?'; + + @override + String get studyGoodMove => 'Geras ėjimas'; + + @override + String get studyMistake => 'Klaida'; + + @override + String get studyBrilliantMove => 'Puikus ėjimas'; + + @override + String get studyBlunder => 'Šiurkšti klaida'; + + @override + String get studyInterestingMove => 'Įdomus ėjimas'; + + @override + String get studyDubiousMove => 'Abejotinas ėjimas'; + + @override + String get studyOnlyMove => 'Vienintelis ėjimas'; + + @override + String get studyZugzwang => 'Cugcvangas'; + + @override + String get studyEqualPosition => 'Lygi pozicija'; + + @override + String get studyUnclearPosition => 'Neaiški pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Šiek tiek geriau baltiesiems'; + + @override + String get studyBlackIsSlightlyBetter => 'Šiek tiek geriau juodiesiems'; + + @override + String get studyWhiteIsBetter => 'Geriau baltiesiems'; + + @override + String get studyBlackIsBetter => 'Geriau juodiesiems'; + + @override + String get studyWhiteIsWinning => 'Laimi baltieji'; + + @override + String get studyBlackIsWinning => 'Laimi juodieji'; + + @override + String get studyNovelty => 'Naujovė'; + + @override + String get studyDevelopment => 'Plėtojimas'; + + @override + String get studyInitiative => 'Iniciatyva'; + + @override + String get studyAttack => 'Ataka'; + + @override + String get studyCounterplay => 'Kontraėjimas'; + + @override + String get studyTimeTrouble => 'Laiko problemos'; + + @override + String get studyWithCompensation => 'Su kompensacija'; + + @override + String get studyWithTheIdea => 'Su mintimi'; + + @override + String get studyNextChapter => 'Kitas skyrius'; + + @override + String get studyPrevChapter => 'Ankstenis skyrius'; + + @override + String get studyStudyActions => 'Studijos veiksmai'; + + @override + String get studyTopics => 'Temos'; + + @override + String get studyMyTopics => 'Mano temos'; + + @override + String get studyPopularTopics => 'Populiarios temos'; + + @override + String get studyManageTopics => 'Valdyti temas'; + + @override + String get studyBack => 'Atgal'; + + @override + String get studyPlayAgain => 'Žaisti dar kartą'; + + @override + String get studyWhatWouldYouPlay => 'Ar norėtumėte žaisti nuo šios pozicijos?'; + + @override + String get studyYouCompletedThisLesson => 'Sveikiname! Jūs pabaigėte šią pamoką.'; + + @override + String studyPerPage(String param) { + return '$param puslapyje'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count skyrių', + many: '$count skyrių', + few: '$count skyriai', + one: '$count skyrius', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partijų', + many: '$count partijų', + few: '$count partijos', + one: '$count partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count narių', + many: '$count narių', + few: '$count nariai', + one: '$count narys', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimų', + many: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimo', + few: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimų', + one: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimo', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'ką tik'; + + @override + String get timeagoRightNow => 'dabar'; + + @override + String get timeagoCompleted => 'užbaigta'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count sekundžių', + many: 'po $count sekundės', + few: 'po $count sekundžių', + one: 'po $count sekundės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count minučių', + many: 'po $count minutės', + few: 'po $count minučių', + one: 'po $count minutės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count valandų', + many: 'po $count valandos', + few: 'po $count valandų', + one: 'po $count valandos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count dienų', + many: 'po $count dienos', + few: 'po $count dienų', + one: 'po $count dienos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count savaičių', + many: 'po $count savaitės', + few: 'po $count savaičių', + one: 'po $count savaitės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count mėnesių', + many: 'po $count mėnesio', + few: 'po $count mėnesių', + one: 'po $count mėnesio', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count metų', + many: 'po $count metų', + few: 'po $count metų', + one: 'po $count metų', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count minučių', + many: 'Prieš $count minutės', + few: 'Prieš $count minutes', + one: 'Prieš $count minutę', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count valandų', + many: 'Prieš $count valandos', + few: 'Prieš $count valandas', + one: 'Prieš $count valandą', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count dienų', + many: 'Prieš $count dienos', + few: 'Prieš $count dienas', + one: 'Prieš $count dieną', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count savaičių', + many: 'Prieš $count savaitės', + few: 'Prieš $count savaites', + one: 'Prieš $count savaitę', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count mėnesių', + many: 'Prieš $count mėnesio', + few: 'Prieš $count mėnesius', + one: 'Prieš $count mėnesį', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count metų', + many: 'Prieš $count metų', + few: 'Prieš $count metus', + one: 'Prieš $count metus', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liko $count minučių', + many: 'Liko $count minučių', + few: 'Liko $count minutės', + one: 'Liko $count minutė', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liko $count valandų', + many: 'Liko $count valandų', + few: 'Liko $count valandos', + one: 'Liko $count valanda', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index 4111b6e32d..6f03a65fca 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsLv extends AppLocalizations { AppLocalizationsLv([String locale = 'lv']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsLv extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivitāte'; @@ -254,6 +251,17 @@ class AppLocalizationsLv extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +373,256 @@ class AppLocalizationsLv extends AppLocalizations { @override String get broadcastBroadcasts => 'Raidījumi'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Reāllaika turnīru raidījumi'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Jauns reāllaika raidījums'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Pievienot raundu'; + + @override + String get broadcastOngoing => 'Notiekošie'; + + @override + String get broadcastUpcoming => 'Gaidāmie'; + + @override + String get broadcastCompleted => 'Notikušie'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Raunda nosaukums'; + + @override + String get broadcastRoundNumber => 'Raunda skaitlis'; + + @override + String get broadcastTournamentName => 'Turnīra nosaukums'; + + @override + String get broadcastTournamentDescription => 'Īss turnīra apraksts'; + + @override + String get broadcastFullDescription => 'Pilns pasākuma apraksts'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobligāts garš raidījuma apraksts. Pieejams $param1. Garumam jābūt mazāk kā $param2 rakstzīmēm.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, ko Lichess aptaujās, lai iegūtu PGN atjauninājumus. Tam jābūt publiski piekļūstamam no interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobligāts, ja zināt, kad pasākums sākas'; + + @override + String get broadcastCurrentGameUrl => 'Pašreizējās spēles URL'; + + @override + String get broadcastDownloadAllRounds => 'Lejupielādēt visus raundus'; + + @override + String get broadcastResetRound => 'Atiestatīt šo raundu'; + + @override + String get broadcastDeleteRound => 'Dzēst šo raundu'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Neatgriezeniski dzēst raundu un tā spēles.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izdzēst visas šī raunda spēles. To atjaunošanai būs nepieciešams aktīvs avots.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izaicinājumi: $param1'; @@ -626,6 +881,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šaha pulkstenis'; @@ -767,6 +1025,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Paziņojumu skaņa'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Uzdevumi'; @@ -1412,10 +1673,10 @@ class AppLocalizationsLv extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Pretiniekam ir ierobežoti iespējamie gājieni, un visi no tiem pasliktina pretinieka pozīciju.'; @override - String get puzzleThemeHealthyMix => 'Veselīgs sajaukums'; + String get puzzleThemeMix => 'Veselīgs sajaukums'; @override - String get puzzleThemeHealthyMixDescription => 'Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.'; + String get puzzleThemeMixDescription => 'Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.'; @override String get puzzleThemePlayerGames => 'Spēlētāja spēles'; @@ -1789,9 +2050,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get byCPL => 'Pēc CPL'; - @override - String get openStudy => 'Atvērt izpēti'; - @override String get enable => 'Iespējot'; @@ -1819,9 +2077,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get removesTheDepthLimit => 'Noņem dziļuma ierobežojumu un uztur tavu datoru siltu'; - @override - String get engineManager => 'Dzinēja pārvaldnieks'; - @override String get blunder => 'Rupja kļūda'; @@ -2085,6 +2340,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get gamesPlayed => 'Izspēlētās spēles'; + @override + String get ok => 'OK'; + @override String get cancel => 'Atcelt'; @@ -2459,9 +2717,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get unblock => 'Atbloķēt'; - @override - String get followsYou => 'Sekotāji'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 sāka sekot $param2'; @@ -2794,7 +3049,13 @@ class AppLocalizationsLv extends AppLocalizations { String get other => 'Cits'; @override - String get reportDescriptionHelp => 'Ielīmējiet spēles saiti un paskaidrojiet, kas nav kārtībā ar lietotāja uzvedību. Nepietiks, ja tikai norādīsiet, ka \"lietotājs krāpjas\" — lūdzu, pastāstiet, kā nonācāt pie šī secinājuma. Ja jūsu ziņojums būs rakstīts angliski, par to varēsim parūpēties ātrāk.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Lūdzu, norādiet vismaz vienu saiti uz spēli, kurā pretinieks ir krāpies.'; @@ -4099,6 +4360,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4789,9 +5053,710 @@ class AppLocalizationsLv extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess straumētāji'; + @override + String get studyPrivate => 'Privāta'; + + @override + String get studyMyStudies => 'Manas izpētes'; + + @override + String get studyStudiesIContributeTo => 'Izpētes, kurās piedalos'; + + @override + String get studyMyPublicStudies => 'Manas publiskās izpētes'; + + @override + String get studyMyPrivateStudies => 'Manas privātās izpētes'; + + @override + String get studyMyFavoriteStudies => 'Mana izpēšu izlase'; + + @override + String get studyWhatAreStudies => 'Kas ir izpētes?'; + + @override + String get studyAllStudies => 'Visas izpētes'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Izpētes, ko izveidoja $param'; + } + + @override + String get studyNoneYet => 'Pagaidām nevienas.'; + + @override + String get studyHot => 'Nesen populārās'; + + @override + String get studyDateAddedNewest => 'Pievienošanas datums (jaunākās)'; + + @override + String get studyDateAddedOldest => 'Pievienošanas datums (vecākās)'; + + @override + String get studyRecentlyUpdated => 'Nesen atjaunotās'; + + @override + String get studyMostPopular => 'Populārākās'; + + @override + String get studyAlphabetical => 'Alfabētiskā secībā'; + + @override + String get studyAddNewChapter => 'Pievienot nodaļu'; + + @override + String get studyAddMembers => 'Pievienot dalībniekus'; + + @override + String get studyInviteToTheStudy => 'Ielūgt uz izpēti'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Lūdzu, ielūdziet tikai cilvēkus, kurus pazīstat un kuri vēlas pievienoties izpētei.'; + + @override + String get studySearchByUsername => 'Meklēt pēc lietotājvārda'; + + @override + String get studySpectator => 'Skatītājs'; + + @override + String get studyContributor => 'Ieguldītājs'; + + @override + String get studyKick => 'Izmest'; + + @override + String get studyLeaveTheStudy => 'Pamest izpēti'; + + @override + String get studyYouAreNowAContributor => 'Tagad esat ieguldītājs'; + + @override + String get studyYouAreNowASpectator => 'Tagad esat skatītājs'; + + @override + String get studyPgnTags => 'PGN birkas'; + + @override + String get studyLike => 'Patīk'; + + @override + String get studyUnlike => 'Noņemt atzīmi \"patīk\"'; + + @override + String get studyNewTag => 'Jauna birka'; + + @override + String get studyCommentThisPosition => 'Komentēt šo pozīciju'; + + @override + String get studyCommentThisMove => 'Komentēt šo gājienu'; + + @override + String get studyAnnotateWithGlyphs => 'Anotēt ar glifiem'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Šī nodaļa ir par īsu lai to analizētu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tikai izpētes ieguldītāji var pieprasīt datoranalīzi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Iegūstiet pilnu servera puses pamatvarianta datoranalīzi.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Pārliecinieties, ka nodaļa ir pabeigta. Datoranalīzi var pieprasīt tikai vienreiz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Visi SYNC dalībnieki paliek vienā pozīcijā'; + + @override + String get studyShareChanges => 'Koplietot izmaiņas ar skatītājiem un saglabāt tās serverī'; + + @override + String get studyPlaying => 'Notiek'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Pirmais'; + + @override + String get studyPrevious => 'Iepriekšējais'; + + @override + String get studyNext => 'Nākamais'; + + @override + String get studyLast => 'Pēdējais'; + @override String get studyShareAndExport => 'Koplietot & eksportēt'; + @override + String get studyCloneStudy => 'Klonēt'; + + @override + String get studyStudyPgn => 'Izpētes PGN'; + + @override + String get studyDownloadAllGames => 'Lejupielādēt visas spēles'; + + @override + String get studyChapterPgn => 'Nodaļas PGN'; + + @override + String get studyCopyChapterPgn => 'Kopēt PGN'; + + @override + String get studyDownloadGame => 'Lejupielādēt spēli'; + + @override + String get studyStudyUrl => 'Izpētes URL'; + + @override + String get studyCurrentChapterUrl => 'Pašreizējās nodaļas URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Šo varat ielīmēt forumā, lai iegultu'; + + @override + String get studyStartAtInitialPosition => 'Sākt no sākotnējās pozīcijas'; + + @override + String studyStartAtX(String param) { + return 'Sākt ar $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Iegult savā mājaslapā vai blogā'; + + @override + String get studyReadMoreAboutEmbedding => 'Lasīt vairāk par iegulšanu'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Iegult var tikai publiskas izpētes!'; + + @override + String get studyOpen => 'Atvērt'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 piedāvā \"$param1\"'; + } + + @override + String get studyStudyNotFound => 'Izpēte nav atrasta'; + + @override + String get studyEditChapter => 'Rediģēt nodaļu'; + + @override + String get studyNewChapter => 'Jauna nodaļa'; + + @override + String studyImportFromChapterX(String param) { + return 'Importēt no $param'; + } + + @override + String get studyOrientation => 'Orientācija'; + + @override + String get studyAnalysisMode => 'Analīzes režīms'; + + @override + String get studyPinnedChapterComment => 'Piesprausts nodaļas komentārs'; + + @override + String get studySaveChapter => 'Saglabāt nodaļu'; + + @override + String get studyClearAnnotations => 'Notīrīt piezīmes'; + + @override + String get studyClearVariations => 'Notīrīt variantus'; + + @override + String get studyDeleteChapter => 'Dzēst nodaļu'; + + @override + String get studyDeleteThisChapter => 'Vai dzēst šo nodaļu? Atpakaļceļa nav!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Notīrīt visus komentārus un figūras šajā nodaļā?'; + + @override + String get studyRightUnderTheBoard => 'Tieši zem galdiņa'; + + @override + String get studyNoPinnedComment => 'Neviens'; + + @override + String get studyNormalAnalysis => 'Parasta analīze'; + + @override + String get studyHideNextMoves => 'Slēpt turpmākos gājienus'; + + @override + String get studyInteractiveLesson => 'Interaktīva nodarbība'; + + @override + String studyChapterX(String param) { + return '$param. nodaļa'; + } + + @override + String get studyEmpty => 'Tukšs'; + + @override + String get studyStartFromInitialPosition => 'Sākt no sākotnējās pozīcijas'; + + @override + String get studyEditor => 'Redaktors'; + + @override + String get studyStartFromCustomPosition => 'Sākt no pielāgotas pozīcijas'; + + @override + String get studyLoadAGameByUrl => 'Ielādēt spēli, norādot URL'; + + @override + String get studyLoadAPositionFromFen => 'Ielādēt pozīciju no FEN'; + + @override + String get studyLoadAGameFromPgn => 'Ielādēt spēli no PGN'; + + @override + String get studyAutomatic => 'Automātisks'; + + @override + String get studyUrlOfTheGame => 'Spēles URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ielādēt spēli no $param1 vai $param2'; + } + + @override + String get studyCreateChapter => 'Izveidot nodaļu'; + + @override + String get studyCreateStudy => 'Izveidot izpēti'; + + @override + String get studyEditStudy => 'Rediģēt izpēti'; + + @override + String get studyVisibility => 'Redzamība'; + + @override + String get studyPublic => 'Publiska'; + + @override + String get studyUnlisted => 'Nerindota'; + + @override + String get studyInviteOnly => 'Tikai ar ielūgumu'; + + @override + String get studyAllowCloning => 'Atļaut dublēšanu'; + + @override + String get studyNobody => 'Neviens'; + + @override + String get studyOnlyMe => 'Tikai es'; + + @override + String get studyContributors => 'Ieguldītāji'; + + @override + String get studyMembers => 'Dalībnieki'; + + @override + String get studyEveryone => 'Visi'; + + @override + String get studyEnableSync => 'Iespējot sinhronizāciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jā: paturēt visus vienā pozīcijā'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nē: ļaut katram brīvi pārlūkot'; + + @override + String get studyPinnedStudyComment => 'Piesprausts izpētes komentārs'; + @override String get studyStart => 'Sākt'; + + @override + String get studySave => 'Saglabāt'; + + @override + String get studyClearChat => 'Notīrīt saraksti'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vai dzēst izpētes sarakstes vēsturi? Atpakaļceļa nav!'; + + @override + String get studyDeleteStudy => 'Dzēst izpēti'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Dzēst visu izpēti? Atpakaļceļa nav! Ievadiet izpētes nosaukumu, lai apstiprinātu: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kur vēlaties to izpētīt?'; + + @override + String get studyGoodMove => 'Labs gājiens'; + + @override + String get studyMistake => 'Kļūda'; + + @override + String get studyBrilliantMove => 'Izcils gājiens'; + + @override + String get studyBlunder => 'Rupja kļūda'; + + @override + String get studyInterestingMove => 'Interesants gājiens'; + + @override + String get studyDubiousMove => 'Apšaubāms gājiens'; + + @override + String get studyOnlyMove => 'Vienīgais gājiens'; + + @override + String get studyZugzwang => 'Gājiena spaids'; + + @override + String get studyEqualPosition => 'Vienlīdzīga pozīcija'; + + @override + String get studyUnclearPosition => 'Neskaidra pozīcija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Baltajiem nedaudz labāka pozīcija'; + + @override + String get studyBlackIsSlightlyBetter => 'Melnajiem nedaudz labāka pozīcija'; + + @override + String get studyWhiteIsBetter => 'Baltajiem labāka pozīcija'; + + @override + String get studyBlackIsBetter => 'Melnajiem labāka pozīcija'; + + @override + String get studyWhiteIsWinning => 'Baltie tuvojas uzvarai'; + + @override + String get studyBlackIsWinning => 'Melnie tuvojas uzvarai'; + + @override + String get studyNovelty => 'Oriģināls gājiens'; + + @override + String get studyDevelopment => 'Attīstība'; + + @override + String get studyInitiative => 'Iniciatīva'; + + @override + String get studyAttack => 'Uzbrukums'; + + @override + String get studyCounterplay => 'Pretspēle'; + + @override + String get studyTimeTrouble => 'Laika trūkuma grūtības'; + + @override + String get studyWithCompensation => 'Ar atlīdzinājumu'; + + @override + String get studyWithTheIdea => 'Ar domu'; + + @override + String get studyNextChapter => 'Nākamā nodaļa'; + + @override + String get studyPrevChapter => 'Iepriekšējā nodaļa'; + + @override + String get studyStudyActions => 'Izpētes darbības'; + + @override + String get studyTopics => 'Temati'; + + @override + String get studyMyTopics => 'Mani temati'; + + @override + String get studyPopularTopics => 'Populāri temati'; + + @override + String get studyManageTopics => 'Pārvaldīt tematus'; + + @override + String get studyBack => 'Atpakaļ'; + + @override + String get studyPlayAgain => 'Spēlēt vēlreiz'; + + @override + String get studyWhatWouldYouPlay => 'Kā jūs spēlētu šādā pozīcijā?'; + + @override + String get studyYouCompletedThisLesson => 'Apsveicam! Pabeidzāt šo nodarbību.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Nodaļas', + one: '$count Nodaļa', + zero: '$count Nodaļas', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Spēles', + one: '$count Spēle', + zero: '$count Spēles', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Dalībnieki', + one: '$count Dalībnieks', + zero: '$count Dalībnieki', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēles', + one: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēli', + zero: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēles', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'tikko'; + + @override + String get timeagoRightNow => 'tieši tagad'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count sekundēm', + one: 'pēc $count sekundes', + zero: 'pēc $count sekundēm', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count minūtēm', + one: 'pēc $count minūtes', + zero: 'pēc $count minūtēm', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count stundām', + one: 'pēc $count stundas', + zero: 'pēc $count stundām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count dienām', + one: 'pēc $count dienas', + zero: 'pēc $count dienām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count nedēļām', + one: 'pēc $count nedēļas', + zero: 'pēc $count nedēļām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count mēnešiem', + one: 'pēc $count mēneša', + zero: 'pēc $count mēnešiem', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count gadiem', + one: 'pēc $count gada', + zero: 'pēc $count gadiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count minūtēm', + one: 'pirms $count minūtes', + zero: 'pirms $count minūtēm', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count stundām', + one: 'pirms $count stundas', + zero: 'pirms $count stundām', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count dienām', + one: 'pirms $count dienas', + zero: 'pirms $count dienām', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count nedēļām', + one: 'pirms $count nedēļas', + zero: 'pirms $count nedēļām', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count mēnešiem', + one: 'pirms $count mēneša', + zero: 'pirms $count mēnešiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count gadiem', + one: 'pirms $count gada', + zero: 'pirms $count gadiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 91912a2485..97db7f6e43 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsMk extends AppLocalizations { AppLocalizationsMk([String locale = 'mk']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Повратна информација'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsMk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Тактилен фидбек'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Системски бои'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Активност'; @@ -246,6 +243,17 @@ class AppLocalizationsMk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsMk extends AppLocalizations { @override String get broadcastBroadcasts => 'Емитувања'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Пренос на турнири во живо'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Ново емитување во живо'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Во тек'; + + @override + String get broadcastUpcoming => 'Претстојни'; + + @override + String get broadcastCompleted => 'Завршени'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Заокружен број'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Цел опис на настанот'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Незадолжителен, долг опис на емитуваниот настан. $param1 е достапен. Должината мора да е пократка од $param2 знаци.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL кое Lichess ќе го користи за ажурирање на PGN датотеката. Мора да биде јавно достапно на интернет.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Незадолжително, ако знаете кога почнува настанот'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховски часовник'; @@ -750,6 +1008,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Загатки'; @@ -1390,10 +1651,10 @@ class AppLocalizationsMk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'Healthy mix'; + String get puzzleThemeMix => 'Healthy mix'; @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1767,9 +2028,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get byCPL => 'По CPL'; - @override - String get openStudy => 'Отвори студија'; - @override String get enable => 'Овозможи'; @@ -1797,9 +2055,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get removesTheDepthLimit => 'Неограничена длабочина на анализа, го загрева вашиот компјутер'; - @override - String get engineManager => 'Менаџер на компјутерот'; - @override String get blunder => 'Глупа грешка'; @@ -2063,6 +2318,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get gamesPlayed => 'Одиграни партии'; + @override + String get ok => 'OK'; + @override String get cancel => 'Откажи'; @@ -2437,9 +2695,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get unblock => 'Одблокирај'; - @override - String get followsYou => 'Те следи'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 почна да го следи $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsMk extends AppLocalizations { String get other => 'Друго'; @override - String get reportDescriptionHelp => 'Внесете линк од играта/игрите и објаснете каде е проблемот во однесувањето на овој корисник. Немојте само да обвините за мамење, туку објаснете како дојдовте до тој заклучок. Вашата пријава ќе биди разгледана побрзо ако е напишана на англиски јазик.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Ве молиме доставете барем една врска до партија со мамење.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsMk extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamers'; + @override + String get studyPrivate => 'Private'; + + @override + String get studyMyStudies => 'My studies'; + + @override + String get studyStudiesIContributeTo => 'Studies I contribute to'; + + @override + String get studyMyPublicStudies => 'My public studies'; + + @override + String get studyMyPrivateStudies => 'My private studies'; + + @override + String get studyMyFavoriteStudies => 'My favourite studies'; + + @override + String get studyWhatAreStudies => 'What are studies?'; + + @override + String get studyAllStudies => 'All studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; + } + + @override + String get studyNoneYet => 'None yet.'; + + @override + String get studyHot => 'Hot'; + + @override + String get studyDateAddedNewest => 'Date added (newest)'; + + @override + String get studyDateAddedOldest => 'Date added (oldest)'; + + @override + String get studyRecentlyUpdated => 'Recently updated'; + + @override + String get studyMostPopular => 'Most popular'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Add a new chapter'; + + @override + String get studyAddMembers => 'Add members'; + + @override + String get studyInviteToTheStudy => 'Invite to the study'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; + + @override + String get studySearchByUsername => 'Search by username'; + + @override + String get studySpectator => 'Spectator'; + + @override + String get studyContributor => 'Contributor'; + + @override + String get studyKick => 'Kick'; + + @override + String get studyLeaveTheStudy => 'Leave the study'; + + @override + String get studyYouAreNowAContributor => 'You are now a contributor'; + + @override + String get studyYouAreNowASpectator => 'You are now a spectator'; + + @override + String get studyPgnTags => 'PGN tags'; + + @override + String get studyLike => 'Like'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'New tag'; + + @override + String get studyCommentThisPosition => 'Comment on this position'; + + @override + String get studyCommentThisMove => 'Comment on this move'; + + @override + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analysed.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; + + @override + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'Playing'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'First'; + + @override + String get studyPrevious => 'Previous'; + + @override + String get studyNext => 'Следно'; + + @override + String get studyLast => 'Last'; + @override String get studyShareAndExport => 'Share & export'; + @override + String get studyCloneStudy => 'Clone'; + + @override + String get studyStudyPgn => 'Study PGN'; + + @override + String get studyDownloadAllGames => 'Download all games'; + + @override + String get studyChapterPgn => 'Chapter PGN'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Download game'; + + @override + String get studyStudyUrl => 'Study URL'; + + @override + String get studyCurrentChapterUrl => 'Current chapter URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; + + @override + String get studyStartAtInitialPosition => 'Start at initial position'; + + @override + String studyStartAtX(String param) { + return 'Start at $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вгради во твојот сајт'; + + @override + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; + + @override + String get studyOpen => 'Open'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; + } + + @override + String get studyStudyNotFound => 'Study not found'; + + @override + String get studyEditChapter => 'Edit chapter'; + + @override + String get studyNewChapter => 'New chapter'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Analysis mode'; + + @override + String get studyPinnedChapterComment => 'Pinned chapter comment'; + + @override + String get studySaveChapter => 'Save chapter'; + + @override + String get studyClearAnnotations => 'Clear annotations'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Delete chapter'; + + @override + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; + + @override + String get studyRightUnderTheBoard => 'Right under the board'; + + @override + String get studyNoPinnedComment => 'None'; + + @override + String get studyNormalAnalysis => 'Normal analysis'; + + @override + String get studyHideNextMoves => 'Hide next moves'; + + @override + String get studyInteractiveLesson => 'Interactive lesson'; + + @override + String studyChapterX(String param) { + return 'Chapter $param'; + } + + @override + String get studyEmpty => 'Empty'; + + @override + String get studyStartFromInitialPosition => 'Start from initial position'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start from custom position'; + + @override + String get studyLoadAGameByUrl => 'Load games by URLs'; + + @override + String get studyLoadAPositionFromFen => 'Load a position from FEN'; + + @override + String get studyLoadAGameFromPgn => 'Load games from PGN'; + + @override + String get studyAutomatic => 'Automatic'; + + @override + String get studyUrlOfTheGame => 'URL of the games, one per line'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; + } + + @override + String get studyCreateChapter => 'Create chapter'; + + @override + String get studyCreateStudy => 'Create study'; + + @override + String get studyEditStudy => 'Edit study'; + + @override + String get studyVisibility => 'Visibility'; + + @override + String get studyPublic => 'Public'; + + @override + String get studyUnlisted => 'Unlisted'; + + @override + String get studyInviteOnly => 'Invite only'; + + @override + String get studyAllowCloning => 'Allow cloning'; + + @override + String get studyNobody => 'Nobody'; + + @override + String get studyOnlyMe => 'Only me'; + + @override + String get studyContributors => 'Contributors'; + + @override + String get studyMembers => 'Members'; + + @override + String get studyEveryone => 'Everyone'; + + @override + String get studyEnableSync => 'Enable sync'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; + + @override + String get studyPinnedStudyComment => 'Pinned study comment'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Зачувај'; + + @override + String get studyClearChat => 'Clear chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; + + @override + String get studyDeleteStudy => 'Delete study'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; + + @override + String get studyGoodMove => 'Добар потег'; + + @override + String get studyMistake => 'Грешка'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Глупа грешка'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chapters', + one: '$count Chapter', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Games', + one: '$count Game', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Members', + one: '$count Member', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'тукушто'; + + @override + String get timeagoRightNow => 'Тукушто'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count секунди', + one: 'За $count секунди', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count минути', + one: 'За $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count часови', + one: 'За $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count денови', + one: 'За $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count седмици', + one: 'За $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count месеци', + one: 'За $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count години', + one: 'За $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count минути', + one: 'Пред $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count часа', + one: 'пред $count час', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count дена', + one: 'пред $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count седмици', + one: 'Пред $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count месеци', + one: 'Пред $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count години', + one: 'Пред $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index ee123d534b..ee9e5cc479 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsNb extends AppLocalizations { AppLocalizationsNb([String locale = 'nb']) : super(locale); @override - String get mobileHomeTab => 'Hjem'; + String get mobileAllGames => 'Alle partier'; @override - String get mobilePuzzlesTab => 'Nøtter'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Verktøy'; + String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; @override - String get mobileWatchTab => 'Se'; + String get mobileClearButton => 'Tøm'; @override - String get mobileSettingsTab => 'Valg'; + String get mobileCorrespondenceClearSavedMove => 'Fjern lagret trekk'; @override - String get mobileMustBeLoggedIn => 'Du må være logget inn for å vise denne siden.'; + String get mobileCustomGameJoinAGame => 'Bli med på et parti'; @override - String get mobileSystemColors => 'Systemfarger'; + String get mobileFeedbackButton => 'Tilbakemeldinger'; @override - String get mobileFeedbackButton => 'Tilbakemeldinger'; + String mobileGreeting(String param) { + return 'Hei, $param'; + } @override - String get mobileOkButton => 'Ok'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsHapticFeedback => 'Haptiske tilbakemeldinger'; + String get mobileHideVariation => 'Skjul variant'; @override - String get mobileSettingsImmersiveMode => 'Fordypelsesmodus'; + String get mobileHomeTab => 'Hjem'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.'; + String get mobileLiveStreamers => 'Direktestrømmere'; @override - String get mobileNotFollowingAnyUser => 'Du følger ingen brukere.'; + String get mobileMustBeLoggedIn => 'Du må være logget inn for å vise denne siden.'; @override - String get mobileAllGames => 'Alle partier'; + String get mobileNoSearchResults => 'Ingen treff'; @override - String get mobileRecentSearches => 'Nylige søk'; + String get mobileNotFollowingAnyUser => 'Du følger ingen brukere.'; @override - String get mobileClearButton => 'Tøm'; + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsNb extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen treff'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikker når de dras'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte denne runden?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du mister rekken og poengsummen din blir lagret.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ingenting her, endre filteret'; @override String get mobilePuzzleStormNothingToShow => 'Ingenting her. Spill noen runder med Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne sjakknøtten'; + String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter du klarer i løpet av 3 minutter.'; @override - String get mobileShareGameURL => 'Del URL til partiet'; + String get mobilePuzzleStreakAbortWarning => 'Du mister rekken og poengsummen din blir lagret.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.'; @override - String get mobileSharePositionAsFEN => 'Del stillingen som FEN'; + String get mobilePuzzlesTab => 'Nøtter'; @override - String get mobileShowVariations => 'Vis varianter'; + String get mobileRecentSearches => 'Nylige søk'; @override - String get mobileHideVariation => 'Skjul variant'; + String get mobileSettingsHapticFeedback => 'Haptiske tilbakemeldinger'; @override - String get mobileShowComments => 'Vis kommentarer'; + String get mobileSettingsImmersiveMode => 'Fordypelsesmodus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte denne runden?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ingenting her, endre filteret'; + String get mobileSettingsTab => 'Valg'; @override - String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileCancelDrawOffer => 'Avbryt remistilbud'; + String get mobileShareGameURL => 'Del URL til partiet'; @override - String get mobileWaitingForOpponentToJoin => 'Venter på motstanderen ...'; + String get mobileSharePositionAsFEN => 'Del stillingen som FEN'; @override - String get mobileBlindfoldMode => 'Blindsjakk'; + String get mobileSharePuzzle => 'Del denne sjakknøtten'; @override - String get mobileLiveStreamers => 'Direktestrømmere'; + String get mobileShowComments => 'Vis kommentarer'; @override - String get mobileCustomGameJoinAGame => 'Bli med på et parti'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Fjern lagret trekk'; + String get mobileShowVariations => 'Vis varianter'; @override String get mobileSomethingWentWrong => 'Noe gikk galt.'; @override - String get mobileShowResult => 'Vis resultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.'; + String get mobileSystemColors => 'Systemfarger'; @override - String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter som mulig i løpet av 3 minutter.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Verktøy'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Venter på motstanderen ...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Se'; @override String get activityActivity => 'Aktivitet'; @@ -246,6 +243,17 @@ class AppLocalizationsNb extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Har spilt ferdig $count fjernsjakkpartier i $param2', + one: 'Har spilt ferdig $count fjernsjakkparti i $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsNb extends AppLocalizations { @override String get broadcastBroadcasts => 'Overføringer'; + @override + String get broadcastMyBroadcasts => 'Mine overføringer'; + @override String get broadcastLiveBroadcasts => 'Direkteoverføringer av turneringer'; + @override + String get broadcastBroadcastCalendar => 'Kalender for overføringer'; + + @override + String get broadcastNewBroadcast => 'Ny direkteoverføring'; + + @override + String get broadcastSubscribedBroadcasts => 'Overføringer som du abonnerer på'; + + @override + String get broadcastAboutBroadcasts => 'Om overføringer'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hvordan bruke overføringer hos Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runden vil ha de samme medlemmene og bidragsyterne som den forrige.'; + + @override + String get broadcastAddRound => 'Legg til runde'; + + @override + String get broadcastOngoing => 'Pågående'; + + @override + String get broadcastUpcoming => 'Kommende'; + + @override + String get broadcastCompleted => 'Fullført'; + + @override + String get broadcastCompletedHelp => 'Lichess oppdager fullførte runder basert på kildepartiene. Bruk denne knappen hvis det ikke finnes noen kilde.'; + + @override + String get broadcastRoundName => 'Rundenavn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnavn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivelse av turneringen'; + + @override + String get broadcastFullDescription => 'Full beskrivelse av turneringen'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valgfri lang beskrivelse av turneringen. $param1 er tilgjengelig. Beskrivelsen må være kortere enn $param2 tegn.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL til PGN-kilden'; + + @override + String get broadcastSourceUrlHelp => 'Lenke som Lichess vil hente PGN-oppdateringer fra. Den må være offentlig tilgjengelig på internett.'; + + @override + String get broadcastSourceGameIds => 'Opptil 64 ID-er for partier hos Lichess. De må være adskilt med mellomrom.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringens lokale tidssone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valgfritt, hvis du vet når arrangementet starter'; + + @override + String get broadcastCurrentGameUrl => 'URL for dette partiet'; + + @override + String get broadcastDownloadAllRounds => 'Last ned alle rundene'; + + @override + String get broadcastResetRound => 'Nullstill denne runden'; + + @override + String get broadcastDeleteRound => 'Slett denne runden'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slett runden og tilhørende partier ugjenkallelig.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Slett alle partiene i denne runden. Kilden må være aktiv for å gjenopprette dem.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slett denne turneringen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slett hele turneringen for godt, sammen med alle rundene og alle partiene.'; + + @override + String get broadcastShowScores => 'Vis poeng for spillerne basert på resultater av partiene'; + + @override + String get broadcastReplacePlayerTags => 'Valgfritt: erstatt spillernavn, ratinger og titler'; + + @override + String get broadcastFideFederations => 'FIDE-forbund'; + + @override + String get broadcastTop10Rating => 'Topp 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spillere'; + + @override + String get broadcastFidePlayerNotFound => 'Fant ikke FIDE-spiller'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Forbund'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Uratet'; + + @override + String get broadcastRecentTournaments => 'Nylige turneringer'; + + @override + String get broadcastOpenLichess => 'Åpne i Lichess'; + + @override + String get broadcastTeams => 'Lag'; + + @override + String get broadcastBoards => 'Brett'; + + @override + String get broadcastOverview => 'Oversikt'; + + @override + String get broadcastSubscribeTitle => 'Abonner for å bli varslet når hver runde starter. Du kan velge varselform i kontoinnstillingene dine.'; + + @override + String get broadcastUploadImage => 'Last opp bilde for turneringen'; + + @override + String get broadcastNoBoardsYet => 'Ingen brett. De vises når partiene er lastet opp.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brett kan lastes med en kilde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starter etter $param'; + } + + @override + String get broadcastStartVerySoon => 'Overføringen starter straks.'; + + @override + String get broadcastNotYetStarted => 'Overføringen har ikke startet.'; + + @override + String get broadcastOfficialWebsite => 'Offisiell nettside'; + + @override + String get broadcastStandings => 'Resultatliste'; + + @override + String get broadcastOfficialStandings => 'Offisiell tabell'; + + @override + String broadcastIframeHelp(String param) { + return 'Flere alternativer på $param'; + } + + @override + String get broadcastWebmastersPage => 'administratorens side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'En offentlig PGN-kilde i sanntid for denne runden. Vi tilbyr også en $param for raskere og mer effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bygg inn denne overføringen på nettstedet ditt'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bygg inn $param på nettstedet ditt'; + } + + @override + String get broadcastRatingDiff => 'Ratingdifferanse'; + + @override + String get broadcastGamesThisTournament => 'Partier i denne turneringen'; + + @override + String get broadcastScore => 'Poengsum'; + + @override + String get broadcastAllTeams => 'Alle lag'; + + @override + String get broadcastTournamentFormat => 'Turneringsformat'; + + @override + String get broadcastTournamentLocation => 'Turneringssted'; + + @override + String get broadcastTopPlayers => 'Toppspillere'; + + @override + String get broadcastTimezone => 'Tidssone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; + + @override + String get broadcastOptionalDetails => 'Valgfrie detaljer'; + + @override + String get broadcastPastBroadcasts => 'Tidligere overføringer'; + + @override + String get broadcastAllBroadcastsByMonth => 'Vis alle overføringer etter måned'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count overføringer', + one: '$count overføring', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utfordringer: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get preferencesInGameOnly => 'Bare under partier'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sjakkur'; @@ -750,6 +1008,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bjellevarsel med lyd'; + @override + String get preferencesBlindfold => 'Blindsjakk'; + @override String get puzzlePuzzles => 'Sjakknøtter'; @@ -1390,10 +1651,10 @@ class AppLocalizationsNb extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Motstanderen kan bare utføre trekk som forverrer egen stilling.'; @override - String get puzzleThemeHealthyMix => 'Frisk blanding'; + String get puzzleThemeMix => 'Frisk blanding'; @override - String get puzzleThemeHealthyMixDescription => 'Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.'; + String get puzzleThemeMixDescription => 'Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.'; @override String get puzzleThemePlayerGames => 'Spillerpartier'; @@ -1767,9 +2028,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get byCPL => 'Etter CBT'; - @override - String get openStudy => 'Åpne studie'; - @override String get enable => 'Aktiver'; @@ -1797,9 +2055,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get removesTheDepthLimit => 'Fjerner dybdebegrensning, og holder maskinen din varm'; - @override - String get engineManager => 'Innstillinger for sjakkmotorer'; - @override String get blunder => 'Bukk'; @@ -2063,6 +2318,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get gamesPlayed => 'partier spilt'; + @override + String get ok => 'OK'; + @override String get cancel => 'Avbryt'; @@ -2437,9 +2695,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get unblock => 'Fjern blokkering'; - @override - String get followsYou => 'Følger deg'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 begynte å følge $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsNb extends AppLocalizations { String get other => 'Annet'; @override - String get reportDescriptionHelp => 'Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel.'; + String get reportCheatBoostHelp => 'Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel. Skriv en utdypende begrunnelse, ikke bare «vedkommende jukser».'; + + @override + String get reportUsernameHelp => 'Forklar hvorfor brukernavnet er støtende. Skriv en utdypende begrunnelse, ikke bare «det er støtende/upassende». Dette gjelder særlig hvis fornærmelsen er tilslørt, ikke er på engelsk, er et slanguttrykk eller er en historisk/kulturell referanse.'; + + @override + String get reportProcessedFasterInEnglish => 'Rapporten din blir behandlet raskere hvis den er skrevet på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Oppgi minst én lenke til et jukseparti.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get nothingToSeeHere => 'Ingenting her for nå.'; + @override + String get stats => 'Statistikk'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsNb extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-strømmere'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studier'; + + @override + String get studyStudiesIContributeTo => 'Studier jeg bidrar til'; + + @override + String get studyMyPublicStudies => 'Mine offentlige studier'; + + @override + String get studyMyPrivateStudies => 'Mine private studier'; + + @override + String get studyMyFavoriteStudies => 'Mine favorittstudier'; + + @override + String get studyWhatAreStudies => 'Hva er studier?'; + + @override + String get studyAllStudies => 'Alle studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier opprettet av $param'; + } + + @override + String get studyNoneYet => 'Ingen så langt.'; + + @override + String get studyHot => 'Hett'; + + @override + String get studyDateAddedNewest => 'Dato tilføyd (nyeste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføyd (eldste)'; + + @override + String get studyRecentlyUpdated => 'Nylig oppdatert'; + + @override + String get studyMostPopular => 'Mest populære'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Legg til kapittel'; + + @override + String get studyAddMembers => 'Legg til medlemmer'; + + @override + String get studyInviteToTheStudy => 'Inviter til studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter bare folk du kjenner som ønsker å delta i studien.'; + + @override + String get studySearchByUsername => 'Søk på brukernavn'; + + @override + String get studySpectator => 'Tilskuer'; + + @override + String get studyContributor => 'Bidragsyter'; + + @override + String get studyKick => 'Kast ut'; + + @override + String get studyLeaveTheStudy => 'Forlat studien'; + + @override + String get studyYouAreNowAContributor => 'Du er nå bidragsyter'; + + @override + String get studyYouAreNowASpectator => 'Du er nå tilskuer'; + + @override + String get studyPgnTags => 'PGN-merkelapper'; + + @override + String get studyLike => 'Lik'; + + @override + String get studyUnlike => 'Slutt å like'; + + @override + String get studyNewTag => 'Ny merkelapp'; + + @override + String get studyCommentThisPosition => 'Kommenter denne stillingen'; + + @override + String get studyCommentThisMove => 'Kommenter dette trekket'; + + @override + String get studyAnnotateWithGlyphs => 'Kommenter med symboler'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittelet er for kort for analyse.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Bare bidragsyterne til studien kan be om maskinanalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få full maskinanalyse av hovedvarianten fra serveren.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sørg for at kapittelet er fullført. Du kan bare be om analyse én gang.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle synkroniserte medlemmer ser den samme stillingen'; + + @override + String get studyShareChanges => 'Del endringer med tilskuere og lagre dem på serveren'; + + @override + String get studyPlaying => 'Pågår'; + + @override + String get studyShowEvalBar => 'Evalueringssøyler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Forrige'; + + @override + String get studyNext => 'Neste'; + + @override + String get studyLast => 'Siste'; + @override String get studyShareAndExport => 'Del og eksporter'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie-PGN'; + + @override + String get studyDownloadAllGames => 'Last ned alle partiene'; + + @override + String get studyChapterPgn => 'Kapittel-PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Last ned partiet'; + + @override + String get studyStudyUrl => 'Studie-URL'; + + @override + String get studyCurrentChapterUrl => 'Kapittel-URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan lime inn dette i forumet for å bygge det inn der'; + + @override + String get studyStartAtInitialPosition => 'Start ved innledende stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Bygg inn på nettstedet ditt eller bloggen din'; + + @override + String get studyReadMoreAboutEmbedding => 'Les mer om å bygge inn'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Bare offentlige studier kan bygges inn!'; + + @override + String get studyOpen => 'Åpne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 presentert av $param2'; + } + + @override + String get studyStudyNotFound => 'Fant ikke studien'; + + @override + String get studyEditChapter => 'Rediger kapittel'; + + @override + String get studyNewChapter => 'Nytt kapittel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer fra $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Fastspikrede kapittelkommenter'; + + @override + String get studySaveChapter => 'Lagre kapittelet'; + + @override + String get studyClearAnnotations => 'Fjern notater'; + + @override + String get studyClearVariations => 'Fjern varianter'; + + @override + String get studyDeleteChapter => 'Slett kapittel'; + + @override + String get studyDeleteThisChapter => 'Slette dette kapittelet? Du kan ikke angre!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Fjern alle kommentarer og figurer i dette kapittelet?'; + + @override + String get studyRightUnderTheBoard => 'Rett under brettet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul neste trekk'; + + @override + String get studyInteractiveLesson => 'Interaktiv leksjon'; + + @override + String studyChapterX(String param) { + return 'Kapittel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved innledende stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start fra innledende stilling'; + + @override + String get studyLoadAGameByUrl => 'Last inn partier fra URL-er'; + + @override + String get studyLoadAPositionFromFen => 'Last inn en stilling fra FEN'; + + @override + String get studyLoadAGameFromPgn => 'Last inn partier fra PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiene, én pr. linje'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Last inn partier fra $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opprett kapittel'; + + @override + String get studyCreateStudy => 'Opprett en studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlighet'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ikke listet'; + + @override + String get studyInviteOnly => 'Bare etter invitasjon'; + + @override + String get studyAllowCloning => 'Tillat kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Bare meg'; + + @override + String get studyContributors => 'Bidragsytere'; + + @override + String get studyMembers => 'Medlemmer'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Aktiver synkronisering'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behold alle i samme stilling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: la folk se fritt gjennom'; + + @override + String get studyPinnedStudyComment => 'Fastspikrede studiekommentarer'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Lagre'; + + @override + String get studyClearChat => 'Fjern samtalen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slette studiens samtalehistorikk? Du kan ikke angre!'; + + @override + String get studyDeleteStudy => 'Slett studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slette hele studien? Du kan ikke angre! Bekreft ved å skrive inn navnet på studien: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvilken studie vil du bruke?'; + + @override + String get studyGoodMove => 'Godt trekk'; + + @override + String get studyMistake => 'Feil'; + + @override + String get studyBrilliantMove => 'Strålende trekk'; + + @override + String get studyBlunder => 'Bukk'; + + @override + String get studyInterestingMove => 'Interessant trekk'; + + @override + String get studyDubiousMove => 'Tvilsomt trekk'; + + @override + String get studyOnlyMove => 'Eneste trekk'; + + @override + String get studyZugzwang => 'Trekktvang'; + + @override + String get studyEqualPosition => 'Lik stilling'; + + @override + String get studyUnclearPosition => 'Uavklart stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Hvit står litt bedre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart står litt bedre'; + + @override + String get studyWhiteIsBetter => 'Hvit står bedre'; + + @override + String get studyBlackIsBetter => 'Svart står bedre'; + + @override + String get studyWhiteIsWinning => 'Hvit står til vinst'; + + @override + String get studyBlackIsWinning => 'Svart står til vinst'; + + @override + String get studyNovelty => 'Nyvinning'; + + @override + String get studyDevelopment => 'Utvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Angrep'; + + @override + String get studyCounterplay => 'Motspill'; + + @override + String get studyTimeTrouble => 'Tidsnød'; + + @override + String get studyWithCompensation => 'Med kompensasjon'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Neste kapittel'; + + @override + String get studyPrevChapter => 'Forrige kapittel'; + + @override + String get studyStudyActions => 'Studiehandlinger'; + + @override + String get studyTopics => 'Emner'; + + @override + String get studyMyTopics => 'Mine emner'; + + @override + String get studyPopularTopics => 'Populære emner'; + + @override + String get studyManageTopics => 'Administrer emner'; + + @override + String get studyBack => 'Tilbake'; + + @override + String get studyPlayAgain => 'Spill igjen'; + + @override + String get studyWhatWouldYouPlay => 'Hva vil du spille i denne stillingen?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulerer! Du har fullført denne leksjonen.'; + + @override + String studyPerPage(String param) { + return '$param per side'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitler', + one: '$count kapittel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count medlemmer', + one: '$count medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Sett inn PGN-teksten din her, maksimum $count partier', + one: 'Sett inn PGN-teksten din her, maksimum $count parti', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'om litt'; + + @override + String get timeagoRightNow => 'for litt siden'; + + @override + String get timeagoCompleted => 'fullført'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutter', + one: 'om $count minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timer', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count døgn', + one: 'om $count døgn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count uker', + one: 'om $count uke', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count måneder', + one: 'om $count måned', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count minutter siden', + one: 'for $count minutt siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count timer siden', + one: 'for $count time siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count døgn siden', + one: 'for $count døgn siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count uker siden', + one: 'for $count uke siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count måneder siden', + one: 'for $count måned siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count år siden', + one: 'for $count år siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter igjen', + one: '$count minutt igjen', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer igjen', + one: '$count time igjen', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index 9781f805ca..c7eb877818 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsNl extends AppLocalizations { AppLocalizationsNl([String locale = 'nl']) : super(locale); @override - String get mobileHomeTab => 'Startscherm'; + String get mobileAllGames => 'Alle partijen'; @override - String get mobilePuzzlesTab => 'Puzzels'; + String get mobileAreYouSure => 'Weet je het zeker?'; @override - String get mobileToolsTab => 'Gereedschap'; + String get mobileCancelTakebackOffer => 'Terugnameaanbod annuleren'; @override - String get mobileWatchTab => 'Kijken'; + String get mobileClearButton => 'Wissen'; @override - String get mobileSettingsTab => 'Instellingen'; + String get mobileCorrespondenceClearSavedMove => 'Opgeslagen zet wissen'; @override - String get mobileMustBeLoggedIn => 'Je moet ingelogd zijn om deze pagina te bekijken.'; + String get mobileCustomGameJoinAGame => 'Een partij beginnen'; @override - String get mobileSystemColors => 'Systeemkleuren'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsHapticFeedback => 'Haptische feedback'; + String get mobileHideVariation => 'Verberg varianten'; @override - String get mobileSettingsImmersiveMode => 'Volledig scherm-modus'; + String get mobileHomeTab => 'Startscherm'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'U volgt geen gebruiker.'; + String get mobileMustBeLoggedIn => 'Je moet ingelogd zijn om deze pagina te bekijken.'; @override - String get mobileAllGames => 'Alle partijen'; + String get mobileNoSearchResults => 'Geen resultaten'; @override - String get mobileRecentSearches => 'Recente zoekopdrachten'; + String get mobileNotFollowingAnyUser => 'U volgt geen gebruiker.'; @override - String get mobileClearButton => 'Wissen'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Geen resultaten'; + String get mobilePrefMagnifyDraggedPiece => 'Versleept stuk vergroot weergeven'; @override - String get mobileAreYouSure => 'Weet je het zeker?'; + String get mobilePuzzleStormConfirmEndRun => 'Wil je deze reeks beëindigen?'; @override - String get mobilePuzzleStreakAbortWarning => 'Je verliest je huidige reeks en de score wordt opgeslagen.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niets te tonen, wijzig de filters'; @override String get mobilePuzzleStormNothingToShow => 'Niets om te tonen. Speel een aantal reeksen Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Deze puzzel delen'; + String get mobilePuzzleStormSubtitle => 'Los zoveel mogelijk puzzels op in 3 minuten.'; @override - String get mobileShareGameURL => 'Partij URL delen'; + String get mobilePuzzleStreakAbortWarning => 'Je verliest je huidige reeks en de score wordt opgeslagen.'; @override - String get mobileShareGamePGN => 'PGN delen'; + String get mobilePuzzleThemesSubtitle => 'Speel puzzels uit je favorieten openingen, of kies een thema.'; @override - String get mobileSharePositionAsFEN => 'Stelling delen als FEN'; + String get mobilePuzzlesTab => 'Puzzels'; @override - String get mobileShowVariations => 'Toon varianten'; + String get mobileRecentSearches => 'Recente zoekopdrachten'; @override - String get mobileHideVariation => 'Verberg varianten'; + String get mobileSettingsHapticFeedback => 'Haptische feedback'; @override - String get mobileShowComments => 'Opmerkingen weergeven'; + String get mobileSettingsImmersiveMode => 'Volledig scherm-modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wil je dit uitvoeren beëindigen?'; + String get mobileSettingsImmersiveModeSubtitle => 'Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Instellingen'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'PGN delen'; @override - String get mobileCancelDrawOffer => 'Remiseaanbod intrekken'; + String get mobileShareGameURL => 'Partij URL delen'; @override - String get mobileWaitingForOpponentToJoin => 'Wachten op een tegenstander...'; + String get mobileSharePositionAsFEN => 'Stelling delen als FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Deze puzzel delen'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Opmerkingen weergeven'; @override - String get mobileCustomGameJoinAGame => 'Een partij beginnen'; + String get mobileShowResult => 'Toon resultaat'; @override - String get mobileCorrespondenceClearSavedMove => 'Opgeslagen zet wissen'; + String get mobileShowVariations => 'Toon varianten'; @override String get mobileSomethingWentWrong => 'Er is iets fout gegaan.'; @override - String get mobileShowResult => 'Toon resultaat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Speel puzzels uit je favorieten openingen, of kies een thema.'; + String get mobileSystemColors => 'Systeemkleuren'; @override - String get mobilePuzzleStormSubtitle => 'Los zoveel mogelijk puzzels op in 3 minuten.'; + String get mobileTheme => 'Thema'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Gereedschap'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Wachten op een tegenstander...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Kijken'; @override String get activityActivity => 'Activiteit'; @@ -174,8 +171,8 @@ class AppLocalizationsNl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Beoefende $count posities van $param2', - one: 'Beoefende $count positie van $param2', + other: 'Oefende $count posities van $param2', + one: 'Oefende $count positie van $param2', ); return '$_temp0'; } @@ -246,6 +243,17 @@ class AppLocalizationsNl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Voltooide $count $param2 correspondentiepartijen', + one: 'Voltooide $count $param2 correspondentiepartijen', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsNl extends AppLocalizations { @override String get broadcastBroadcasts => 'Uitzendingen'; + @override + String get broadcastMyBroadcasts => 'Mijn uitzendingen'; + @override String get broadcastLiveBroadcasts => 'Live toernooi uitzendingen'; + @override + String get broadcastBroadcastCalendar => 'Uitzendkalender'; + + @override + String get broadcastNewBroadcast => 'Nieuwe live uitzending'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Over uitzending'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hoe Lichess Uitzendingen te gebruiken.'; + + @override + String get broadcastTheNewRoundHelp => 'De nieuwe ronde zal dezelfde leden en bijdragers hebben als de vorige.'; + + @override + String get broadcastAddRound => 'Ronde toevoegen'; + + @override + String get broadcastOngoing => 'Lopend'; + + @override + String get broadcastUpcoming => 'Aankomend'; + + @override + String get broadcastCompleted => 'Voltooid'; + + @override + String get broadcastCompletedHelp => 'Lichess detecteert voltooiing van de ronde op basis van de bronpartijen. Gebruik deze schakelaar als er geen bron is.'; + + @override + String get broadcastRoundName => 'Naam ronde'; + + @override + String get broadcastRoundNumber => 'Ronde'; + + @override + String get broadcastTournamentName => 'Naam toernooi'; + + @override + String get broadcastTournamentDescription => 'Korte toernooibeschrijving'; + + @override + String get broadcastFullDescription => 'Volledige beschrijving evenement'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionele lange beschrijving van de uitzending. $param1 is beschikbaar. Totale lengte moet minder zijn dan $param2 tekens.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL van PGN-bron'; + + @override + String get broadcastSourceUrlHelp => 'Link die Lichess gebruikt om PGN updates te krijgen. Deze moet openbaar toegankelijk zijn via internet.'; + + @override + String get broadcastSourceGameIds => 'Tot 64 Lichess partij-ID\'\'s, gescheiden door spaties.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum in de lokale tijdzone van het tornooi: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optioneel, als je weet wanneer het evenement start'; + + @override + String get broadcastCurrentGameUrl => 'Huidige partij-link'; + + @override + String get broadcastDownloadAllRounds => 'Alle rondes downloaden'; + + @override + String get broadcastResetRound => 'Deze ronde opnieuw instellen'; + + @override + String get broadcastDeleteRound => 'Deze ronde verwijderen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Deze ronde en bijbehorende partijen definitief verwijderen.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Alle partijen van deze ronde verwijderen. De bron zal actief moeten zijn om ze opnieuw te maken.'; + + @override + String get broadcastEditRoundStudy => 'Studieronde bewerken'; + + @override + String get broadcastDeleteTournament => 'Verwijder dit toernooi'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Verwijder definitief het hele toernooi, inclusief alle rondes en partijen.'; + + @override + String get broadcastShowScores => 'Toon scores van spelers op basis van partij-uitslagen'; + + @override + String get broadcastReplacePlayerTags => 'Optioneel: vervang spelersnamen, beoordelingen en titels'; + + @override + String get broadcastFideFederations => 'FIDE-federaties'; + + @override + String get broadcastTop10Rating => 'Top 10-rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spelers'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-speler niet gevonden'; + + @override + String get broadcastFideProfile => 'FIDE-profiel'; + + @override + String get broadcastFederation => 'Federatie'; + + @override + String get broadcastAgeThisYear => 'Leeftijd dit jaar'; + + @override + String get broadcastUnrated => 'Zonder rating'; + + @override + String get broadcastRecentTournaments => 'Recente toernooien'; + + @override + String get broadcastOpenLichess => 'Openen in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Borden'; + + @override + String get broadcastOverview => 'Overzicht'; + + @override + String get broadcastSubscribeTitle => 'Krijg een melding wanneer elke ronde start. Je kunt bel- of pushmeldingen voor uitzendingen in je accountvoorkeuren in-/uitschakelen.'; + + @override + String get broadcastUploadImage => 'Toernooifoto uploaden'; + + @override + String get broadcastNoBoardsYet => 'Nog geen borden. Deze zullen verschijnen van zodra er partijen worden geüpload.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Borden kunnen geladen worden met een bron of via de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Start na $param'; + } + + @override + String get broadcastStartVerySoon => 'De uitzending begint binnenkort.'; + + @override + String get broadcastNotYetStarted => 'De uitzending is nog niet begonnen.'; + + @override + String get broadcastOfficialWebsite => 'Officiële website'; + + @override + String get broadcastStandings => 'Klassement'; + + @override + String get broadcastOfficialStandings => 'Officiële standen'; + + @override + String broadcastIframeHelp(String param) { + return 'Meer opties voor de $param'; + } + + @override + String get broadcastWebmastersPage => 'pagina van de webmaster'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Een publieke real-time PGN-bron voor deze ronde. We bieden ook een $param aan voor een snellere en efficiëntere synchronisatie.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Deze uitzending insluiten in je website'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param insluiten in je website'; + } + + @override + String get broadcastRatingDiff => 'Ratingverschil'; + + @override + String get broadcastGamesThisTournament => 'Partijen in dit toernooi'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'Alle teams'; + + @override + String get broadcastTournamentFormat => 'Toernooivorm'; + + @override + String get broadcastTournamentLocation => 'Toernooilocatie'; + + @override + String get broadcastTopPlayers => 'Topspelers'; + + @override + String get broadcastTimezone => 'Tijdzone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-rating categorie'; + + @override + String get broadcastOptionalDetails => 'Optionele info'; + + @override + String get broadcastPastBroadcasts => 'Afgelopen uitzendingen'; + + @override + String get broadcastAllBroadcastsByMonth => 'Alle uitzendingen per maand weergeven'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uitzendingen', + one: '$count uitzending', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Uitdagingen: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get preferencesInGameOnly => 'Alleen tijdens partij'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schaakklok'; @@ -750,6 +1008,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Meldingsgeluid'; + @override + String get preferencesBlindfold => 'Geblinddoekt'; + @override String get puzzlePuzzles => 'Puzzels'; @@ -1390,10 +1651,10 @@ class AppLocalizationsNl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De tegenstander is beperkt in de zetten die hij kan doen, en elke zet verslechtert zijn stelling.'; @override - String get puzzleThemeHealthyMix => 'Gezonde mix'; + String get puzzleThemeMix => 'Gezonde mix'; @override - String get puzzleThemeHealthyMixDescription => 'Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.'; + String get puzzleThemeMixDescription => 'Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.'; @override String get puzzleThemePlayerGames => 'Eigen partijen'; @@ -1767,9 +2028,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get byCPL => 'Door CPL'; - @override - String get openStudy => 'Open Study'; - @override String get enable => 'Aanzetten'; @@ -1797,9 +2055,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get removesTheDepthLimit => 'Verwijdert de dieptelimiet, en houdt je computer warm'; - @override - String get engineManager => 'Engine-beheer'; - @override String get blunder => 'Blunder'; @@ -2063,6 +2318,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get gamesPlayed => 'Gespeelde partijen'; + @override + String get ok => 'Oké'; + @override String get cancel => 'Annuleren'; @@ -2121,7 +2379,7 @@ class AppLocalizationsNl extends AppLocalizations { String get standard => 'Standaard'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Aangepaste positie'; @override String get unlimited => 'Onbeperkt'; @@ -2437,9 +2695,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get unblock => 'Deblokkeren'; - @override - String get followsYou => 'Volgt u'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 volgt nu $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsNl extends AppLocalizations { String get other => 'Anders'; @override - String get reportDescriptionHelp => 'Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen \'hij speelt vals\', maar vertel ons hoe u bent gekomen op deze conclusie. Uw rapportage zal sneller worden verwerkt als het in het Engels is geschreven.'; + String get reportCheatBoostHelp => 'Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen \'hij speelt vals\', maar leg ook uit hoe je tot deze conclusie komt.'; + + @override + String get reportUsernameHelp => 'Leg uit wat er aan deze gebruikersnaam beledigend is. Zeg niet gewoon \"het is aanstootgevend/ongepast\", maar vertel ons hoe je tot deze conclusie komt, vooral als de belediging verhuld wordt, niet in het Engels is, in dialect is, of een historische of culturele verwijzing is.'; + + @override + String get reportProcessedFasterInEnglish => 'Je melding wordt sneller verwerkt als deze in het Engels is geschreven.'; @override String get error_provideOneCheatedGameLink => 'Geef ten minste één link naar een partij waarin vals gespeeld is.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get nothingToSeeHere => 'Hier is momenteel niets te zien.'; + @override + String get stats => 'Statistieken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsNl extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamers'; + @override + String get studyPrivate => 'Privé'; + + @override + String get studyMyStudies => 'Mijn Studies'; + + @override + String get studyStudiesIContributeTo => 'Studies waaraan ik bijdraag'; + + @override + String get studyMyPublicStudies => 'Mijn openbare studies'; + + @override + String get studyMyPrivateStudies => 'Mijn privé studies'; + + @override + String get studyMyFavoriteStudies => 'Mijn favoriete studies'; + + @override + String get studyWhatAreStudies => 'Wat zijn studies?'; + + @override + String get studyAllStudies => 'Alle studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies gemaakt door $param'; + } + + @override + String get studyNoneYet => 'Nog geen...'; + + @override + String get studyHot => 'Populair'; + + @override + String get studyDateAddedNewest => 'Datum toegevoegd (nieuwste)'; + + @override + String get studyDateAddedOldest => 'Datum toegevoegd (oudste)'; + + @override + String get studyRecentlyUpdated => 'Recent bijgewerkt'; + + @override + String get studyMostPopular => 'Meest populair'; + + @override + String get studyAlphabetical => 'Alfabetisch'; + + @override + String get studyAddNewChapter => 'Nieuw hoofdstuk toevoegen'; + + @override + String get studyAddMembers => 'Deelnemers toevoegen'; + + @override + String get studyInviteToTheStudy => 'Uitnodigen voor de studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Nodig alleen deelnemers uit die jou kennen en actief mee willen doen aan deze studie.'; + + @override + String get studySearchByUsername => 'Zoeken op gebruikersnaam'; + + @override + String get studySpectator => 'Kijker'; + + @override + String get studyContributor => 'Bijdrager'; + + @override + String get studyKick => 'Verwijder'; + + @override + String get studyLeaveTheStudy => 'Verlaat de studie'; + + @override + String get studyYouAreNowAContributor => 'Je bent nu een bijdrager'; + + @override + String get studyYouAreNowASpectator => 'Je bent nu een toeschouwer'; + + @override + String get studyPgnTags => 'PGN labels'; + + @override + String get studyLike => 'Vind ik leuk'; + + @override + String get studyUnlike => 'Vind ik niet meer leuk'; + + @override + String get studyNewTag => 'Nieuw label'; + + @override + String get studyCommentThisPosition => 'Reageer op deze positie'; + + @override + String get studyCommentThisMove => 'Reageer op deze zet'; + + @override + String get studyAnnotateWithGlyphs => 'Maak aantekeningen met symbolen'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Dit hoofdstuk is te kort om geanalyseerd te worden.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Alleen de bijdragers kunnen een computer analyse aanvragen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Krijg een volledige computer analyse van de hoofdlijn.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Zorg ervoor dat het hoofdstuk voltooid is. Je kunt slechts één keer een analyse aanvragen.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC leden blijven op dezelfde positie'; + + @override + String get studyShareChanges => 'Deel veranderingen met toeschouwers en sla deze op op de server'; + + @override + String get studyPlaying => 'Spelend'; + + @override + String get studyShowEvalBar => 'Evaluatiebalk'; + + @override + String get studyFirst => 'Eerste'; + + @override + String get studyPrevious => 'Vorige'; + + @override + String get studyNext => 'Volgende'; + + @override + String get studyLast => 'Laatste'; + @override String get studyShareAndExport => 'Deel & exporteer'; + @override + String get studyCloneStudy => 'Kopiëren'; + + @override + String get studyStudyPgn => 'PGN bestuderen'; + + @override + String get studyDownloadAllGames => 'Download alle partijen'; + + @override + String get studyChapterPgn => 'Hoofdstuk PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopiëren'; + + @override + String get studyDownloadGame => 'Partij downloaden'; + + @override + String get studyStudyUrl => 'Studie URL'; + + @override + String get studyCurrentChapterUrl => 'Huidige hoofdstuk URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Je kunt deze link plakken wanneer je een bericht schrijft op het forum om de partij interactief weer te geven'; + + @override + String get studyStartAtInitialPosition => 'Begin bij de startpositie'; + + @override + String studyStartAtX(String param) { + return 'Beginnen bij $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Insluiten in blog of website'; + + @override + String get studyReadMoreAboutEmbedding => 'Lees meer over insluiten'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Alleen openbare studies kunnen worden ingevoegd!'; + + @override + String get studyOpen => 'Open'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 aangeboden door $param2'; + } + + @override + String get studyStudyNotFound => 'Studie niet gevonden'; + + @override + String get studyEditChapter => 'Hoofdstuk bewerken'; + + @override + String get studyNewChapter => 'Nieuw hoofdstuk'; + + @override + String studyImportFromChapterX(String param) { + return 'Importeren van $param'; + } + + @override + String get studyOrientation => 'Oriëntatie'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Vastgezet commentaar van het hoofdstuk'; + + @override + String get studySaveChapter => 'Hoofdstuk opslaan'; + + @override + String get studyClearAnnotations => 'Wis aantekeningen'; + + @override + String get studyClearVariations => 'Verwijder variaties'; + + @override + String get studyDeleteChapter => 'Verwijder hoofdstuk'; + + @override + String get studyDeleteThisChapter => 'Wil je dit hoofdstuk verwijderen? Je kan dit niet ongedaan maken!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Verwijder alle aantekeningen, tekens en getekende figuren in dit hoofdstuk?'; + + @override + String get studyRightUnderTheBoard => 'Recht onder het bord'; + + @override + String get studyNoPinnedComment => 'Geen'; + + @override + String get studyNormalAnalysis => 'Normale analyse'; + + @override + String get studyHideNextMoves => 'Verberg volgende zetten'; + + @override + String get studyInteractiveLesson => 'Interactieve les'; + + @override + String studyChapterX(String param) { + return 'Hoofdstuk $param'; + } + + @override + String get studyEmpty => 'Leeg'; + + @override + String get studyStartFromInitialPosition => 'Start bij de initiële positie'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start bij een aangepaste positie'; + + @override + String get studyLoadAGameByUrl => 'Laad partijen via een URL'; + + @override + String get studyLoadAPositionFromFen => 'Laad een spel via een FEN'; + + @override + String get studyLoadAGameFromPgn => 'Laad partijen via een PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL van de partijen, één per regel'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Laad partijen van $param1 of $param2'; + } + + @override + String get studyCreateChapter => 'Creëer hoofdstuk'; + + @override + String get studyCreateStudy => 'Maak studie'; + + @override + String get studyEditStudy => 'Bewerk studie'; + + @override + String get studyVisibility => 'Zichtbaarheid'; + + @override + String get studyPublic => 'Openbaar'; + + @override + String get studyUnlisted => 'Niet openbaar'; + + @override + String get studyInviteOnly => 'Alleen op uitnodiging'; + + @override + String get studyAllowCloning => 'Klonen toestaan'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Alleen ik'; + + @override + String get studyContributors => 'Bijdragers'; + + @override + String get studyMembers => 'Deelnemers'; + + @override + String get studyEveryone => 'Iedereen'; + + @override + String get studyEnableSync => 'Synchronisatie inschakelen'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: houd iedereen op dezelfde stelling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: laat mensen vrij bladeren'; + + @override + String get studyPinnedStudyComment => 'Vastgezette studie reactie'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Opslaan'; + + @override + String get studyClearChat => 'Maak de chat leeg'; + + @override + String get studyDeleteTheStudyChatHistory => 'Verwijder de studiechat geschiedenis? Er is geen weg terug!'; + + @override + String get studyDeleteStudy => 'Studie verwijderen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'De hele studie verwijderen? Er is geen weg terug! Type de naam van de studie om te bevestigen dat je de studie wilt verwijderen: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Waar wil je dat bestuderen?'; + + @override + String get studyGoodMove => 'Goede zet'; + + @override + String get studyMistake => 'Fout'; + + @override + String get studyBrilliantMove => 'Briljante zet'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Interessante zet'; + + @override + String get studyDubiousMove => 'Dubieuze zet'; + + @override + String get studyOnlyMove => 'Enig mogelijke zet'; + + @override + String get studyZugzwang => 'Zetdwang'; + + @override + String get studyEqualPosition => 'Stelling in evenwicht'; + + @override + String get studyUnclearPosition => 'Onduidelijke stelling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wit staat iets beter'; + + @override + String get studyBlackIsSlightlyBetter => 'Zwart staat iets beter'; + + @override + String get studyWhiteIsBetter => 'Wit staat beter'; + + @override + String get studyBlackIsBetter => 'Zwart staat beter'; + + @override + String get studyWhiteIsWinning => 'Wit staat gewonnen'; + + @override + String get studyBlackIsWinning => 'Zwart staat gewonnen'; + + @override + String get studyNovelty => 'Noviteit'; + + @override + String get studyDevelopment => 'Ontwikkeling'; + + @override + String get studyInitiative => 'Initiatief'; + + @override + String get studyAttack => 'Aanval'; + + @override + String get studyCounterplay => 'Tegenspel'; + + @override + String get studyTimeTrouble => 'Tijdnood'; + + @override + String get studyWithCompensation => 'Met compensatie'; + + @override + String get studyWithTheIdea => 'Met het idee'; + + @override + String get studyNextChapter => 'Volgende hoofdstuk'; + + @override + String get studyPrevChapter => 'Vorige hoofdstuk'; + + @override + String get studyStudyActions => 'Studie sneltoetsen'; + + @override + String get studyTopics => 'Onderwerpen'; + + @override + String get studyMyTopics => 'Mijn onderwerpen'; + + @override + String get studyPopularTopics => 'Populaire onderwerpen'; + + @override + String get studyManageTopics => 'Onderwerpen beheren'; + + @override + String get studyBack => 'Terug'; + + @override + String get studyPlayAgain => 'Opnieuw spelen'; + + @override + String get studyWhatWouldYouPlay => 'Wat zou je in deze stelling spelen?'; + + @override + String get studyYouCompletedThisLesson => 'Gefeliciteerd! Je hebt deze les voltooid.'; + + @override + String studyPerPage(String param) { + return '$param per pagina'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hoofdstukken', + one: '$count hoofdstuk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partijen', + one: '$count Partij', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Deelnemers', + one: '$count Deelnemer', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Plak je PGN tekst hier, tot $count spellen mogelijk', + one: 'Plak je PGN tekst hier, tot $count spel mogelijk', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'zojuist'; + + @override + String get timeagoRightNow => 'op dit moment'; + + @override + String get timeagoCompleted => 'voltooid'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count seconden', + one: 'over $count seconde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count minuten', + one: 'over $count minuut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count uur', + one: 'over $count uur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count dagen', + one: 'over $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count weken', + one: 'over $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count maanden', + one: 'over $count maand', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count jaren', + one: 'over $count jaar', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuten geleden', + one: '$count minuut geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uur geleden', + one: '$count uur geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagen geleden', + one: '$count dag geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weken geleden', + one: '$count week geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count maanden geleden', + one: '$count maand geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jaar geleden', + one: '$count jaar geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuten resterend', + one: '$count minuut resterend', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uur resterend', + one: '$count uur resterend', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index ac94f6395c..2ebae3b71a 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsNn extends AppLocalizations { AppLocalizationsNn([String locale = 'nn']) : super(locale); @override - String get mobileHomeTab => 'Startside'; + String get mobileAllGames => 'Alle spel'; @override - String get mobilePuzzlesTab => 'Oppgåver'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Verktøy'; + String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; @override - String get mobileWatchTab => 'Sjå'; + String get mobileClearButton => 'Tøm'; @override - String get mobileSettingsTab => 'Innstillingar'; + String get mobileCorrespondenceClearSavedMove => 'Fjern lagra trekk'; @override - String get mobileMustBeLoggedIn => 'Du må vera innlogga for å sjå denne sida.'; + String get mobileCustomGameJoinAGame => 'Bli med på eit parti'; @override - String get mobileSystemColors => 'Systemfargar'; + String get mobileFeedbackButton => 'Tilbakemelding'; @override - String get mobileFeedbackButton => 'Tilbakemelding'; + String mobileGreeting(String param) { + return 'Hei $param'; + } @override - String get mobileOkButton => 'Ok'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsHapticFeedback => 'Haptisk tilbakemelding'; + String get mobileHideVariation => 'Skjul variant'; @override - String get mobileSettingsImmersiveMode => 'Immersiv modus'; + String get mobileHomeTab => 'Startside'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.'; + String get mobileLiveStreamers => 'Direkte strøymarar'; @override - String get mobileNotFollowingAnyUser => 'Du følgjer ingen brukarar.'; + String get mobileMustBeLoggedIn => 'Du må vera innlogga for å sjå denne sida.'; @override - String get mobileAllGames => 'Alle spel'; + String get mobileNoSearchResults => 'Ingen resultat'; @override - String get mobileRecentSearches => 'Nylege søk'; + String get mobileNotFollowingAnyUser => 'Du følgjer ingen brukarar.'; @override - String get mobileClearButton => 'Tøm'; + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsNn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen resultat'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikke som vert trekt'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte dette løpet?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du vil mista din noverande vinstrekke og poengsummen din vert lagra.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ikkje noko å syna, ver venleg å endre filtera'; @override String get mobilePuzzleStormNothingToShow => 'Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne oppgåva'; + String get mobilePuzzleStormSubtitle => 'Løys så mange oppgåver som du maktar på tre minutt.'; @override - String get mobileShareGameURL => 'Del URLen til partiet'; + String get mobilePuzzleStreakAbortWarning => 'Du vil mista din noverande vinstrekke og poengsummen din vert lagra.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spel oppgåver frå favorittopningane dine, eller velg eit tema.'; @override - String get mobileSharePositionAsFEN => 'Del stilling som FEN'; + String get mobilePuzzlesTab => 'Oppgåver'; @override - String get mobileShowVariations => 'Vis variantpilar'; + String get mobileRecentSearches => 'Nylege søk'; @override - String get mobileHideVariation => 'Skjul variant'; + String get mobileSettingsHapticFeedback => 'Haptisk tilbakemelding'; @override - String get mobileShowComments => 'Vis kommentarar'; + String get mobileSettingsImmersiveMode => 'Immersiv modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte dette løpet?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ikkje noko å syna, ver venleg å endre filtera'; + String get mobileSettingsTab => 'Innstillingar'; @override - String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileCancelDrawOffer => 'Avbryt remistilbud'; + String get mobileShareGameURL => 'Del URLen til partiet'; @override - String get mobileWaitingForOpponentToJoin => 'Ventar på motspelar...'; + String get mobileSharePositionAsFEN => 'Del stilling som FEN'; @override - String get mobileBlindfoldMode => 'Blindsjakk'; + String get mobileSharePuzzle => 'Del denne oppgåva'; @override - String get mobileLiveStreamers => 'Direkte strøymarar'; + String get mobileShowComments => 'Vis kommentarar'; @override - String get mobileCustomGameJoinAGame => 'Bli med på eit parti'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Fjern lagra trekk'; + String get mobileShowVariations => 'Vis variantpilar'; @override String get mobileSomethingWentWrong => 'Det oppsto ein feil.'; @override - String get mobileShowResult => 'Vis resultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Spel oppgåver frå favorittopningane dine, eller velg eit tema.'; + String get mobileSystemColors => 'Systemfargar'; @override - String get mobilePuzzleStormSubtitle => 'Løys så mange oppgåver som du maktar på tre minutt.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Hei $param'; - } + String get mobileToolsTab => 'Verktøy'; @override - String get mobileGreetingWithoutName => 'Hei'; + String get mobileWaitingForOpponentToJoin => 'Ventar på motspelar...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Sjå'; @override String get activityActivity => 'Aktivitet'; @@ -246,13 +243,24 @@ class AppLocalizationsNn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Har spelt $count $param2-fjernsjakkparti', + one: 'Har spelt $count $param2-fjernsjakkparti', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Fylgjer $count spelarar', - one: 'Fylgjer $count spelar', + other: 'Følgjer $count spelarar', + one: 'Følgjer $count spelar', ); return '$_temp0'; } @@ -262,8 +270,8 @@ class AppLocalizationsNn extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Har $count nye fylgjarar', - one: 'Har $count nye fylgjarar', + other: 'Har $count nye følgjarar', + one: 'Har $count ny følgjar', ); return '$_temp0'; } @@ -348,9 +356,256 @@ class AppLocalizationsNn extends AppLocalizations { @override String get broadcastBroadcasts => 'Overføringar'; + @override + String get broadcastMyBroadcasts => 'Mine sendingar'; + @override String get broadcastLiveBroadcasts => 'Direktesende turneringar'; + @override + String get broadcastBroadcastCalendar => 'Kaldender for sendingar'; + + @override + String get broadcastNewBroadcast => 'Ny direktesending'; + + @override + String get broadcastSubscribedBroadcasts => 'Sendingar du abonnerar på'; + + @override + String get broadcastAboutBroadcasts => 'Om sending'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Korleis bruke Lichess-sendingar.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runden vil ha same medlemar og bidragsytarar som den førre.'; + + @override + String get broadcastAddRound => 'Legg til ein runde'; + + @override + String get broadcastOngoing => 'Pågåande'; + + @override + String get broadcastUpcoming => 'Kommande'; + + @override + String get broadcastCompleted => 'Fullførde'; + + @override + String get broadcastCompletedHelp => 'Lichess detekterer ferdigspela rundar basert på kjeldeparita. Bruk denne innstillinga om det ikkje finst ei kjelde.'; + + @override + String get broadcastRoundName => 'Rundenamn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnamn'; + + @override + String get broadcastTournamentDescription => 'Kortfatta skildring av turneringa'; + + @override + String get broadcastFullDescription => 'Full omtale av arrangementet'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfri lang omtale av turneringa. $param1 er tilgjengeleg. Omtalen må vera kortare enn $param2 teikn.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN kjelde-URL'; + + @override + String get broadcastSourceUrlHelp => 'Lenke som Lichess vil hente PGN-oppdateringar frå. Den må vera offentleg tilgjengeleg på internett.'; + + @override + String get broadcastSourceGameIds => 'Opp til 64 Lichess spel-ID\'ar, skilde med mellomrom.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringas lokale tidssone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfritt, om du veit når arrangementet startar'; + + @override + String get broadcastCurrentGameUrl => 'URL til pågåande parti'; + + @override + String get broadcastDownloadAllRounds => 'Last ned alle rundene'; + + @override + String get broadcastResetRound => 'Tilbakestill denne runden'; + + @override + String get broadcastDeleteRound => 'Slett denne runden'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slett runden og tilhøyrande parti ugjenkalleleg.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Fjern alle parti frå denne runden. Kjelda må vera aktiv om dei skal kunne rettast opp att.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slett denne turneringa'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slett heile turneringa med alle rundene og alle partia.'; + + @override + String get broadcastShowScores => 'Vis poengsummane til spelarar basert på spelresultatet deira'; + + @override + String get broadcastReplacePlayerTags => 'Valfritt: bytt ut spelarnamn, rangeringar og titlar'; + + @override + String get broadcastFideFederations => 'FIDE-forbund'; + + @override + String get broadcastTop10Rating => 'Topp 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spelarar'; + + @override + String get broadcastFidePlayerNotFound => 'Fann ikkje FIDE-spelar'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Forbund'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Urangert'; + + @override + String get broadcastRecentTournaments => 'Nylegaste turneringar'; + + @override + String get broadcastOpenLichess => 'Opne i Lichess'; + + @override + String get broadcastTeams => 'Lag'; + + @override + String get broadcastBoards => 'Brett'; + + @override + String get broadcastOverview => 'Oversikt'; + + @override + String get broadcastSubscribeTitle => 'Abonner for å få melding når kvarr runde startar. I konto-innstillingane dine kan du velje kva form varslane skal sendas som.'; + + @override + String get broadcastUploadImage => 'Last opp turneringsbilete'; + + @override + String get broadcastNoBoardsYet => 'Førebels er det ikkje brett å syne. Desse vert først vist når spel er lasta opp.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brett kan lastas med ei kjelde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Startar etter $param'; + } + + @override + String get broadcastStartVerySoon => 'Sending vil starte om ikkje lenge.'; + + @override + String get broadcastNotYetStarted => 'Sendinga har førebels ikkje starta.'; + + @override + String get broadcastOfficialWebsite => 'Offisiell nettside'; + + @override + String get broadcastStandings => 'Resultat'; + + @override + String get broadcastOfficialStandings => 'Offisiell tabell'; + + @override + String broadcastIframeHelp(String param) { + return 'Fleire alternativ på $param'; + } + + @override + String get broadcastWebmastersPage => 'administratoren si side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Ei offentleg PGN-kjelde i sanntid for denne runden. Vi tilbyr og ei $param for raskare og meir effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bygg inn denne sendinga på nettstaden din'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bygg inn $param på nettstaden din'; + } + + @override + String get broadcastRatingDiff => 'Rangeringsdiff'; + + @override + String get broadcastGamesThisTournament => 'Spel i denne turneringa'; + + @override + String get broadcastScore => 'Poengskår'; + + @override + String get broadcastAllTeams => 'Alle lag'; + + @override + String get broadcastTournamentFormat => 'Turneringsformat'; + + @override + String get broadcastTournamentLocation => 'Turneringsstad'; + + @override + String get broadcastTopPlayers => 'Toppspelarar'; + + @override + String get broadcastTimezone => 'Tidssone'; + + @override + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; + + @override + String get broadcastOptionalDetails => 'Valfrie detaljar'; + + @override + String get broadcastPastBroadcasts => 'Tidlegare overføringar'; + + @override + String get broadcastAllBroadcastsByMonth => 'Vis alle overføringar etter månad'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sendingar', + one: '$count sending', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utfordringar: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get preferencesInGameOnly => 'Berre under eit parti'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sjakkur'; @@ -750,6 +1008,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Varsellyd'; + @override + String get preferencesBlindfold => 'Blindsjakk'; + @override String get puzzlePuzzles => 'Taktikkoppgåver'; @@ -1390,10 +1651,10 @@ class AppLocalizationsNn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ei stilling der alle moglege trekk skadar stillinga.'; @override - String get puzzleThemeHealthyMix => 'Blanda drops'; + String get puzzleThemeMix => 'Blanda drops'; @override - String get puzzleThemeHealthyMixDescription => 'Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.'; + String get puzzleThemeMixDescription => 'Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.'; @override String get puzzleThemePlayerGames => 'Spelar parti'; @@ -1767,9 +2028,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Opne studie'; - @override String get enable => 'Aktiver'; @@ -1797,9 +2055,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get removesTheDepthLimit => 'Tar bort avgrensing i søke-djupna, og varmar opp maskina'; - @override - String get engineManager => 'Innstillingar for sjakkprogram'; - @override String get blunder => 'Bukk'; @@ -2063,6 +2318,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get gamesPlayed => 'Spelte parti'; + @override + String get ok => 'Ok'; + @override String get cancel => 'Avbryt'; @@ -2410,13 +2668,13 @@ class AppLocalizationsNn extends AppLocalizations { String get favoriteOpponents => 'Favorittmotstandarar'; @override - String get follow => 'Fylgj'; + String get follow => 'Følg'; @override - String get following => 'Fylgjer'; + String get following => 'Følgjer'; @override - String get unfollow => 'Slutt å fylgja'; + String get unfollow => 'Slutt å følgja'; @override String followX(String param) { @@ -2437,12 +2695,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get unblock => 'Fjern blokkering'; - @override - String get followsYou => 'Fylgjer deg'; - @override String xStartedFollowingY(String param1, String param2) { - return '$param1 byrja å fylgja $param2'; + return '$param1 byrja å følgja $param2'; } @override @@ -2772,7 +3027,13 @@ class AppLocalizationsNn extends AppLocalizations { String get other => 'Anna'; @override - String get reportDescriptionHelp => 'Lim inn link til partiet/partia og forklar kva som er gale med åtferda til denne brukaren.'; + String get reportCheatBoostHelp => 'Legg ved lenke til partiet/partia og forklar kva som er gale med åtferda til denne brukaren. Å berre påstå at brukaren juksar er ikkje nok, men gje ei nærare forklaring på korleis du kom til denne konklusjonen.'; + + @override + String get reportUsernameHelp => 'Forklår kva som gjer brukarnamnet er støytande. Det held ikkje med å påstå at \"namnet er støytande/upassande\", men fortell oss korleis du kom til denne konklusjonen, spesielt om tydinga er uklår, ikkje er på engelsk, er eit slanguttrykk, eller har ein historisk/kulturell referanse.'; + + @override + String get reportProcessedFasterInEnglish => 'Rapporten din blir raskare behandla om du skriv på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Oppgje minst ei lenke til eit jukseparti.'; @@ -2845,7 +3106,7 @@ class AppLocalizationsNn extends AppLocalizations { String get privacyPolicy => 'Personvernpolitikk'; @override - String get letOtherPlayersFollowYou => 'Lat andre spelarar fylgja deg'; + String get letOtherPlayersFollowYou => 'Lat andre spelarar følgja deg'; @override String get letOtherPlayersChallengeYou => 'Lat andre spelarar utfordra deg'; @@ -4077,6 +4338,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get nothingToSeeHere => 'Ikkje noko å sjå nett no.'; + @override + String get stats => 'Statistikk'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4412,8 +4676,8 @@ class AppLocalizationsNn extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count fylgjarar', - one: '$count fylgjar', + other: '$count følgjarar', + one: '$count følgjar', ); return '$_temp0'; } @@ -4424,7 +4688,7 @@ class AppLocalizationsNn extends AppLocalizations { count, locale: localeName, other: '$count fylgjer', - one: '$count fylgjer', + one: '$count følgjer', ); return '$_temp0'; } @@ -4723,9 +4987,693 @@ class AppLocalizationsNn extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-strøymarar'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studiar'; + + @override + String get studyStudiesIContributeTo => 'Studiar eg bidreg til'; + + @override + String get studyMyPublicStudies => 'Mine offentlege studiar'; + + @override + String get studyMyPrivateStudies => 'Mine private studiar'; + + @override + String get studyMyFavoriteStudies => 'Mine favorittstudiar'; + + @override + String get studyWhatAreStudies => 'Kva er studiar?'; + + @override + String get studyAllStudies => 'Alle studiar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studiar oppretta av $param'; + } + + @override + String get studyNoneYet => 'Ingen så langt.'; + + @override + String get studyHot => 'Omtykt'; + + @override + String get studyDateAddedNewest => 'Dato tilføydd (siste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføydd (første)'; + + @override + String get studyRecentlyUpdated => 'Nyleg oppdatert'; + + @override + String get studyMostPopular => 'Mest omtykt'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Føy til eit nytt kapittel'; + + @override + String get studyAddMembers => 'Legg til medlemar'; + + @override + String get studyInviteToTheStudy => 'Inviter til studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter berre folk du kjenner og som aktivt ynskjer å delta i studien.'; + + @override + String get studySearchByUsername => 'Søk på brukarnamn'; + + @override + String get studySpectator => 'Tilskodar'; + + @override + String get studyContributor => 'Bidragsytar'; + + @override + String get studyKick => 'Kast ut'; + + @override + String get studyLeaveTheStudy => 'Forlat studien'; + + @override + String get studyYouAreNowAContributor => 'Du er no bidragsytar'; + + @override + String get studyYouAreNowASpectator => 'Du er no tilskodar'; + + @override + String get studyPgnTags => 'PGN-merkelappar'; + + @override + String get studyLike => 'Lik'; + + @override + String get studyUnlike => 'Slutt å lika'; + + @override + String get studyNewTag => 'Ny merkelapp'; + + @override + String get studyCommentThisPosition => 'Kommenter denne stillinga'; + + @override + String get studyCommentThisMove => 'Kommenter dette trekket'; + + @override + String get studyAnnotateWithGlyphs => 'Kommenter med symbol'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittelet er for kort for å analyserast.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Berre bidragsytarar til studien kan be om maskinanalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få full maskinanalyse av hovedvarianten frå serveren.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sørg for at kapittelet er fullført. Du kan berre be om analyse ein gong.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC-medlemene ser den same stillingen'; + + @override + String get studyShareChanges => 'Lagre endringar på serveren og del dei med tilskodarar'; + + @override + String get studyPlaying => 'Spelar no'; + + @override + String get studyShowEvalBar => 'Evalueringssøyler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Attende'; + + @override + String get studyNext => 'Neste'; + + @override + String get studyLast => 'Siste'; + @override String get studyShareAndExport => 'Del & eksporter'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie-PGN'; + + @override + String get studyDownloadAllGames => 'Last ned alle spel'; + + @override + String get studyChapterPgn => 'Kapittel-PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Last ned spel'; + + @override + String get studyStudyUrl => 'Studie-URL'; + + @override + String get studyCurrentChapterUrl => 'Kapittel-URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan lime inn dette i forumet for å syna det der'; + + @override + String get studyStartAtInitialPosition => 'Start ved innleiande stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Inkorporer i websida eller bloggen din'; + + @override + String get studyReadMoreAboutEmbedding => 'Les meir om innbygging'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Berre offentlege studiar kan byggast inn!'; + + @override + String get studyOpen => 'Opne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 presentert av $param2'; + } + + @override + String get studyStudyNotFound => 'Fann ikkje studien'; + + @override + String get studyEditChapter => 'Rediger kapittel'; + + @override + String get studyNewChapter => 'Nytt kapittel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer frå $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Fastspikra kapittelkommentar'; + + @override + String get studySaveChapter => 'Lagre kapittelet'; + + @override + String get studyClearAnnotations => 'Fjern notat'; + + @override + String get studyClearVariations => 'Fjern variantar'; + + @override + String get studyDeleteChapter => 'Slett kapittel'; + + @override + String get studyDeleteThisChapter => 'Slette dette kapittelet? Avgjerda er endeleg og kan ikkje angrast!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Fjern alle kommentarar og figurar i dette kapittelet?'; + + @override + String get studyRightUnderTheBoard => 'Rett under brettet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul neste trekk'; + + @override + String get studyInteractiveLesson => 'Interaktiv leksjon'; + + @override + String studyChapterX(String param) { + return 'Kapittel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved innleiande stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start frå innleiande stilling'; + + @override + String get studyLoadAGameByUrl => 'Last opp eit parti frå URL'; + + @override + String get studyLoadAPositionFromFen => 'Last opp ein stilling frå FEN'; + + @override + String get studyLoadAGameFromPgn => 'Last opp eit parti frå PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Last opp eit parti frå $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opprett kapittel'; + + @override + String get studyCreateStudy => 'Opprett ein studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlegheit'; + + @override + String get studyPublic => 'Offentleg'; + + @override + String get studyUnlisted => 'Ikkje opplista'; + + @override + String get studyInviteOnly => 'Berre etter invitasjon'; + + @override + String get studyAllowCloning => 'Tillat kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Berre meg'; + + @override + String get studyContributors => 'Bidragsytarar'; + + @override + String get studyMembers => 'Medlemar'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Aktiver synk'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behald alle i den same stilllinga'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: lat folk sjå fritt gjennom'; + + @override + String get studyPinnedStudyComment => 'Fastspikra studiekommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Lagre'; + + @override + String get studyClearChat => 'Fjern teksten frå kommentarfeltet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slette studiens kommentar-historikk? Du kan ikkje angre!'; + + @override + String get studyDeleteStudy => 'Slett studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slette heile studien? Avgjerda er endeleg og kan ikke gjeras om! Skriv namnet på studien som skal stadfestast: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kva for ein studie vil du bruke?'; + + @override + String get studyGoodMove => 'Godt trekk'; + + @override + String get studyMistake => 'Mistak'; + + @override + String get studyBrilliantMove => 'Strålande trekk'; + + @override + String get studyBlunder => 'Bukk'; + + @override + String get studyInterestingMove => 'Interessant trekk'; + + @override + String get studyDubiousMove => 'Tvilsamt trekk'; + + @override + String get studyOnlyMove => 'Einaste moglege trekk'; + + @override + String get studyZugzwang => 'Trekktvang'; + + @override + String get studyEqualPosition => 'Lik stilling'; + + @override + String get studyUnclearPosition => 'Uavklart stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Kvit står litt betre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart står litt betre'; + + @override + String get studyWhiteIsBetter => 'Kvit står betre'; + + @override + String get studyBlackIsBetter => 'Svart står betre'; + + @override + String get studyWhiteIsWinning => 'Kvit står til vinst'; + + @override + String get studyBlackIsWinning => 'Svart står til vinst'; + + @override + String get studyNovelty => 'Nyskapning'; + + @override + String get studyDevelopment => 'Utvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Åtak'; + + @override + String get studyCounterplay => 'Motspel'; + + @override + String get studyTimeTrouble => 'Tidsnaud'; + + @override + String get studyWithCompensation => 'Med kompensasjon'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Neste kapittel'; + + @override + String get studyPrevChapter => 'Førre kapittel'; + + @override + String get studyStudyActions => 'Studiehandlingar'; + + @override + String get studyTopics => 'Tema'; + + @override + String get studyMyTopics => 'Mine tema'; + + @override + String get studyPopularTopics => 'Omtykte tema'; + + @override + String get studyManageTopics => 'Administrer tema'; + + @override + String get studyBack => 'Tilbake'; + + @override + String get studyPlayAgain => 'Spel på ny'; + + @override + String get studyWhatWouldYouPlay => 'Kva vil du spela i denne stillinga?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulerar! Du har fullført denne leksjonen.'; + + @override + String studyPerPage(String param) { + return '$param per side'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapittel', + one: '$count kapittel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parti', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count medlemar', + one: '$count medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Sett inn PGN-teksten din her, maksimum $count parti', + one: 'Sett inn PGN-teksten din her, maksimum $count parti', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'for kort tid sidan'; + + @override + String get timeagoRightNow => 'nett no'; + + @override + String get timeagoCompleted => 'fullført'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekund', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutt', + one: 'om $count minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timar', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dagar', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count veker', + one: 'om $count veke', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count månader', + one: 'om $count månad', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutt sidan', + one: '$count minutt sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timar sidan', + one: '$count time sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagar sidan', + one: '$count dag sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count veker sidan', + one: '$count veke sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count månader sidan', + one: '$count månad sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år sidan', + one: '$count år sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutt igjen', + one: '$count minutt igjen', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timar igjen', + one: '$count time igjen', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index ac34b50c46..4abd844ab1 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsPl extends AppLocalizations { AppLocalizationsPl([String locale = 'pl']) : super(locale); @override - String get mobileHomeTab => 'Start'; + String get mobileAllGames => 'Wszystkie partie'; @override - String get mobilePuzzlesTab => 'Zadania'; + String get mobileAreYouSure => 'Jesteś pewien?'; @override - String get mobileToolsTab => 'Narzędzia'; + String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; @override - String get mobileWatchTab => 'Oglądaj'; + String get mobileClearButton => 'Wyczyść'; @override - String get mobileSettingsTab => 'Ustawienia'; + String get mobileCorrespondenceClearSavedMove => 'Usuń zapisany ruch'; @override - String get mobileMustBeLoggedIn => 'Musisz być zalogowany, aby wyświetlić tę stronę.'; + String get mobileCustomGameJoinAGame => 'Dołącz do partii'; @override - String get mobileSystemColors => 'Kolory systemowe'; + String get mobileFeedbackButton => 'Opinie'; @override - String get mobileFeedbackButton => 'Opinie'; + String mobileGreeting(String param) { + return 'Witaj $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Witaj'; @override - String get mobileSettingsHapticFeedback => 'Wibracja przy dotknięciu'; + String get mobileHideVariation => 'Ukryj wariant'; @override - String get mobileSettingsImmersiveMode => 'Tryb pełnoekranowy'; + String get mobileHomeTab => 'Start'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.'; + String get mobileLiveStreamers => 'Aktywni streamerzy'; @override - String get mobileNotFollowingAnyUser => 'Nie obserwujesz żadnego gracza.'; + String get mobileMustBeLoggedIn => 'Musisz być zalogowany, aby wyświetlić tę stronę.'; @override - String get mobileAllGames => 'Wszystkie partie'; + String get mobileNoSearchResults => 'Brak wyników'; @override - String get mobileRecentSearches => 'Ostatnio wyszukiwane'; + String get mobileNotFollowingAnyUser => 'Nie obserwujesz żadnego gracza.'; @override - String get mobileClearButton => 'Wyczyść'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Brak wyników'; + String get mobilePrefMagnifyDraggedPiece => 'Powiększ przeciąganą bierkę'; @override - String get mobileAreYouSure => 'Jesteś pewien?'; + String get mobilePuzzleStormConfirmEndRun => 'Czy chcesz zakończyć tę serię?'; @override - String get mobilePuzzleStreakAbortWarning => 'Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.'; + String get mobilePuzzleStormFilterNothingToShow => 'Brak wyników, zmień proszę filtry'; @override String get mobilePuzzleStormNothingToShow => 'Nic do wyświetlenia. Rozegraj kilka serii.'; @override - String get mobileSharePuzzle => 'Udostępnij to zadanie'; + String get mobilePuzzleStormSubtitle => 'Rozwiąż jak najwięcej zadań w ciągu 3 minut.'; @override - String get mobileShareGameURL => 'Udostępnij adres URL partii'; + String get mobilePuzzleStreakAbortWarning => 'Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.'; @override - String get mobileShareGamePGN => 'Udostępnij PGN'; + String get mobilePuzzleThemesSubtitle => 'Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.'; @override - String get mobileSharePositionAsFEN => 'Udostępnij pozycję jako FEN'; + String get mobilePuzzlesTab => 'Zadania'; @override - String get mobileShowVariations => 'Pokaż warianty'; + String get mobileRecentSearches => 'Ostatnio wyszukiwane'; @override - String get mobileHideVariation => 'Ukryj wariant'; + String get mobileSettingsHapticFeedback => 'Wibracja przy dotknięciu'; @override - String get mobileShowComments => 'Pokaż komentarze'; + String get mobileSettingsImmersiveMode => 'Tryb pełnoekranowy'; @override - String get mobilePuzzleStormConfirmEndRun => 'Czy chcesz zakończyć tę serię?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Brak wyników, zmień proszę filtry'; + String get mobileSettingsTab => 'Ustawienia'; @override - String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; + String get mobileShareGamePGN => 'Udostępnij PGN'; @override - String get mobileCancelDrawOffer => 'Anuluj propozycję remisu'; + String get mobileShareGameURL => 'Udostępnij adres URL partii'; @override - String get mobileWaitingForOpponentToJoin => 'Oczekiwanie na dołączenie przeciwnika...'; + String get mobileSharePositionAsFEN => 'Udostępnij pozycję jako FEN'; @override - String get mobileBlindfoldMode => 'Gra na ślepo'; + String get mobileSharePuzzle => 'Udostępnij to zadanie'; @override - String get mobileLiveStreamers => 'Aktywni streamerzy'; + String get mobileShowComments => 'Pokaż komentarze'; @override - String get mobileCustomGameJoinAGame => 'Dołącz do partii'; + String get mobileShowResult => 'Pokaż wynik'; @override - String get mobileCorrespondenceClearSavedMove => 'Usuń zapisany ruch'; + String get mobileShowVariations => 'Pokaż warianty'; @override String get mobileSomethingWentWrong => 'Coś poszło nie tak.'; @override - String get mobileShowResult => 'Pokaż wynik'; - - @override - String get mobilePuzzleThemesSubtitle => 'Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.'; + String get mobileSystemColors => 'Kolory systemowe'; @override - String get mobilePuzzleStormSubtitle => 'Rozwiąż jak najwięcej zadań w ciągu 3 minut.'; + String get mobileTheme => 'Motyw'; @override - String mobileGreeting(String param) { - return 'Witaj $param'; - } + String get mobileToolsTab => 'Narzędzia'; @override - String get mobileGreetingWithoutName => 'Witaj'; + String get mobileWaitingForOpponentToJoin => 'Oczekiwanie na dołączenie przeciwnika...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Oglądaj'; @override String get activityActivity => 'Aktywność'; @@ -262,6 +259,19 @@ class AppLocalizationsPl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zakończone $count $param2 partii korespondencyjnych', + many: 'Zakończone $count $param2 partii korespondencyjnych', + few: 'Zakończone $count $param2 partie korespondencyjne', + one: 'Zakończona $count $param2 partia korespondencyjna', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsPl extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmisje'; + @override + String get broadcastMyBroadcasts => 'Moje transmisje'; + @override String get broadcastLiveBroadcasts => 'Transmisje turniejów na żywo'; + @override + String get broadcastBroadcastCalendar => 'Kalendarz transmisji'; + + @override + String get broadcastNewBroadcast => 'Nowa transmisja na żywo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subskrybowane transmisje'; + + @override + String get broadcastAboutBroadcasts => 'O transmisji'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Jak korzystać z transmisji na Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Nowa runda będzie miała tych samych uczestników co poprzednia.'; + + @override + String get broadcastAddRound => 'Dodaj rundę'; + + @override + String get broadcastOngoing => 'Trwające'; + + @override + String get broadcastUpcoming => 'Nadchodzące'; + + @override + String get broadcastCompleted => 'Zakończone'; + + @override + String get broadcastCompletedHelp => 'Lichess wykrywa ukończenie rundy w oparciu o śledzone partie. Użyj tego przełącznika, jeśli nie ma takich partii.'; + + @override + String get broadcastRoundName => 'Nazwa rundy'; + + @override + String get broadcastRoundNumber => 'Numer rundy'; + + @override + String get broadcastTournamentName => 'Nazwa turnieju'; + + @override + String get broadcastTournamentDescription => 'Krótki opis turnieju'; + + @override + String get broadcastFullDescription => 'Pełny opis wydarzenia'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opcjonalny długi opis transmisji. $param1 jest dostępny. Długość musi być mniejsza niż $param2 znaków.'; + } + + @override + String get broadcastSourceSingleUrl => 'Adres URL zapisu PGN'; + + @override + String get broadcastSourceUrlHelp => 'Adres URL, który Lichess będzie udostępniał, aby można było uzyskać aktualizacje PGN. Musi być publicznie dostępny z internetu.'; + + @override + String get broadcastSourceGameIds => 'Do 64 identyfikatorów partii, oddzielonych spacjami.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data rozpoczęcia w lokalnej strefie czasowej turnieju: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcjonalne, jeśli wiesz kiedy wydarzenie się rozpocznie'; + + @override + String get broadcastCurrentGameUrl => 'Adres URL bieżącej partii'; + + @override + String get broadcastDownloadAllRounds => 'Pobierz wszystkie rundy'; + + @override + String get broadcastResetRound => 'Zresetuj tę rundę'; + + @override + String get broadcastDeleteRound => 'Usuń tę rundę'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Ostatecznie usuń rundę i jej wszystkie partie.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Usuń wszystkie partie w tej rundzie. Źródło będzie musiało być aktywne, aby je odtworzyć.'; + + @override + String get broadcastEditRoundStudy => 'Edytuj opracowanie rundy'; + + @override + String get broadcastDeleteTournament => 'Usuń ten turniej'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Ostatecznie usuń cały turniej, jego wszystkie rundy i partie.'; + + @override + String get broadcastShowScores => 'Pokaż wyniki graczy na podstawie wyników gry'; + + @override + String get broadcastReplacePlayerTags => 'Opcjonalnie: zmień nazwy, rankingi oraz tytuły gracza'; + + @override + String get broadcastFideFederations => 'Federacje FIDE'; + + @override + String get broadcastTop10Rating => '10 najlepszych rankingów'; + + @override + String get broadcastFidePlayers => 'Zawodnicy FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Nie znaleziono zawodnika FIDE'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federacja'; + + @override + String get broadcastAgeThisYear => 'Wiek w tym roku'; + + @override + String get broadcastUnrated => 'Bez rankingu'; + + @override + String get broadcastRecentTournaments => 'Najnowsze turnieje'; + + @override + String get broadcastOpenLichess => 'Otwórz w Lichess'; + + @override + String get broadcastTeams => 'Drużyny'; + + @override + String get broadcastBoards => 'Szachownice'; + + @override + String get broadcastOverview => 'Podgląd'; + + @override + String get broadcastSubscribeTitle => 'Subskrybuj, aby dostawać powiadomienia o każdej rozpoczętej rundzie. W preferencjach konta możesz przełączać czy chcesz powiadomienia dźwiękowe czy wyskakujące notyfikacje tekstowe.'; + + @override + String get broadcastUploadImage => 'Prześlij logo turnieju'; + + @override + String get broadcastNoBoardsYet => 'Szachownice pojawią się jak tylko załadują się partie.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Szachownice mogą być załadowane bezpośrednio ze źródła lub przez $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Rozpoczyna się po $param'; + } + + @override + String get broadcastStartVerySoon => 'Transmisja wkrótce się rozpocznie.'; + + @override + String get broadcastNotYetStarted => 'Transmisja jeszcze się nie rozpoczęła.'; + + @override + String get broadcastOfficialWebsite => 'Oficjalna strona'; + + @override + String get broadcastStandings => 'Klasyfikacja'; + + @override + String get broadcastOfficialStandings => 'Oficjalna klasyfikacja'; + + @override + String broadcastIframeHelp(String param) { + return 'Więcej opcji na $param'; + } + + @override + String get broadcastWebmastersPage => 'stronie webmasterów'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Publiczne źródło PGN w czasie rzeczywistym dla tej rundy. Oferujemy również $param dla szybszej i skuteczniejszej synchronizacji.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Umieść tę transmisję na swojej stronie internetowej'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Osadź $param na swojej stronie internetowej'; + } + + @override + String get broadcastRatingDiff => 'Różnica rankingu'; + + @override + String get broadcastGamesThisTournament => 'Partie w tym turnieju'; + + @override + String get broadcastScore => 'Wynik'; + + @override + String get broadcastAllTeams => 'Wszystkie kluby'; + + @override + String get broadcastTournamentFormat => 'Format turnieju'; + + @override + String get broadcastTournamentLocation => 'Lokalizacja turnieju'; + + @override + String get broadcastTopPlayers => 'Najlepsi gracze'; + + @override + String get broadcastTimezone => 'Strefa czasowa'; + + @override + String get broadcastFideRatingCategory => 'Kategoria rankingu FIDE'; + + @override + String get broadcastOptionalDetails => 'Opcjonalne szczegóły'; + + @override + String get broadcastPastBroadcasts => 'Poprzednie transmisje'; + + @override + String get broadcastAllBroadcastsByMonth => 'Zobacz wszystkie transmisje w danym miesiącu'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmisji', + many: '$count transmisji', + few: '$count transmisje', + one: '$count transmisja', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Wyzwania: $param1'; @@ -490,7 +749,7 @@ class AppLocalizationsPl extends AppLocalizations { String get perfStatProvisional => 'prowizoryczny'; @override - String get perfStatNotEnoughRatedGames => 'Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodną ranking.'; + String get perfStatNotEnoughRatedGames => 'Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodny ranking.'; @override String perfStatProgressOverLastXGames(String param) { @@ -643,6 +902,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get preferencesInGameOnly => 'Tylko w partii'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Zegar szachowy'; @@ -784,6 +1046,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Dźwięk powiadomień'; + @override + String get preferencesBlindfold => 'Gra na ślepo'; + @override String get puzzlePuzzles => 'Zadania szachowe'; @@ -1434,10 +1699,10 @@ class AppLocalizationsPl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ograniczone ruchy przeciwnika powodują, że każde posunięcie pogarsza jego pozycję.'; @override - String get puzzleThemeHealthyMix => 'Miszmasz'; + String get puzzleThemeMix => 'Miszmasz'; @override - String get puzzleThemeHealthyMixDescription => 'Bądź gotów na wszystko! Jak podczas prawdziwej partii.'; + String get puzzleThemeMixDescription => 'Po trochu wszystkiego. Nie wiesz czego się spodziewać, więc bądź gotów na wszystko! Tak jak w prawdziwej partii.'; @override String get puzzleThemePlayerGames => 'Partie gracza'; @@ -1686,7 +1951,7 @@ class AppLocalizationsPl extends AppLocalizations { String get expandVariations => 'Rozwiń warianty'; @override - String get forceVariation => 'Przedstaw jako wariant'; + String get forceVariation => 'Zamień w wariant'; @override String get copyVariationPgn => 'Skopiuj wariant PGN'; @@ -1773,7 +2038,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get playFirstOpeningEndgameExplorerMove => 'Zagraj pierwsze posunięcie z przeglądarki otwarć/końcówek'; + String get playFirstOpeningEndgameExplorerMove => 'Zagraj pierwsze posunięcie z biblioteki otwarć'; @override String get winPreventedBy50MoveRule => 'Bez wygranej ze względu na regułę 50 ruchów'; @@ -1811,9 +2076,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get byCPL => 'Wg SCP'; - @override - String get openStudy => 'Otwórz opracowanie'; - @override String get enable => 'Włącz'; @@ -1841,9 +2103,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get removesTheDepthLimit => 'Usuwa limit głębokości analizy i rozgrzewa Twój komputer do czerwoności ;)'; - @override - String get engineManager => 'Ustawienia silnika'; - @override String get blunder => 'Błąd'; @@ -2107,6 +2366,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get gamesPlayed => 'Rozegranych partii'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anuluj'; @@ -2481,9 +2743,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get unblock => 'Odblokuj'; - @override - String get followsYou => 'Obserwuje Cię'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 obserwuje $param2'; @@ -2759,7 +3018,7 @@ class AppLocalizationsPl extends AppLocalizations { String get website => 'Strona internetowa'; @override - String get mobile => 'Mobile'; + String get mobile => 'Aplikacja mobilna'; @override String get help => 'Porada:'; @@ -2816,7 +3075,13 @@ class AppLocalizationsPl extends AppLocalizations { String get other => 'Inne'; @override - String get reportDescriptionHelp => 'Wklej odnośnik do partii i wyjaśnij, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko, że „oszukuje”, ale wytłumacz nam, na jakiej podstawie doszedłeś/aś do takiego wniosku. Odniesiemy się do twojego zgłoszenia szybciej, jeżeli napiszesz je w języku angielskim.'; + String get reportCheatBoostHelp => 'Wklej link do partii i wytłumacz, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko \"on oszukiwał\", lecz napisz jak doszedłeś/aś do tego wniosku.'; + + @override + String get reportUsernameHelp => 'Wytłumacz, co w nazwie użytkownika jest obraźliwe. Nie pisz tylko \"nazwa jest obraźliwa\", lecz napisz jak doszedłeś/aś do tego wniosku, zwłaszcza jeśli jest mało znany, nie po angielsku, slangowy lub odniesieniem do kultury/historii.'; + + @override + String get reportProcessedFasterInEnglish => 'Twoje zgłoszenie będzie sprawdzone szybciej, jeśli zostanie napisane po angielsku.'; @override String get error_provideOneCheatedGameLink => 'Podaj przynajmniej jeden odnośnik do gry, w której oszukiwano.'; @@ -4121,6 +4386,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get nothingToSeeHere => 'W tej chwili nie ma nic do zobaczenia.'; + @override + String get stats => 'Statystyki'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5123,731 @@ class AppLocalizationsPl extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamerzy Lichess'; + @override + String get studyPrivate => 'Prywatne'; + + @override + String get studyMyStudies => 'Moje opracowania'; + + @override + String get studyStudiesIContributeTo => 'Opracowania, które współtworzę'; + + @override + String get studyMyPublicStudies => 'Moje publiczne opracowania'; + + @override + String get studyMyPrivateStudies => 'Moje prywatne opracowania'; + + @override + String get studyMyFavoriteStudies => 'Moje ulubione opracowania'; + + @override + String get studyWhatAreStudies => 'Czym są opracowania?'; + + @override + String get studyAllStudies => 'Wszystkie opracowania'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Opracowanie stworzone przez $param'; + } + + @override + String get studyNoneYet => 'Jeszcze brak.'; + + @override + String get studyHot => 'Hity'; + + @override + String get studyDateAddedNewest => 'Data dodania (od najnowszych)'; + + @override + String get studyDateAddedOldest => 'Data dodania (od najstarszych)'; + + @override + String get studyRecentlyUpdated => 'Ostatnio aktualizowane'; + + @override + String get studyMostPopular => 'Najpopularniejsze'; + + @override + String get studyAlphabetical => 'Alfabetycznie'; + + @override + String get studyAddNewChapter => 'Dodaj nowy rozdział'; + + @override + String get studyAddMembers => 'Dodaj uczestników'; + + @override + String get studyInviteToTheStudy => 'Zaproś do opracowania'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Zapraszaj do opracowania tylko znajomych, którzy chcą w nim aktywnie uczestniczyć.'; + + @override + String get studySearchByUsername => 'Szukaj wg nazwy użytkownika'; + + @override + String get studySpectator => 'Obserwator'; + + @override + String get studyContributor => 'Współautor'; + + @override + String get studyKick => 'Wyrzuć'; + + @override + String get studyLeaveTheStudy => 'Opuść opracowanie'; + + @override + String get studyYouAreNowAContributor => 'Jesteś teraz współautorem'; + + @override + String get studyYouAreNowASpectator => 'Jesteś teraz obserwatorem'; + + @override + String get studyPgnTags => 'Znaczniki PGN'; + + @override + String get studyLike => 'Lubię to'; + + @override + String get studyUnlike => 'Cofnij polubienie'; + + @override + String get studyNewTag => 'Nowy znacznik'; + + @override + String get studyCommentThisPosition => 'Skomentuj tę pozycję'; + + @override + String get studyCommentThisMove => 'Skomentuj ten ruch'; + + @override + String get studyAnnotateWithGlyphs => 'Dodaj adnotacje symbolami'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Rozdział jest zbyt krótki do analizy.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tylko współautorzy opracowania mogą prosić o analizę komputerową.'; + + @override + String get studyGetAFullComputerAnalysis => 'Uzyskaj pełną, zdalną analizę komputerową głównego wariantu.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Upewnij się, że rozdział jest kompletny. O jego analizę możesz poprosić tylko raz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Wszyscy zsynchronizowani uczestnicy pozostają na tej samej pozycji'; + + @override + String get studyShareChanges => 'Współdzielenie zmian z obserwatorami i ich zapis na serwerze'; + + @override + String get studyPlaying => 'W toku'; + + @override + String get studyShowEvalBar => 'Paski ewaluacji'; + + @override + String get studyFirst => 'Pierwszy'; + + @override + String get studyPrevious => 'Poprzedni'; + + @override + String get studyNext => 'Następny'; + + @override + String get studyLast => 'Ostatni'; + @override String get studyShareAndExport => 'Udostępnianie i eksport'; + @override + String get studyCloneStudy => 'Powiel'; + + @override + String get studyStudyPgn => 'PGN opracowania'; + + @override + String get studyDownloadAllGames => 'Pobierz wszystkie partie'; + + @override + String get studyChapterPgn => 'PGN rozdziału'; + + @override + String get studyCopyChapterPgn => 'Kopiuj PGN'; + + @override + String get studyDownloadGame => 'Pobierz partię'; + + @override + String get studyStudyUrl => 'Link do opracowania'; + + @override + String get studyCurrentChapterUrl => 'URL bieżącego rozdziału'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Możesz wkleić to, aby osadzić na forum'; + + @override + String get studyStartAtInitialPosition => 'Rozpocznij z pozycji początkowej'; + + @override + String studyStartAtX(String param) { + return 'Rozpocznij od $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Udostępnij na swojej stronie lub na blogu'; + + @override + String get studyReadMoreAboutEmbedding => 'Dowiedz się więcej o osadzaniu'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Tylko publiczne opracowania mogą być osadzane!'; + + @override + String get studyOpen => 'Otwórz'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 przygotowane przez $param2'; + } + + @override + String get studyStudyNotFound => 'Nie znaleziono opracowania'; + + @override + String get studyEditChapter => 'Edytuj rozdział'; + + @override + String get studyNewChapter => 'Nowy rozdział'; + + @override + String studyImportFromChapterX(String param) { + return 'Zaimportuj z $param'; + } + + @override + String get studyOrientation => 'Orientacja'; + + @override + String get studyAnalysisMode => 'Rodzaj analizy'; + + @override + String get studyPinnedChapterComment => 'Przypięty komentarz'; + + @override + String get studySaveChapter => 'Zapisz rozdział'; + + @override + String get studyClearAnnotations => 'Usuń adnotacje'; + + @override + String get studyClearVariations => 'Wyczyść warianty'; + + @override + String get studyDeleteChapter => 'Usuń rozdział'; + + @override + String get studyDeleteThisChapter => 'Usunąć ten rozdział? Nie będzie można tego cofnąć!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Usunąć wszystkie komentarze i oznaczenia w tym rozdziale?'; + + @override + String get studyRightUnderTheBoard => 'Pod szachownicą, po prawej stronie'; + + @override + String get studyNoPinnedComment => 'Brak'; + + @override + String get studyNormalAnalysis => 'Normalna'; + + @override + String get studyHideNextMoves => 'Ukryj następne posunięcia'; + + @override + String get studyInteractiveLesson => 'Lekcja interaktywna'; + + @override + String studyChapterX(String param) { + return 'Rozdział $param'; + } + + @override + String get studyEmpty => 'Pusty'; + + @override + String get studyStartFromInitialPosition => 'Rozpocznij z pozycji początkowej'; + + @override + String get studyEditor => 'Edytor'; + + @override + String get studyStartFromCustomPosition => 'Rozpocznij z ustawionej pozycji'; + + @override + String get studyLoadAGameByUrl => 'Zaimportuj partię z linku'; + + @override + String get studyLoadAPositionFromFen => 'Zaimportuj partię z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Zaimportuj partię z PGN'; + + @override + String get studyAutomatic => 'Automatycznie'; + + @override + String get studyUrlOfTheGame => 'Link do partii'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Zaimportuj partię z $param1 lub $param2'; + } + + @override + String get studyCreateChapter => 'Stwórz rozdział'; + + @override + String get studyCreateStudy => 'Stwórz opracowanie'; + + @override + String get studyEditStudy => 'Edytuj opracowanie'; + + @override + String get studyVisibility => 'Widoczność'; + + @override + String get studyPublic => 'Publiczne'; + + @override + String get studyUnlisted => 'Niepubliczne'; + + @override + String get studyInviteOnly => 'Tylko zaproszeni'; + + @override + String get studyAllowCloning => 'Pozwól kopiować'; + + @override + String get studyNobody => 'Nikt'; + + @override + String get studyOnlyMe => 'Tylko ja'; + + @override + String get studyContributors => 'Współautorzy'; + + @override + String get studyMembers => 'Uczestnicy'; + + @override + String get studyEveryone => 'Każdy'; + + @override + String get studyEnableSync => 'Włącz synchronizację'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Tak: utrzymaj wszystkich w tej samej pozycji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nie: pozwól oglądać wszystkim'; + + @override + String get studyPinnedStudyComment => 'Przypięte komentarze'; + @override String get studyStart => 'Rozpocznij'; + + @override + String get studySave => 'Zapisz'; + + @override + String get studyClearChat => 'Wyczyść czat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Usunąć historię czatu opracowania? Nie będzie można tego cofnąć!'; + + @override + String get studyDeleteStudy => 'Usuń opracowanie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Usunąć opracowanie? Nie będzie można go odzyskać! Wpisz nazwę opracowania, aby potwierdzić operację: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdzie chcesz się tego uczyć?'; + + @override + String get studyGoodMove => 'Dobry ruch'; + + @override + String get studyMistake => 'Pomyłka'; + + @override + String get studyBrilliantMove => 'Świetny ruch'; + + @override + String get studyBlunder => 'Błąd'; + + @override + String get studyInterestingMove => 'Interesujący ruch'; + + @override + String get studyDubiousMove => 'Wątpliwy ruch'; + + @override + String get studyOnlyMove => 'Jedyny ruch'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Równa pozycja'; + + @override + String get studyUnclearPosition => 'Niejasna pozycja'; + + @override + String get studyWhiteIsSlightlyBetter => 'Białe stoją nieznacznie lepiej'; + + @override + String get studyBlackIsSlightlyBetter => 'Czarne stoją nieznacznie lepiej'; + + @override + String get studyWhiteIsBetter => 'Białe stoją lepiej'; + + @override + String get studyBlackIsBetter => 'Czarne stoją lepiej'; + + @override + String get studyWhiteIsWinning => 'Białe wygrywają'; + + @override + String get studyBlackIsWinning => 'Czarne wygrywają'; + + @override + String get studyNovelty => 'Nowość'; + + @override + String get studyDevelopment => 'Rozwój'; + + @override + String get studyInitiative => 'Inicjatywa'; + + @override + String get studyAttack => 'Atak'; + + @override + String get studyCounterplay => 'Przeciwdziałanie'; + + @override + String get studyTimeTrouble => 'Problem z czasem'; + + @override + String get studyWithCompensation => 'Z rekompensatą'; + + @override + String get studyWithTheIdea => 'Z pomysłem'; + + @override + String get studyNextChapter => 'Następny rozdział'; + + @override + String get studyPrevChapter => 'Poprzedni rozdział'; + + @override + String get studyStudyActions => 'Opcje opracowań'; + + @override + String get studyTopics => 'Tematy'; + + @override + String get studyMyTopics => 'Moje tematy'; + + @override + String get studyPopularTopics => 'Popularne tematy'; + + @override + String get studyManageTopics => 'Zarządzaj tematami'; + + @override + String get studyBack => 'Powrót'; + + @override + String get studyPlayAgain => 'Odtwórz ponownie'; + + @override + String get studyWhatWouldYouPlay => 'Co byś zagrał w tej pozycji?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulacje! Ukończono tę lekcję.'; + + @override + String studyPerPage(String param) { + return '$param na stronie'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rozdziałów', + many: '$count rozdziałów', + few: '$count rozdziały', + one: '$count rozdział', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partii', + many: '$count partii', + few: '$count partie', + one: '$count partia', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uczestników', + many: '$count uczestników', + few: '$count uczestników', + one: '$count uczestnik', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Wklej tutaj swój PGN, max $count partii', + many: 'Wklej tutaj swój PGN, max $count partii', + few: 'Wklej tutaj swój PGN, max $count partie', + one: 'Wklej tutaj swój PGN, max $count partię', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'właśnie teraz'; + + @override + String get timeagoRightNow => 'w tej chwili'; + + @override + String get timeagoCompleted => 'ukończone'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekund', + many: 'za $count sekund', + few: 'za $count sekundy', + one: 'za $count sekundę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minut', + many: 'za $count minuty', + few: 'za $count minuty', + one: 'za $count minutę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godzin', + many: 'za $count godzin', + few: 'za $count godziny', + one: 'za $count godzinę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dni', + many: 'za $count dni', + few: 'za $count dni', + one: 'za $count dzień', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count tygodni', + many: 'za $count tygodni', + few: 'za $count tygodnie', + one: 'za $count tydzień', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count miesięcy', + many: 'za $count miesięcy', + few: 'za $count miesiące', + one: 'za $count miesiąc', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count lat', + many: 'za $count lat', + few: 'za $count lata', + one: 'za $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minut temu', + many: '$count minut temu', + few: '$count minuty temu', + one: '$count minutę temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count godzin temu', + many: '$count godzin temu', + few: '$count godziny temu', + one: '$count godzinę temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dni temu', + many: '$count dni temu', + few: '$count dni temu', + one: '$count dzień temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tygodni temu', + many: '$count tygodni temu', + few: '$count tygodnie temu', + one: '$count tydzień temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count miesięcy temu', + many: '$count miesięcy temu', + few: '$count miesiące temu', + one: '$count miesiąc temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lat temu', + many: '$count lat temu', + few: '$count lata temu', + one: '$count rok temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pozostało $count minut', + many: 'Pozostało $count minut', + few: 'Pozostały $count minuty', + one: 'Pozostała $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pozostało $count godzin', + many: 'Pozostało $count godzin', + few: 'Pozostały $count godziny', + one: 'Pozostała $count godzina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index fc5c158b3a..bae067a3e3 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsPt extends AppLocalizations { AppLocalizationsPt([String locale = 'pt']) : super(locale); @override - String get mobileHomeTab => 'Início'; + String get mobileAllGames => 'Todos os jogos'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get mobileAreYouSure => 'Tens a certeza?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; @override - String get mobileWatchTab => 'Assistir'; + String get mobileClearButton => 'Limpar'; @override - String get mobileSettingsTab => 'Definições'; + String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvo'; @override - String get mobileMustBeLoggedIn => 'Tem de iniciar sessão para visualizar esta página.'; + String get mobileCustomGameJoinAGame => 'Entrar num jogo'; @override - String get mobileSystemColors => 'Cores do sistema'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Olá, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get mobileSettingsHapticFeedback => 'Feedback tátil'; + String get mobileHideVariation => 'Ocultar variação'; @override - String get mobileSettingsImmersiveMode => 'Modo imersivo'; + String get mobileHomeTab => 'Início'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.'; + String get mobileLiveStreamers => 'Streamers em direto'; @override - String get mobileNotFollowingAnyUser => 'Não segues nenhum utilizador.'; + String get mobileMustBeLoggedIn => 'Tem de iniciar sessão para visualizar esta página.'; @override - String get mobileAllGames => 'Todos os jogos'; + String get mobileNoSearchResults => 'Sem resultados'; @override - String get mobileRecentSearches => 'Pesquisas recentes'; + String get mobileNotFollowingAnyUser => 'Não segues nenhum utilizador.'; @override - String get mobileClearButton => 'Limpar'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sem resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça arrastada'; @override - String get mobileAreYouSure => 'Tens a certeza?'; + String get mobilePuzzleStormConfirmEndRun => 'Queres terminar esta corrida?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderas a tua sequência atual e a pontuação será salva.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar, por favor, altera os filtros'; @override String get mobilePuzzleStormNothingToShow => 'Nada para mostrar. Joga alguns Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Partilhar este problema'; + String get mobilePuzzleStormSubtitle => 'Resolve quantos problemas for possível em 3 minutos.'; @override - String get mobileShareGameURL => 'Partilhar o URL do jogo'; + String get mobilePuzzleStreakAbortWarning => 'Perderas a tua sequência atual e a pontuação será salva.'; @override - String get mobileShareGamePGN => 'Partilhar PGN'; + String get mobilePuzzleThemesSubtitle => 'Joga problemas das tuas aberturas favoritas, ou escolhe um tema.'; @override - String get mobileSharePositionAsFEN => 'Partilhar posição como FEN'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get mobileShowVariations => 'Mostrar variações'; + String get mobileRecentSearches => 'Pesquisas recentes'; @override - String get mobileHideVariation => 'Ocultar variação'; + String get mobileSettingsHapticFeedback => 'Feedback tátil'; @override - String get mobileShowComments => 'Mostrar comentários'; + String get mobileSettingsImmersiveMode => 'Modo imersivo'; @override - String get mobilePuzzleStormConfirmEndRun => 'Queres terminar esta corrida?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar, por favor, altera os filtros'; + String get mobileSettingsTab => 'Definições'; @override - String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; + String get mobileShareGamePGN => 'Partilhar PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Partilhar o URL do jogo'; @override - String get mobileWaitingForOpponentToJoin => 'À espera do adversário entrar...'; + String get mobileSharePositionAsFEN => 'Partilhar posição como FEN'; @override - String get mobileBlindfoldMode => 'De olhos vendados'; + String get mobileSharePuzzle => 'Partilhar este problema'; @override - String get mobileLiveStreamers => 'Streamers em direto'; + String get mobileShowComments => 'Mostrar comentários'; @override - String get mobileCustomGameJoinAGame => 'Entrar num jogo'; + String get mobileShowResult => 'Mostrar resultado'; @override - String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvo'; + String get mobileShowVariations => 'Mostrar variações'; @override String get mobileSomethingWentWrong => 'Algo deu errado.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'À espera do adversário entrar...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Assistir'; @override String get activityActivity => 'Atividade'; @@ -246,6 +243,17 @@ class AppLocalizationsPt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completou $count jogos $param2 por correspondência', + one: 'Completou $count jogo $param2 por correspondência', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsPt extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmissões'; + @override + String get broadcastMyBroadcasts => 'As minhas transmissões'; + @override String get broadcastLiveBroadcasts => 'Transmissões do torneio em direto'; + @override + String get broadcastBroadcastCalendar => 'Calendário de transmissão'; + + @override + String get broadcastNewBroadcast => 'Nova transmissão em direto'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmissões subscritas'; + + @override + String get broadcastAboutBroadcasts => 'Sobre Transmissões'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como usar as Transmissões do Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'A nova ronda terá os mesmos membros e contribuidores que a anterior.'; + + @override + String get broadcastAddRound => 'Adicionar uma ronda'; + + @override + String get broadcastOngoing => 'A decorrer'; + + @override + String get broadcastUpcoming => 'Brevemente'; + + @override + String get broadcastCompleted => 'Concluído'; + + @override + String get broadcastCompletedHelp => 'Lichess deteta a conclusão da ronda baseada nos jogos da fonte. Use essa opção se não houver fonte.'; + + @override + String get broadcastRoundName => 'Nome da ronda'; + + @override + String get broadcastRoundNumber => 'Número da ronda'; + + @override + String get broadcastTournamentName => 'Nome do torneio'; + + @override + String get broadcastTournamentDescription => 'Breve descrição do torneio'; + + @override + String get broadcastFullDescription => 'Descrição completa do evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrição longa do evento opcional da transmissão. $param1 está disponível. Tem de ter menos que $param2 carácteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL da fonte PGN'; + + @override + String get broadcastSourceUrlHelp => 'Link que o Lichess vai verificar para obter atualizações da PGN. Deve ser acessível ao público a partir da internet.'; + + @override + String get broadcastSourceGameIds => 'Até 64 IDs de jogo Lichess, separados por espaços.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data de início no fuso horário local do torneio: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, se souberes quando começa o evento'; + + @override + String get broadcastCurrentGameUrl => 'Link da partida atual'; + + @override + String get broadcastDownloadAllRounds => 'Transferir todas as rondas'; + + @override + String get broadcastResetRound => 'Reiniciar esta ronda'; + + @override + String get broadcastDeleteRound => 'Apagar esta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente a ronda e os seus jogos.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todos os jogos desta ronda. A fonte deverá estar ativa para poder recriá-los.'; + + @override + String get broadcastEditRoundStudy => 'Editar estudo da ronda'; + + @override + String get broadcastDeleteTournament => 'Eliminar este torneio'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Excluir definitivamente todo o torneio, todas as rondas e todos os jogos.'; + + @override + String get broadcastShowScores => 'Mostra as pontuações dos jogadores com base nos resultados dos jogos'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: substituir nomes de jogadores, avaliações e títulos'; + + @override + String get broadcastFideFederations => 'Federações FIDE'; + + @override + String get broadcastTop10Rating => '10 melhores classificações'; + + @override + String get broadcastFidePlayers => 'Jogadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jogador FIDE não encontrado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federação'; + + @override + String get broadcastAgeThisYear => 'Idade neste ano'; + + @override + String get broadcastUnrated => 'Sem classificação'; + + @override + String get broadcastRecentTournaments => 'Torneio recentes'; + + @override + String get broadcastOpenLichess => 'Abrir no Lichess'; + + @override + String get broadcastTeams => 'Equipas'; + + @override + String get broadcastBoards => 'Tabuleiros'; + + @override + String get broadcastOverview => 'Visão geral'; + + @override + String get broadcastSubscribeTitle => 'Subscreva para ser notificado quando cada ronda começar. Podes ativar o sino ou as notificações push para transmissões nas preferências da tua conta.'; + + @override + String get broadcastUploadImage => 'Carregar imagem do torneio'; + + @override + String get broadcastNoBoardsYet => 'Ainda não há tabuleiros. Estes aparecerão assim que os jogos forem carregados.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Os tabuleiros podem ser carregados com uma fonte ou através do $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Começa após $param'; + } + + @override + String get broadcastStartVerySoon => 'A transmissão terá início muito em breve.'; + + @override + String get broadcastNotYetStarted => 'A transmissão ainda não começou.'; + + @override + String get broadcastOfficialWebsite => 'Website oficial'; + + @override + String get broadcastStandings => 'Classificações'; + + @override + String get broadcastOfficialStandings => 'Classificações oficiais'; + + @override + String broadcastIframeHelp(String param) { + return 'Mais opções na $param'; + } + + @override + String get broadcastWebmastersPage => 'página webmasters'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Uma fonte PGN pública em tempo real para esta ronda. Oferecemos também a $param para uma sincronização mais rápida e eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incorporar esta transmissão no teu website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incorporar $param no teu website'; + } + + @override + String get broadcastRatingDiff => 'Diferença de Elo'; + + @override + String get broadcastGamesThisTournament => 'Jogos deste torneio'; + + @override + String get broadcastScore => 'Pontuação'; + + @override + String get broadcastAllTeams => 'Todas as equipas'; + + @override + String get broadcastTournamentFormat => 'Formato do torneio'; + + @override + String get broadcastTournamentLocation => 'Localização do Torneio'; + + @override + String get broadcastTopPlayers => 'Melhores jogadores'; + + @override + String get broadcastTimezone => 'Fuso horário'; + + @override + String get broadcastFideRatingCategory => 'Categoria do Elo FIDE'; + + @override + String get broadcastOptionalDetails => 'Detalhes opcionais'; + + @override + String get broadcastPastBroadcasts => 'Transmissões anteriores'; + + @override + String get broadcastAllBroadcastsByMonth => 'Ver todas as transmissões por mês'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmissões', + one: '$count transmissão', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafios: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get preferencesInGameOnly => 'Apenas em Jogo'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Relógio de xadrez'; @@ -750,6 +1008,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Som da notificação'; + @override + String get preferencesBlindfold => 'De olhos vendados'; + @override String get puzzlePuzzles => 'Problemas'; @@ -1390,10 +1651,10 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.'; @override - String get puzzleThemeHealthyMix => 'Mistura saudável'; + String get puzzleThemeMix => 'Mistura saudável'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; + String get puzzleThemeMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; @override String get puzzleThemePlayerGames => 'Jogos de jogadores'; @@ -1767,9 +2028,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get byCPL => 'Por CPL'; - @override - String get openStudy => 'Abrir estudo'; - @override String get enable => 'Ativar'; @@ -1797,9 +2055,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get removesTheDepthLimit => 'Remove o limite de profundidade e mantém o teu computador quente'; - @override - String get engineManager => 'Gestão do motor'; - @override String get blunder => 'Erro grave'; @@ -1878,7 +2133,7 @@ class AppLocalizationsPt extends AppLocalizations { String get friends => 'Amigos'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'outros jogadores'; @override String get discussions => 'Conversas'; @@ -2063,6 +2318,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get gamesPlayed => 'Partidas jogadas'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancelar'; @@ -2401,7 +2659,7 @@ class AppLocalizationsPt extends AppLocalizations { String get retry => 'Tentar novamente'; @override - String get reconnecting => 'Reconectando'; + String get reconnecting => 'A reconectar'; @override String get noNetwork => 'Desligado'; @@ -2437,9 +2695,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Segue-te'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 começou a seguir $param2'; @@ -2712,10 +2967,10 @@ class AppLocalizationsPt extends AppLocalizations { String get yes => 'Sim'; @override - String get website => 'Website'; + String get website => 'Site'; @override - String get mobile => 'Mobile'; + String get mobile => 'Telemóvel'; @override String get help => 'Ajuda:'; @@ -2772,7 +3027,13 @@ class AppLocalizationsPt extends AppLocalizations { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.'; + String get reportCheatBoostHelp => 'Cola o(s) link(s) do(s) jogo(s) e explica o que está errado no comportamento deste utilizador. Não digas apenas “eles fazem batota”, mas diz-nos como chegaste a essa conclusão.'; + + @override + String get reportUsernameHelp => 'Explica o que este nome de utilizador tem de ofensivo. Não digas apenas “é ofensivo/inapropriado”, mas diz-nos como chegaste a essa conclusão, especialmente se o insulto for ofuscado, não estiver em inglês, estiver em calão ou for uma referência histórica/cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'A tua denúncia será processado mais rapidamente se estiver escrito em inglês.'; @override String get error_provideOneCheatedGameLink => 'Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.'; @@ -3403,7 +3664,7 @@ class AppLocalizationsPt extends AppLocalizations { String get phoneAndTablet => 'Telemóvel e tablet'; @override - String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + String get bulletBlitzClassical => 'Bullet, rápida, clássica'; @override String get correspondenceChess => 'Xadrez por correspondência'; @@ -4077,6 +4338,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + @override + String get stats => 'Estatísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4724,3429 +4988,4374 @@ class AppLocalizationsPt extends AppLocalizations { String get streamerLichessStreamers => 'Streamers no Lichess'; @override - String get studyShareAndExport => 'Partilhar & exportar'; + String get studyPrivate => 'Privado'; @override - String get studyStart => 'Iniciar'; -} + String get studyMyStudies => 'Os meus estudos'; -/// The translations for Portuguese, as used in Brazil (`pt_BR`). -class AppLocalizationsPtBr extends AppLocalizationsPt { - AppLocalizationsPtBr(): super('pt_BR'); + @override + String get studyStudiesIContributeTo => 'Estudos para os quais contribui'; @override - String get mobileHomeTab => 'Início'; + String get studyMyPublicStudies => 'Os meus estudos públicos'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get studyMyPrivateStudies => 'Os meus estudos privados'; @override - String get mobileToolsTab => 'Ferramentas'; + String get studyMyFavoriteStudies => 'Os meus estudos favoritos'; @override - String get mobileWatchTab => 'Assistir'; + String get studyWhatAreStudies => 'O que são estudos?'; @override - String get mobileSettingsTab => 'Ajustes'; + String get studyAllStudies => 'Todos os estudos'; @override - String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; + String studyStudiesCreatedByX(String param) { + return 'Estudos criados por $param'; + } @override - String get mobileSystemColors => 'Cores do sistema'; + String get studyNoneYet => 'Nenhum ainda.'; @override - String get mobileFeedbackButton => 'Comentários'; + String get studyHot => 'Destaques'; @override - String get mobileOkButton => 'Ok'; + String get studyDateAddedNewest => 'Data em que foi adicionado (mais recente)'; @override - String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; + String get studyDateAddedOldest => 'Data em que foi adicionado (mais antigo)'; @override - String get mobileSettingsImmersiveMode => 'Modo imerssivo'; + String get studyRecentlyUpdated => 'Atualizado recentemente'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se as telas dos jogos e desafios.'; + String get studyMostPopular => 'Mais popular'; @override - String get mobileNotFollowingAnyUser => 'Você não estar seguindo nenhum usuário.'; + String get studyAlphabetical => 'Ordem alfabética'; @override - String get mobileAllGames => 'Todos os jogos'; + String get studyAddNewChapter => 'Adicionar um novo capítulo'; @override - String get mobileRecentSearches => 'Pesquisas recentes'; + String get studyAddMembers => 'Adicionar membros'; @override - String get mobileClearButton => 'Limpar'; + String get studyInviteToTheStudy => 'Convidar para o estudo'; @override - String mobilePlayersMatchingSearchTerm(String param) { - return 'Usuários com \"$param\"'; - } + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convida apenas pessoas que conheças e que querem participar ativamente neste estudo.'; @override - String get mobileNoSearchResults => 'Sem Resultados'; + String get studySearchByUsername => 'Pesquisar por nome de utilizador'; @override - String get mobileAreYouSure => 'Você tem certeza?'; + String get studySpectator => 'Espectador'; @override - String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; + String get studyContributor => 'Colaborador'; @override - String get mobilePuzzleStormNothingToShow => 'Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.'; + String get studyKick => 'Expulsar'; @override - String get mobileSharePuzzle => 'Tentar novamente este quebra-cabeça'; + String get studyLeaveTheStudy => 'Sair do estudo'; @override - String get mobileShareGameURL => 'Compartilhar URL do jogo'; + String get studyYouAreNowAContributor => 'Agora és um colaborador'; @override - String get mobileShareGamePGN => 'Compartilhar PGN'; + String get studyYouAreNowASpectator => 'Agora és um espectador'; @override - String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; + String get studyPgnTags => 'Etiquetas PGN'; @override - String get mobileShowVariations => 'Mostrar setas da variantes'; + String get studyLike => 'Gostar'; @override - String get mobileHideVariation => 'Ocultar variante forçada'; + String get studyUnlike => 'Remover gosto'; @override - String get mobileShowComments => 'Mostrar comentários'; + String get studyNewTag => 'Nova etiqueta'; @override - String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; + String get studyCommentThisPosition => 'Comentar esta posição'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; + String get studyCommentThisMove => 'Comentar este lance'; @override - String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; + String get studyAnnotateWithGlyphs => 'Anotar com símbolos'; @override - String get mobileCancelDrawOffer => 'Cancelar oferta de revanche'; + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é demasiado curto para ser analisado.'; @override - String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; + String get studyOnlyContributorsCanRequestAnalysis => 'Apenas os colaboradores de estudo podem solicitar uma análise de computador.'; @override - String get mobileBlindfoldMode => 'Venda'; + String get studyGetAFullComputerAnalysis => 'Obtém uma análise completa da linha principal pelo servidor.'; @override - String get mobileLiveStreamers => 'Streamers do Lichess'; + String get studyMakeSureTheChapterIsComplete => 'Certifica-te que o capítulo está completo. Só podes solicitar a análise uma vez.'; @override - String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros do SYNC permanecem na mesma posição'; @override - String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; + String get studyShareChanges => 'Partilha as alterações com espectadores e guarda-as no servidor'; @override - String get mobileSomethingWentWrong => 'Houve algum problema.'; + String get studyPlaying => 'A ser jogado'; @override - String get mobileShowResult => 'Mostrar resultado'; + String get studyShowEvalBar => 'Barras de avaliação'; @override - String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; + String get studyFirst => 'Primeira'; @override - String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; + String get studyPrevious => 'Anterior'; @override - String mobileGreeting(String param) { - return 'Olá, $param'; - } + String get studyNext => 'Seguinte'; @override - String get mobileGreetingWithoutName => 'Olá'; + String get studyLast => 'Última'; @override - String get activityActivity => 'Atividade'; + String get studyShareAndExport => 'Partilhar & exportar'; @override - String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; + String get studyCloneStudy => 'Clonar'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return 'Classificado #$param1 entre $param2'; - } + String get studyStudyPgn => 'PGN do estudo'; @override - String get activitySignedUp => 'Registrou-se no lichess'; + String get studyDownloadAllGames => 'Transferir todas as partidas'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Contribuiu para o lichess.org por $count meses como $param2', - one: 'Contribuiu para o lichess.org por $count mês como $param2', - ); - return '$_temp0'; - } + String get studyChapterPgn => 'PGN do capítulo'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Praticou $count posições em $param2', - one: 'Praticou $count posição em $param2', - ); - return '$_temp0'; - } + String get studyCopyChapterPgn => 'Copiar PGN'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Resolveu $count quebra-cabeças táticos', - one: 'Resolveu $count quebra-cabeça tático', - ); - return '$_temp0'; - } + String get studyDownloadGame => 'Transferir partida'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogou $count partidas de $param2', - one: 'Jogou $count partida de $param2', - ); - return '$_temp0'; - } + String get studyStudyUrl => 'URL do estudo'; @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Publicou $count mensagens em $param2', - one: 'Publicou $count mensagem em $param2', - ); - return '$_temp0'; - } + String get studyCurrentChapterUrl => 'URL do capítulo atual'; @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogou $count movimentos', - one: 'Jogou $count movimento', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'Podes colocar isto no fórum para o incorporares'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'em $count jogos por correspondência', - one: 'em $count jogo por correspondência', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Começar na posição inicial'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Completou $count jogos por correspondência', - one: 'Completou $count jogo por correspondência', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Começar em $param'; } @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Começou a seguir $count jogadores', - one: 'Começou a seguir $count jogador', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Incorporar no teu site ou blog'; @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Ganhou $count novos seguidores', - one: 'Ganhou $count novo seguidor', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Ler mais sobre incorporação'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Hospedou $count exibições simultâneas', - one: 'Hospedou $count exibição simultânea', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Só estudos públicos é que podem ser incorporados!'; @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Participou em $count exibições simultâneas', - one: 'Participou em $count exibição simultânea', - ); - return '$_temp0'; - } + String get studyOpen => 'Abrir'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Criou $count novos estudos', - one: 'Criou $count novo estudo', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, trazido a si pelo $param2'; } @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competiu em $count torneios arena', - one: 'Competiu em $count torneio arena', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Estudo não encontrado'; @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', - one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', - ); - return '$_temp0'; - } + String get studyEditChapter => 'Editar capítulo'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competiu em $count torneios suíços', - one: 'Competiu em $count torneio suíço', - ); - return '$_temp0'; - } + String get studyNewChapter => 'Novo capítulo'; @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Entrou nas $count equipes', - one: 'Entrou na $count equipe', - ); - return '$_temp0'; + String studyImportFromChapterX(String param) { + return 'Importar de $param'; } @override - String get broadcastBroadcasts => 'Transmissões'; + String get studyOrientation => 'Orientação'; @override - String get broadcastLiveBroadcasts => 'Transmissões ao vivo do torneio'; + String get studyAnalysisMode => 'Modo de análise'; @override - String challengeChallengesX(String param1) { - return 'Desafios: $param1'; - } + String get studyPinnedChapterComment => 'Comentário de capítulo afixado'; @override - String get challengeChallengeToPlay => 'Desafiar para jogar'; + String get studySaveChapter => 'Guardar capítulo'; @override - String get challengeChallengeDeclined => 'Desafio recusado'; + String get studyClearAnnotations => 'Limpar anotações'; @override - String get challengeChallengeAccepted => 'Desafio aceito!'; + String get studyClearVariations => 'Limpar variações'; @override - String get challengeChallengeCanceled => 'Desafio cancelado.'; + String get studyDeleteChapter => 'Eliminar capítulo'; @override - String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; + String get studyDeleteThisChapter => 'Eliminar este capítulo? Não há volta atrás!'; @override - String challengeYouCannotChallengeX(String param) { - return 'Você não pode desafiar $param.'; - } + String get studyClearAllCommentsInThisChapter => 'Apagar todos os comentários e símbolos após lances neste capítulo?'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param não aceita desafios.'; - } + String get studyRightUnderTheBoard => 'Mesmo por baixo do tabuleiro'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'O seu rating $param1 é muito diferente de $param2.'; - } + String get studyNoPinnedComment => 'Nenhum'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Não pode desafiar devido ao rating provisório de $param.'; - } + String get studyNormalAnalysis => 'Análise normal'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param só aceita desafios de amigos.'; - } + String get studyHideNextMoves => 'Ocultar os próximos movimentos'; @override - String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; + String get studyInteractiveLesson => 'Lição interativa'; @override - String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; + String studyChapterX(String param) { + return 'Capítulo $param'; + } @override - String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; + String get studyEmpty => 'Vazio'; @override - String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; + String get studyStartFromInitialPosition => 'Começar da posição inicial'; @override - String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; + String get studyEditor => 'Editor'; @override - String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; + String get studyStartFromCustomPosition => 'Iniciar de uma posição personalizada'; @override - String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; + String get studyLoadAGameByUrl => 'Carregar um jogo por URL'; @override - String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; + String get studyLoadAPositionFromFen => 'Carregar uma posição por FEN'; @override - String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; + String get studyLoadAGameFromPgn => 'Carregar um jogo por PGN'; @override - String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; + String get studyAutomatic => 'Automática'; @override - String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; + String get studyUrlOfTheGame => 'URL do jogo'; @override - String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar um jogo do $param1 ou de $param2'; + } @override - String get contactContact => 'Contato'; + String get studyCreateChapter => 'Criar capítulo'; @override - String get contactContactLichess => 'Entrar em contato com Lichess'; + String get studyCreateStudy => 'Criar estudo'; @override - String get patronDonate => 'Doação'; + String get studyEditStudy => 'Editar estudo'; @override - String get patronLichessPatron => 'Apoie o Lichess'; + String get studyVisibility => 'Visibilidade'; @override - String perfStatPerfStats(String param) { - return 'Estatísticas de $param'; - } + String get studyPublic => 'Público'; @override - String get perfStatViewTheGames => 'Ver os jogos'; + String get studyUnlisted => 'Não listado'; @override - String get perfStatProvisional => 'provisório'; + String get studyInviteOnly => 'Apenas por convite'; @override - String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; + String get studyAllowCloning => 'Permitir clonagem'; @override - String perfStatProgressOverLastXGames(String param) { - return 'Progresso nos últimos $param jogos:'; - } + String get studyNobody => 'Ninguém'; @override - String perfStatRatingDeviation(String param) { - return 'Desvio de pontuação: $param.'; - } + String get studyOnlyMe => 'Apenas eu'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; - } + String get studyContributors => 'Contribuidores'; @override - String get perfStatTotalGames => 'Total de partidas'; + String get studyMembers => 'Membros'; @override - String get perfStatRatedGames => 'Partidas valendo pontos'; + String get studyEveryone => 'Toda a gente'; @override - String get perfStatTournamentGames => 'Jogos de torneio'; + String get studyEnableSync => 'Ativar sincronização'; @override - String get perfStatBerserkedGames => 'Partidas Berserked'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Sim: mantenha toda a gente na mesma posição'; @override - String get perfStatTimeSpentPlaying => 'Tempo jogando'; + String get studyNoLetPeopleBrowseFreely => 'Não: deixa as pessoas navegarem livremente'; @override - String get perfStatAverageOpponent => 'Pontuação média do adversário'; + String get studyPinnedStudyComment => 'Comentário de estudo fixo'; @override - String get perfStatVictories => 'Vitórias'; + String get studyStart => 'Iniciar'; @override - String get perfStatDefeats => 'Derrotas'; + String get studySave => 'Guardar'; @override - String get perfStatDisconnections => 'Desconexões'; + String get studyClearChat => 'Limpar o chat'; @override - String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; + String get studyDeleteTheStudyChatHistory => 'Apagar o histórico do chat do estudo? Não há volta atrás!'; @override - String perfStatHighestRating(String param) { - return 'Pontuação mais alta: $param'; - } + String get studyDeleteStudy => 'Eliminar estudo'; @override - String perfStatLowestRating(String param) { - return 'Rating mais baixo: $param'; + String studyConfirmDeleteStudy(String param) { + return 'Eliminar todo o estudo? Não há volta atrás! Digite o nome do estudo para confirmar: $param'; } @override - String perfStatFromXToY(String param1, String param2) { - return 'de $param1 para $param2'; - } + String get studyWhereDoYouWantToStudyThat => 'Onde queres estudar isso?'; @override - String get perfStatWinningStreak => 'Série de Vitórias'; + String get studyGoodMove => 'Boa jogada'; @override - String get perfStatLosingStreak => 'Série de derrotas'; + String get studyMistake => 'Erro'; @override - String perfStatLongestStreak(String param) { - return 'Sequência mais longa: $param'; - } + String get studyBrilliantMove => 'Jogada brilhante'; @override - String perfStatCurrentStreak(String param) { - return 'Sequência atual: $param'; - } + String get studyBlunder => 'Erro grave'; @override - String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; + String get studyInterestingMove => 'Lance interessante'; @override - String get perfStatGamesInARow => 'Partidas jogadas seguidas'; + String get studyDubiousMove => 'Lance duvidoso'; @override - String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; + String get studyOnlyMove => 'Lance único'; @override - String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; + String get studyZugzwang => 'Zugzwang'; @override - String get perfStatNow => 'agora'; + String get studyEqualPosition => 'Posição igual'; @override - String get preferencesPreferences => 'Preferências'; + String get studyUnclearPosition => 'Posição não clara'; @override - String get preferencesDisplay => 'Exibição'; + String get studyWhiteIsSlightlyBetter => 'As brancas estão ligeiramente melhor'; @override - String get preferencesPrivacy => 'Privacidade'; + String get studyBlackIsSlightlyBetter => 'As pretas estão ligeiramente melhor'; @override - String get preferencesNotifications => 'Notificações'; + String get studyWhiteIsBetter => 'As brancas estão melhor'; @override - String get preferencesPieceAnimation => 'Animação das peças'; + String get studyBlackIsBetter => 'As pretas estão melhor'; @override - String get preferencesMaterialDifference => 'Diferença material'; + String get studyWhiteIsWinning => 'Brancas estão ganhando'; @override - String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; + String get studyBlackIsWinning => 'Pretas estão ganhando'; @override - String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; + String get studyNovelty => 'Novidade teórica'; @override - String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; + String get studyDevelopment => 'Desenvolvimento'; @override - String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; + String get studyInitiative => 'Iniciativa'; @override - String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; + String get studyAttack => 'Ataque'; @override - String get preferencesChessPieceSymbol => 'Símbolo da peça'; + String get studyCounterplay => 'Contra-jogo'; @override - String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; + String get studyTimeTrouble => 'Pouco tempo'; @override - String get preferencesZenMode => 'Modo Zen'; + String get studyWithCompensation => 'Com compensação'; @override - String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; + String get studyWithTheIdea => 'Com a ideia'; @override - String get preferencesShowFlairs => 'Mostrar emotes de usuário'; + String get studyNextChapter => 'Próximo capítulo'; @override - String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; + String get studyPrevChapter => 'Capítulo anterior'; @override - String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; + String get studyStudyActions => 'Opções de estudo'; @override - String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; + String get studyTopics => 'Tópicos'; @override - String get preferencesInGameOnly => 'Durante partidas'; + String get studyMyTopics => 'Os meus tópicos'; @override - String get preferencesChessClock => 'Relógio'; + String get studyPopularTopics => 'Tópicos populares'; @override - String get preferencesTenthsOfSeconds => 'Décimos de segundo'; + String get studyManageTopics => 'Gerir tópicos'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; + String get studyBack => 'Voltar'; @override - String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; + String get studyPlayAgain => 'Jogar novamente'; @override - String get preferencesSoundWhenTimeGetsCritical => 'Som ao atingir tempo crítico'; + String get studyWhatWouldYouPlay => 'O que jogaria nessa situação?'; @override - String get preferencesGiveMoreTime => 'Dar mais tempo'; + String get studyYouCompletedThisLesson => 'Parabéns! Completou esta lição.'; @override - String get preferencesGameBehavior => 'Comportamento do jogo'; + String studyPerPage(String param) { + return '$param por página'; + } @override - String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capítulos', + one: '$count capítulo', + ); + return '$_temp0'; + } @override - String get preferencesClickTwoSquares => 'Clicar em duas casas'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Jogos', + one: '$count Jogo', + ); + return '$_temp0'; + } @override - String get preferencesDragPiece => 'Arrastar a peça'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membros', + one: '$count membro', + ); + return '$_temp0'; + } @override - String get preferencesBothClicksAndDrag => 'Ambas'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Cole seu texto PGN aqui, até $count jogos', + one: 'Cole seu texto PGN aqui, até $count jogo', + ); + return '$_temp0'; + } @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; + String get timeagoJustNow => 'agora mesmo'; @override - String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; + String get timeagoRightNow => 'agora mesmo'; @override - String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; + String get timeagoCompleted => 'concluído'; @override - String get preferencesPromoteToQueenAutomatically => 'Promover a Dama automaticamente'; + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count segundos', + one: 'em $count segundos', + ); + return '$_temp0'; + } @override - String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count minutos', + one: 'dentro de $count minutos', + ); + return '$_temp0'; + } @override - String get preferencesWhenPremoving => 'Quando pré-mover'; + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count horas', + one: 'em $count hora', + ); + return '$_temp0'; + } @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count dias', + one: 'em $count dia', + ); + return '$_temp0'; + } @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count semanas', + one: 'em $count semana', + ); + return '$_temp0'; + } @override - String get preferencesMoveConfirmation => 'Confirmação de movimento'; + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count meses', + one: 'em $count mês', + ); + return '$_temp0'; + } @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count anos', + one: 'em $count ano', + ); + return '$_temp0'; + } @override - String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count minutos', + one: 'há $count minuto', + ); + return '$_temp0'; + } @override - String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count horas', + one: 'há $count hora', + ); + return '$_temp0'; + } @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count dias', + one: 'há $count dia', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count semanas', + one: 'há $count semana', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count meses', + one: 'há $count mês', + ); + return '$_temp0'; + } @override - String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count anos', + one: 'há $count ano', + ); + return '$_temp0'; + } @override - String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minuto restante', + ); + return '$_temp0'; + } @override - String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count hora restante', + ); + return '$_temp0'; + } +} - @override - String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; +/// The translations for Portuguese, as used in Brazil (`pt_BR`). +class AppLocalizationsPtBr extends AppLocalizationsPt { + AppLocalizationsPtBr(): super('pt_BR'); @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; + String get mobileAllGames => 'Todos os jogos'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; + String get mobileAreYouSure => 'Você tem certeza?'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; + String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; @override - String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; + String get mobileClearButton => 'Limpar'; @override - String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; + String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; @override - String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; + String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; @override - String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; + String get mobileFeedbackButton => 'Comentários'; @override - String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; + String mobileGreeting(String param) { + return 'Olá, $param'; + } @override - String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get preferencesNotifyChallenge => 'Desafios'; + String get mobileHideVariation => 'Ocultar variante forçada'; @override - String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; + String get mobileHomeTab => 'Início'; @override - String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; + String get mobileLiveStreamers => 'Streamers do Lichess'; @override - String get preferencesNotifyBell => 'Notificação no Lichess'; + String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; @override - String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; + String get mobileNoSearchResults => 'Sem Resultados'; @override - String get preferencesNotifyWeb => 'Navegador'; + String get mobileNotFollowingAnyUser => 'Você não está seguindo nenhum usuário.'; @override - String get preferencesNotifyDevice => 'Dispositivo'; + String get mobileOkButton => 'Ok'; @override - String get preferencesBellNotificationSound => 'Som da notificação'; + String mobilePlayersMatchingSearchTerm(String param) { + return 'Usuários com \"$param\"'; + } @override - String get puzzlePuzzles => 'Quebra-cabeças'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça segurada'; @override - String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; + String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; @override - String get puzzleRecommended => 'Recomendado'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; @override - String get puzzlePhases => 'Fases'; + String get mobilePuzzleStormNothingToShow => 'Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.'; @override - String get puzzleMotifs => 'Motivos táticos'; + String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; @override - String get puzzleAdvanced => 'Avançado'; + String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; @override - String get puzzleLengths => 'Distância'; + String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; @override - String get puzzleMates => 'Xeque-mates'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get puzzleGoals => 'Objetivos'; + String get mobileRecentSearches => 'Pesquisas recentes'; @override - String get puzzleOrigin => 'Origem'; + String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; @override - String get puzzleSpecialMoves => 'Movimentos especiais'; + String get mobileSettingsImmersiveMode => 'Modo imersivo'; @override - String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.'; @override - String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; + String get mobileSettingsTab => 'Ajustes'; @override - String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; + String get mobileShareGamePGN => 'Compartilhar PGN'; @override - String get puzzleDownVote => 'Votar contra o quebra-cabeça'; + String get mobileShareGameURL => 'Compartilhar URL do jogo'; @override - String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; + String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; @override - String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; + String get mobileSharePuzzle => 'Compartilhar este quebra-cabeça'; @override - String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; + String get mobileShowComments => 'Mostrar comentários'; @override - String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; + String get mobileShowResult => 'Mostrar resultado'; @override - String puzzlePuzzleId(String param) { - return 'Quebra-cabeça $param'; - } + String get mobileShowVariations => 'Mostrar setas de variantes'; @override - String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; + String get mobileSomethingWentWrong => 'Houve algum problema.'; @override - String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get puzzleClickToSolve => 'Clique para resolver'; + String get mobileTheme => 'Tema'; @override - String get puzzleGoodMove => 'Boa jogada'; + String get mobileToolsTab => 'Ferramentas'; @override - String get puzzleBestMove => 'Melhor jogada!'; + String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; @override - String get puzzleKeepGoing => 'Continue…'; + String get mobileWatchTab => 'Assistir'; @override - String get puzzlePuzzleSuccess => 'Sucesso!'; + String get activityActivity => 'Atividade'; @override - String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; + String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; @override - String get puzzleByOpenings => 'Por abertura'; + String activityRankedInSwissTournament(String param1, String param2) { + return 'Classificado #$param1 entre $param2'; + } @override - String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; + String get activitySignedUp => 'Registrou-se no lichess'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Contribuiu para o lichess.org por $count meses como $param2', + one: 'Contribuiu para o lichess.org por $count mês como $param2', + ); + return '$_temp0'; + } @override - String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Praticou $count posições em $param2', + one: 'Praticou $count posição em $param2', + ); + return '$_temp0'; + } @override - String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Resolveu $count quebra-cabeças táticos', + one: 'Resolveu $count quebra-cabeça tático', + ); + return '$_temp0'; + } @override - String get puzzleNotTheMove => 'O movimento não é este!'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count partidas de $param2', + one: 'Jogou $count partida de $param2', + ); + return '$_temp0'; + } @override - String get puzzleTrySomethingElse => 'Tente algo diferente.'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Publicou $count mensagens em $param2', + one: 'Publicou $count mensagem em $param2', + ); + return '$_temp0'; + } @override - String puzzleRatingX(String param) { - return 'Rating: $param'; + String activityPlayedNbMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count movimentos', + one: 'Jogou $count movimento', + ); + return '$_temp0'; } @override - String get puzzleHidden => 'oculto'; + String activityInNbCorrespondenceGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count jogos por correspondência', + one: 'em $count jogo por correspondência', + ); + return '$_temp0'; + } @override - String puzzleFromGameLink(String param) { - return 'Do jogo $param'; + String activityCompletedNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completou $count jogos por correspondência', + one: 'Completou $count jogo por correspondência', + ); + return '$_temp0'; } @override - String get puzzleContinueTraining => 'Continue treinando'; + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 partidas por correspondência finalizadas', + one: 'Completou $count $param2 partida por correspondência', + ); + return '$_temp0'; + } @override - String get puzzleDifficultyLevel => 'Nível de dificuldade'; + String activityFollowedNbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Começou a seguir $count jogadores', + one: 'Começou a seguir $count jogador', + ); + return '$_temp0'; + } @override - String get puzzleNormal => 'Normal'; + String activityGainedNbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ganhou $count novos seguidores', + one: 'Ganhou $count novo seguidor', + ); + return '$_temp0'; + } @override - String get puzzleEasier => 'Fácil'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hospedou $count exibições simultâneas', + one: 'Hospedou $count exibição simultânea', + ); + return '$_temp0'; + } @override - String get puzzleEasiest => 'Muito fácil'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Participou em $count exibições simultâneas', + one: 'Participou em $count exibição simultânea', + ); + return '$_temp0'; + } @override - String get puzzleHarder => 'Difícil'; + String activityCreatedNbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Criou $count novos estudos', + one: 'Criou $count novo estudo', + ); + return '$_temp0'; + } @override - String get puzzleHardest => 'Muito difícil'; + String activityCompetedInNbTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competiu em $count torneios arena', + one: 'Competiu em $count torneio arena', + ); + return '$_temp0'; + } @override - String get puzzleExample => 'Exemplo'; + String activityRankedInTournament(int count, String param2, String param3, String param4) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', + one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', + ); + return '$_temp0'; + } @override - String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; + String activityCompetedInNbSwissTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competiu em $count torneios suíços', + one: 'Competiu em $count torneio suíço', + ); + return '$_temp0'; + } @override - String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; + String activityJoinedNbTeams(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Entrou nas $count equipes', + one: 'Entrou na $count equipe', + ); + return '$_temp0'; + } @override - String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; + String get broadcastBroadcasts => 'Transmissões'; @override - String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; + String get broadcastMyBroadcasts => 'Minhas transmissões'; @override - String get puzzleImprovementAreas => 'Áreas de aprimoramento'; + String get broadcastLiveBroadcasts => 'Transmissões ao vivo do torneio'; @override - String get puzzleStrengths => 'Pontos fortes'; + String get broadcastBroadcastCalendar => 'Calendário das transmissões'; @override - String get puzzleHistory => 'Histórico de quebra-cabeças'; + String get broadcastNewBroadcast => 'Nova transmissão ao vivo'; @override - String get puzzleSolved => 'resolvido'; + String get broadcastSubscribedBroadcasts => 'Transmissões em que você se inscreveu'; @override - String get puzzleFailed => 'falhou'; + String get broadcastAboutBroadcasts => 'Sobre as transmissões'; @override - String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; + String get broadcastHowToUseLichessBroadcasts => 'Como usar as transmissões do Lichess.'; @override - String puzzleYourStreakX(String param) { - return 'Sua sequência: $param'; - } + String get broadcastTheNewRoundHelp => 'A nova rodada terá os mesmos membros e colaboradores que a anterior.'; @override - String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; + String get broadcastAddRound => 'Adicionar uma rodada'; @override - String get puzzleContinueTheStreak => 'Continuar a sequência'; + String get broadcastOngoing => 'Em andamento'; @override - String get puzzleNewStreak => 'Nova sequência'; + String get broadcastUpcoming => 'Próximos'; @override - String get puzzleFromMyGames => 'Dos meus jogos'; + String get broadcastCompleted => 'Concluído'; @override - String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; + String get broadcastCompletedHelp => 'O Lichess detecta o fim da rodada baseado nos jogos fonte. Use essa opção se não houver fonte.'; @override - String puzzleFromXGames(String param) { - return 'Problemas de $param\' jogos'; - } + String get broadcastRoundName => 'Nome da rodada'; @override - String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; + String get broadcastRoundNumber => 'Número da rodada'; @override - String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; + String get broadcastTournamentName => 'Nome do torneio'; @override - String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 quebra-cabeças encontrados em $param2 partidas'; - } + String get broadcastTournamentDescription => 'Descrição curta do torneio'; @override - String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; + String get broadcastFullDescription => 'Descrição completa do evento'; @override - String puzzlePercentSolved(String param) { - return '$param resolvido'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrição longa e opcional da transmissão. $param1 está disponível. O tamanho deve ser menor que $param2 caracteres.'; } @override - String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; + String get broadcastSourceSingleUrl => 'URL de origem de PGN'; @override - String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; + String get broadcastSourceUrlHelp => 'URL que Lichess irá verificar para obter atualizações PGN. Deve ser acessível ao público a partir da Internet.'; @override - String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; + String get broadcastSourceGameIds => 'Até 64 IDs de partidas do Lichess, separados por espaços.'; @override - String puzzlePlayedXTimes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogado $count vezes', - one: 'Jogado $count vezes', - ); - return '$_temp0'; + String broadcastStartDateTimeZone(String param) { + return 'Data de início no horário local do torneio: $param'; } @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos abaixo da sua classificação de quebra-cabeças', - one: 'Um ponto abaixo da sua classificação de quebra-cabeças', - ); - return '$_temp0'; - } + String get broadcastStartDateHelp => 'Opcional, se você sabe quando o evento começa'; @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos acima da sua classificação de quebra-cabeças', - one: 'Um ponto acima da sua classificação de quebra-cabeças', - ); - return '$_temp0'; - } + String get broadcastCurrentGameUrl => 'URL da partida atual'; @override - String puzzleNbPlayed(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogados', - one: '$count jogado', - ); - return '$_temp0'; - } + String get broadcastDownloadAllRounds => 'Baixar todas as rodadas'; @override - String puzzleNbToReplay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count a serem repetidos', - one: '$count a ser repetido', - ); - return '$_temp0'; - } + String get broadcastResetRound => 'Reiniciar esta rodada'; @override - String get puzzleThemeAdvancedPawn => 'Peão avançado'; + String get broadcastDeleteRound => 'Excluir esta rodada'; @override - String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; + String get broadcastDefinitivelyDeleteRound => 'Deletar permanentemente todas as partidas desta rodada.'; @override - String get puzzleThemeAdvantage => 'Vantagem'; + String get broadcastDeleteAllGamesOfThisRound => 'Deletar todas as partidas desta rodada. A fonte deverá estar ativa para criá-las novamente.'; @override - String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; + String get broadcastEditRoundStudy => 'Editar estudo da rodada'; @override - String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; + String get broadcastDeleteTournament => 'Excluir este torneio'; @override - String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; + String get broadcastDefinitivelyDeleteTournament => 'Excluir permanentemente todo o torneio, incluindo todas as rodadas e jogos.'; @override - String get puzzleThemeArabianMate => 'Mate árabe'; + String get broadcastShowScores => 'Mostrar pontuações dos jogadores com base nos resultados das partidas'; @override - String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; + String get broadcastReplacePlayerTags => 'Opcional: substituir nomes de jogador, ratings e títulos'; @override - String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; + String get broadcastFideFederations => 'Federações FIDE'; @override - String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; + String get broadcastTop10Rating => 'Classificação top 10'; @override - String get puzzleThemeAttraction => 'Atração'; + String get broadcastFidePlayers => 'Jogadores FIDE'; @override - String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; + String get broadcastFidePlayerNotFound => 'Jogador não encontrando na FIDE'; @override - String get puzzleThemeBackRankMate => 'Mate do corredor'; + String get broadcastFideProfile => 'Perfil FIDE'; @override - String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; + String get broadcastFederation => 'Federação'; @override - String get puzzleThemeBishopEndgame => 'Finais de bispo'; + String get broadcastAgeThisYear => 'Idade atual'; @override - String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; + String get broadcastUnrated => 'Sem rating'; @override - String get puzzleThemeBodenMate => 'Mate de Boden'; + String get broadcastRecentTournaments => 'Torneios recentes'; @override - String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; + String get broadcastOpenLichess => 'Abrir no Lichess'; @override - String get puzzleThemeCastling => 'Roque'; + String get broadcastTeams => 'Equipes'; @override - String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; + String get broadcastBoards => 'Tabuleiros'; @override - String get puzzleThemeCapturingDefender => 'Capture o defensor'; + String get broadcastOverview => 'Visão geral'; @override - String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; + String get broadcastSubscribeTitle => 'Inscreva-se para ser notificado no início de cada rodada. Você pode configurar as notificações de transmissões nas suas preferências.'; @override - String get puzzleThemeCrushing => 'Punindo'; + String get broadcastUploadImage => 'Enviar imagem de torneio'; @override - String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; + String get broadcastNoBoardsYet => 'Sem tabuleiros ainda. Eles vão aparecer quando os jogos forem enviados.'; @override - String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; + String broadcastBoardsCanBeLoaded(String param) { + return 'Tabuleiros são carregados com uma fonte ou pelo $param'; + } @override - String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; + String broadcastStartsAfter(String param) { + return 'Começa após $param'; + } @override - String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; + String get broadcastStartVerySoon => 'A transmissão começará em breve.'; @override - String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; + String get broadcastNotYetStarted => 'A transmissão ainda não começou.'; @override - String get puzzleThemeEquality => 'Igualdade'; + String get broadcastOfficialWebsite => 'Site oficial'; @override - String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; + String get broadcastStandings => 'Classificação'; @override - String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; + String get broadcastOfficialStandings => 'Classificação oficial'; @override - String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; + String broadcastIframeHelp(String param) { + return 'Mais opções na $param'; + } @override - String get puzzleThemeClearance => 'Lance útil'; + String get broadcastWebmastersPage => 'página dos webmasters'; @override - String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; + String broadcastPgnSourceHelp(String param) { + return 'Uma fonte PGN pública ao vivo desta rodada. Há também a $param para uma sincronização mais rápida e eficiente.'; + } @override - String get puzzleThemeDefensiveMove => 'Movimento defensivo'; + String get broadcastEmbedThisBroadcast => 'Incorporar essa transmissão em seu site'; @override - String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; + String broadcastEmbedThisRound(String param) { + return 'Incorporar $param em seu site'; + } @override - String get puzzleThemeDeflection => 'Desvio'; + String get broadcastRatingDiff => 'Diferência de pontos'; @override - String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; + String get broadcastGamesThisTournament => 'Jogos neste torneio'; @override - String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; + String get broadcastScore => 'Pontuação'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; + String get broadcastAllTeams => 'Todas as equipes'; @override - String get puzzleThemeDoubleCheck => 'Xeque duplo'; + String get broadcastTournamentFormat => 'Formato do torneio'; @override - String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; + String get broadcastTournamentLocation => 'Local do torneio'; @override - String get puzzleThemeEndgame => 'Finais'; + String get broadcastTopPlayers => 'Melhores jogadores'; @override - String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; + String get broadcastTimezone => 'Fuso horário'; @override - String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; + String get broadcastFideRatingCategory => 'Categoria de rating FIDE'; @override - String get puzzleThemeExposedKing => 'Rei exposto'; + String get broadcastOptionalDetails => 'Detalhes opcionais'; @override - String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; + String get broadcastPastBroadcasts => 'Transmissões passadas'; @override - String get puzzleThemeFork => 'Garfo (ou duplo)'; + String get broadcastAllBroadcastsByMonth => 'Ver todas as transmissões por mês'; @override - String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmissões', + one: '$count transmissão', + ); + return '$_temp0'; + } @override - String get puzzleThemeHangingPiece => 'Peça pendurada'; + String challengeChallengesX(String param1) { + return 'Desafios: $param1'; + } @override - String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; + String get challengeChallengeToPlay => 'Desafiar para jogar'; @override - String get puzzleThemeHookMate => 'Xeque gancho'; + String get challengeChallengeDeclined => 'Desafio recusado'; @override - String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; + String get challengeChallengeAccepted => 'Desafio aceito!'; @override - String get puzzleThemeInterference => 'Interferência'; + String get challengeChallengeCanceled => 'Desafio cancelado.'; @override - String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; + String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; @override - String get puzzleThemeIntermezzo => 'Lance intermediário'; + String challengeYouCannotChallengeX(String param) { + return 'Você não pode desafiar $param.'; + } @override - String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param não aceita desafios.'; + } @override - String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return 'O seu rating $param1 é muito diferente de $param2.'; + } @override - String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return 'Não pode desafiar devido ao rating provisório de $param.'; + } @override - String get puzzleThemeLong => 'Quebra-cabeças longo'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param só aceita desafios de amigos.'; + } @override - String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; + String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; @override - String get puzzleThemeMaster => 'Partidas de mestres'; + String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; @override - String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; + String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; @override - String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; + String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; @override - String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; + String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; @override - String get puzzleThemeMate => 'Xeque-mate'; + String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; @override - String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; + String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; @override - String get puzzleThemeMateIn1 => 'Mate em 1'; + String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; @override - String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; + String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; @override - String get puzzleThemeMateIn2 => 'Mate em 2'; + String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; @override - String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; + String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; @override - String get puzzleThemeMateIn3 => 'Mate em 3'; + String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; @override - String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; + String get contactContact => 'Contato'; @override - String get puzzleThemeMateIn4 => 'Mate em 4'; + String get contactContactLichess => 'Entrar em contato com Lichess'; @override - String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; + String get patronDonate => 'Doação'; @override - String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; + String get patronLichessPatron => 'Apoie o Lichess'; @override - String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; + String perfStatPerfStats(String param) { + return 'Estatísticas de $param'; + } @override - String get puzzleThemeMiddlegame => 'Meio-jogo'; + String get perfStatViewTheGames => 'Ver os jogos'; @override - String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; + String get perfStatProvisional => 'provisório'; @override - String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; + String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; @override - String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; + String perfStatProgressOverLastXGames(String param) { + return 'Progresso nos últimos $param jogos:'; + } @override - String get puzzleThemeOpening => 'Abertura'; + String perfStatRatingDeviation(String param) { + return 'Desvio de pontuação: $param.'; + } @override - String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; + } @override - String get puzzleThemePawnEndgame => 'Finais de peões'; + String get perfStatTotalGames => 'Total de partidas'; @override - String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; + String get perfStatRatedGames => 'Partidas valendo pontos'; @override - String get puzzleThemePin => 'Cravada'; + String get perfStatTournamentGames => 'Jogos de torneio'; @override - String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; + String get perfStatBerserkedGames => 'Partidas Berserked'; @override - String get puzzleThemePromotion => 'Promoção'; + String get perfStatTimeSpentPlaying => 'Tempo jogando'; @override - String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; + String get perfStatAverageOpponent => 'Pontuação média do adversário'; @override - String get puzzleThemeQueenEndgame => 'Finais de Dama'; + String get perfStatVictories => 'Vitórias'; @override - String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; + String get perfStatDefeats => 'Derrotas'; @override - String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; + String get perfStatDisconnections => 'Desconexões'; @override - String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; + String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; @override - String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; + String perfStatHighestRating(String param) { + return 'Pontuação mais alta: $param'; + } @override - String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; + String perfStatLowestRating(String param) { + return 'Rating mais baixo: $param'; + } @override - String get puzzleThemeQuietMove => 'Lance de preparação'; + String perfStatFromXToY(String param1, String param2) { + return 'de $param1 para $param2'; + } @override - String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; + String get perfStatWinningStreak => 'Série de Vitórias'; @override - String get puzzleThemeRookEndgame => 'Finais de Torres'; + String get perfStatLosingStreak => 'Série de derrotas'; @override - String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; + String perfStatLongestStreak(String param) { + return 'Sequência mais longa: $param'; + } @override - String get puzzleThemeSacrifice => 'Sacrifício'; + String perfStatCurrentStreak(String param) { + return 'Sequência atual: $param'; + } @override - String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; + String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; @override - String get puzzleThemeShort => 'Quebra-cabeças curto'; + String get perfStatGamesInARow => 'Partidas jogadas seguidas'; @override - String get puzzleThemeShortDescription => 'Vitória em dois lances.'; + String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; @override - String get puzzleThemeSkewer => 'Raio X'; + String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; @override - String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; + String get perfStatNow => 'agora'; @override - String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; + String get preferencesPreferences => 'Preferências'; @override - String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; + String get preferencesDisplay => 'Exibição'; @override - String get puzzleThemeSuperGM => 'Super partidas de GMs'; + String get preferencesPrivacy => 'Privacidade'; @override - String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; + String get preferencesNotifications => 'Notificações'; @override - String get puzzleThemeTrappedPiece => 'Peça presa'; + String get preferencesPieceAnimation => 'Animação das peças'; @override - String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; + String get preferencesMaterialDifference => 'Diferença material'; @override - String get puzzleThemeUnderPromotion => 'Subpromoção'; + String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; @override - String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; + String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; @override - String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; + String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; @override - String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; + String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; @override - String get puzzleThemeXRayAttack => 'Ataque em raio X'; + String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; @override - String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; + String get preferencesChessPieceSymbol => 'Símbolo da peça'; @override - String get puzzleThemeZugzwang => 'Zugzwang'; + String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; @override - String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; + String get preferencesZenMode => 'Modo Zen'; @override - String get puzzleThemeHealthyMix => 'Combinação saudável'; + String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; + String get preferencesShowFlairs => 'Mostrar emotes de usuário'; @override - String get puzzleThemePlayerGames => 'Partidas de jogadores'; + String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; @override - String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; + String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; @override - String puzzleThemePuzzleDownloadInformation(String param) { - return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; - } + String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; @override - String get searchSearch => 'Buscar'; + String get preferencesInGameOnly => 'Durante partidas'; @override - String get settingsSettings => 'Configurações'; + String get preferencesChessClock => 'Relógio'; @override - String get settingsCloseAccount => 'Encerrar conta'; + String get preferencesTenthsOfSeconds => 'Décimos de segundo'; @override - String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; @override - String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; + String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; @override - String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; + String get preferencesSoundWhenTimeGetsCritical => 'Som ao atingir tempo crítico'; @override - String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; + String get preferencesGiveMoreTime => 'Dar mais tempo'; @override - String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; + String get preferencesGameBehavior => 'Comportamento do jogo'; @override - String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; + String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; @override - String get playWithAFriend => 'Jogar contra um amigo'; + String get preferencesClickTwoSquares => 'Clicar em duas casas'; @override - String get playWithTheMachine => 'Jogar contra o computador'; + String get preferencesDragPiece => 'Arrastar a peça'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; + String get preferencesBothClicksAndDrag => 'Ambas'; @override - String get gameOver => 'Fim da partida'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; @override - String get waitingForOpponent => 'Aguardando oponente'; + String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; @override - String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; + String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; @override - String get waiting => 'Aguardando'; + String get preferencesPromoteToQueenAutomatically => 'Promover a Dama automaticamente'; @override - String get yourTurn => 'Sua vez'; + String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1 nível $param2'; - } + String get preferencesWhenPremoving => 'Quando pré-mover'; @override - String get level => 'Nível'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; @override - String get strength => 'Nível'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; @override - String get toggleTheChat => 'Ativar/Desativar chat'; + String get preferencesMoveConfirmation => 'Confirmação de movimento'; @override - String get chat => 'Chat'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; @override - String get resign => 'Desistir'; + String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; @override - String get checkmate => 'Xeque-mate'; + String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; @override - String get stalemate => 'Rei afogado'; + String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; @override - String get white => 'Brancas'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; @override - String get black => 'Pretas'; + String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; @override - String get asWhite => 'de brancas'; + String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; @override - String get asBlack => 'de pretas'; + String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; @override - String get randomColor => 'Cor aleatória'; + String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; @override - String get createAGame => 'Criar uma partida'; + String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; @override - String get whiteIsVictorious => 'Brancas vencem'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; @override - String get blackIsVictorious => 'Pretas vencem'; + String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; @override - String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; @override - String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; + String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; @override - String get itsYourTurn => 'É a sua vez!'; + String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; @override - String get cheatDetected => 'Trapaça Detectada'; + String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; @override - String get kingInTheCenter => 'Rei no centro'; + String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; @override - String get threeChecks => 'Três xeques'; + String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; @override - String get raceFinished => 'Corrida terminada'; + String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; @override - String get variantEnding => 'Fim da variante'; + String get preferencesNotifyChallenge => 'Desafios'; @override - String get newOpponent => 'Novo oponente'; + String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; @override - String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; + String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; @override - String get joinTheGame => 'Entrar no jogo'; + String get preferencesNotifyBell => 'Notificação no Lichess'; @override - String get whitePlays => 'Brancas jogam'; + String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; @override - String get blackPlays => 'Pretas jogam'; + String get preferencesNotifyWeb => 'Navegador'; @override - String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; + String get preferencesNotifyDevice => 'Dispositivo'; @override - String get forceResignation => 'Reivindicar vitória'; + String get preferencesBellNotificationSound => 'Som da notificação'; @override - String get forceDraw => 'Reivindicar empate'; + String get preferencesBlindfold => 'Às cegas'; @override - String get talkInChat => 'Por favor, seja gentil no chat!'; + String get puzzlePuzzles => 'Quebra-cabeças'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; + String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; @override - String get whiteResigned => 'Brancas desistiram'; + String get puzzleRecommended => 'Recomendado'; @override - String get blackResigned => 'Pretas desistiram'; + String get puzzlePhases => 'Fases'; @override - String get whiteLeftTheGame => 'Brancas deixaram a partida'; + String get puzzleMotifs => 'Motivos táticos'; @override - String get blackLeftTheGame => 'Pretas deixaram a partida'; + String get puzzleAdvanced => 'Avançado'; @override - String get whiteDidntMove => 'As brancas não se moveram'; + String get puzzleLengths => 'Distância'; @override - String get blackDidntMove => 'As pretas não se moveram'; + String get puzzleMates => 'Xeque-mates'; @override - String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; + String get puzzleGoals => 'Objetivos'; @override - String get computerAnalysis => 'Análise do computador'; + String get puzzleOrigin => 'Origem'; @override - String get computerAnalysisAvailable => 'Análise de computador disponível'; + String get puzzleSpecialMoves => 'Movimentos especiais'; @override - String get computerAnalysisDisabled => 'Análise de computador desativada'; + String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; @override - String get analysis => 'Análise'; + String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; @override - String depthX(String param) { - return 'Profundidade $param'; - } + String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; @override - String get usingServerAnalysis => 'Análise de servidor em uso'; + String get puzzleDownVote => 'Votar contra o quebra-cabeça'; @override - String get loadingEngine => 'Carregando ...'; + String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; @override - String get calculatingMoves => 'Calculando jogadas...'; + String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; @override - String get engineFailed => 'Erro ao carregar o engine'; + String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; @override - String get cloudAnalysis => 'Análise na nuvem'; + String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; @override - String get goDeeper => 'Detalhar'; + String puzzlePuzzleId(String param) { + return 'Quebra-cabeça $param'; + } @override - String get showThreat => 'Mostrar ameaça'; + String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; @override - String get inLocalBrowser => 'no navegador local'; + String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; @override - String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; + String get puzzleClickToSolve => 'Clique para resolver'; @override - String get promoteVariation => 'Promover variante'; + String get puzzleGoodMove => 'Boa jogada'; @override - String get makeMainLine => 'Transformar em linha principal'; + String get puzzleBestMove => 'Melhor jogada!'; @override - String get deleteFromHere => 'Excluir a partir daqui'; + String get puzzleKeepGoing => 'Continue…'; @override - String get collapseVariations => 'Esconder variantes'; + String get puzzlePuzzleSuccess => 'Sucesso!'; @override - String get expandVariations => 'Mostrar variantes'; + String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; @override - String get forceVariation => 'Variante forçada'; + String get puzzleByOpenings => 'Por abertura'; @override - String get copyVariationPgn => 'Copiar PGN da variante'; + String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; @override - String get move => 'Movimentos'; + String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; @override - String get variantLoss => 'Derrota da variante'; + String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; @override - String get variantWin => 'Vitória da variante'; + String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; @override - String get insufficientMaterial => 'Material insuficiente'; + String get puzzleNotTheMove => 'O movimento não é este!'; @override - String get pawnMove => 'Movimento de peão'; + String get puzzleTrySomethingElse => 'Tente algo diferente.'; @override - String get capture => 'Captura'; + String puzzleRatingX(String param) { + return 'Rating: $param'; + } @override - String get close => 'Fechar'; + String get puzzleHidden => 'oculto'; @override - String get winning => 'Vencendo'; + String puzzleFromGameLink(String param) { + return 'Do jogo $param'; + } @override - String get losing => 'Perdendo'; + String get puzzleContinueTraining => 'Continue treinando'; @override - String get drawn => 'Empate'; + String get puzzleDifficultyLevel => 'Nível de dificuldade'; @override - String get unknown => 'Posição desconhecida'; + String get puzzleNormal => 'Normal'; @override - String get database => 'Banco de Dados'; + String get puzzleEasier => 'Fácil'; @override - String get whiteDrawBlack => 'Brancas / Empate / Pretas'; + String get puzzleEasiest => 'Muito fácil'; @override - String averageRatingX(String param) { - return 'Classificação média: $param'; - } + String get puzzleHarder => 'Difícil'; @override - String get recentGames => 'Partidas recentes'; + String get puzzleHardest => 'Muito difícil'; @override - String get topGames => 'Melhores partidas'; + String get puzzleExample => 'Exemplo'; @override - String masterDbExplanation(String param1, String param2, String param3) { - return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; - } + String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; @override - String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; + String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; @override - String get noGameFound => 'Nenhuma partida encontrada'; + String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; @override - String get maxDepthReached => 'Profundidade máxima alcançada!'; + String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; + String get puzzleImprovementAreas => 'Áreas de aprimoramento'; @override - String get openings => 'Aberturas'; + String get puzzleStrengths => 'Pontos fortes'; @override - String get openingExplorer => 'Explorador de aberturas'; + String get puzzleHistory => 'Histórico de quebra-cabeças'; @override - String get openingEndgameExplorer => 'Explorador de Aberturas/Finais'; + String get puzzleSolved => 'resolvido'; @override - String xOpeningExplorer(String param) { - return '$param Explorador de aberturas'; - } + String get puzzleFailed => 'falhou'; @override - String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; + String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; @override - String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; + String puzzleYourStreakX(String param) { + return 'Sua sequência: $param'; + } @override - String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; + String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; @override - String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; + String get puzzleContinueTheStreak => 'Continuar a sequência'; @override - String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; + String get puzzleNewStreak => 'Nova sequência'; @override - String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; + String get puzzleFromMyGames => 'Dos meus jogos'; @override - String get allSet => 'Tudo pronto!'; + String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; @override - String get importPgn => 'Importar PGN'; + String puzzleFromXGames(String param) { + return 'Problemas de $param\' jogos'; + } @override - String get delete => 'Excluir'; + String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; @override - String get deleteThisImportedGame => 'Excluir este jogo importado?'; + String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; @override - String get replayMode => 'Rever a partida'; + String puzzleFromXGamesFound(String param1, String param2) { + return '$param1 quebra-cabeças encontrados em $param2 partidas'; + } @override - String get realtimeReplay => 'Tempo Real'; + String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; @override - String get byCPL => 'Por erros'; + String puzzlePercentSolved(String param) { + return '$param resolvido'; + } @override - String get openStudy => 'Abrir estudo'; + String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; @override - String get enable => 'Ativar'; + String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; @override - String get bestMoveArrow => 'Seta de melhor movimento'; + String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; @override - String get showVariationArrows => 'Mostrar setas das variantes'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogado $count vezes', + one: 'Jogado $count vezes', + ); + return '$_temp0'; + } @override - String get evaluationGauge => 'Escala de avaliação'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos abaixo da sua classificação de quebra-cabeças', + one: 'Um ponto abaixo da sua classificação de quebra-cabeças', + ); + return '$_temp0'; + } @override - String get multipleLines => 'Linhas de análise'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos acima da sua classificação de quebra-cabeças', + one: 'Um ponto acima da sua classificação de quebra-cabeças', + ); + return '$_temp0'; + } @override - String get cpus => 'CPUs'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogados', + one: '$count jogado', + ); + return '$_temp0'; + } @override - String get memory => 'Memória'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count a serem repetidos', + one: '$count a ser repetido', + ); + return '$_temp0'; + } @override - String get infiniteAnalysis => 'Análise infinita'; + String get puzzleThemeAdvancedPawn => 'Peão avançado'; @override - String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; + String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; @override - String get engineManager => 'Gerenciador de engine'; + String get puzzleThemeAdvantage => 'Vantagem'; @override - String get blunder => 'Capivarada'; + String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; @override - String get mistake => 'Erro'; + String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; @override - String get inaccuracy => 'Imprecisão'; + String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; @override - String get moveTimes => 'Tempo por movimento'; + String get puzzleThemeArabianMate => 'Mate árabe'; @override - String get flipBoard => 'Girar o tabuleiro'; + String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; @override - String get threefoldRepetition => 'Tripla repetição'; + String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; @override - String get claimADraw => 'Reivindicar empate'; + String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; @override - String get offerDraw => 'Propor empate'; + String get puzzleThemeAttraction => 'Atração'; @override - String get draw => 'Empate'; + String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; @override - String get drawByMutualAgreement => 'Empate por acordo mútuo'; + String get puzzleThemeBackRankMate => 'Mate do corredor'; @override - String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; + String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; @override - String get currentGames => 'Partidas atuais'; + String get puzzleThemeBishopEndgame => 'Finais de bispo'; @override - String get viewInFullSize => 'Ver em tela cheia'; + String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; @override - String get logOut => 'Sair'; + String get puzzleThemeBodenMate => 'Mate de Boden'; @override - String get signIn => 'Entrar'; + String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; @override - String get rememberMe => 'Lembrar de mim'; + String get puzzleThemeCastling => 'Roque'; @override - String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; + String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; @override - String get signUp => 'Registrar'; + String get puzzleThemeCapturingDefender => 'Capture o defensor'; @override - String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; + String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; @override - String get games => 'Partidas'; + String get puzzleThemeCrushing => 'Punindo'; @override - String get forum => 'Fórum'; + String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; @override - String xPostedInForumY(String param1, String param2) { - return '$param1 publicou no tópico $param2'; - } + String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; @override - String get latestForumPosts => 'Últimas publicações no fórum'; + String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; @override - String get players => 'Jogadores'; + String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; @override - String get friends => 'Amigos'; + String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; @override - String get otherPlayers => 'outros jogadores'; + String get puzzleThemeEquality => 'Igualdade'; @override - String get discussions => 'Discussões'; + String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; @override - String get today => 'Hoje'; + String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; @override - String get yesterday => 'Ontem'; + String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; @override - String get minutesPerSide => 'Minutos por jogador'; + String get puzzleThemeClearance => 'Lance útil'; @override - String get variant => 'Variante'; + String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; @override - String get variants => 'Variantes'; + String get puzzleThemeDefensiveMove => 'Movimento defensivo'; @override - String get timeControl => 'Ritmo'; + String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; @override - String get realTime => 'Tempo real'; + String get puzzleThemeDeflection => 'Desvio'; @override - String get correspondence => 'Correspondência'; + String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; @override - String get daysPerTurn => 'Dias por lance'; + String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; @override - String get oneDay => 'Um dia'; + String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; @override - String get time => 'Tempo'; + String get puzzleThemeDoubleCheck => 'Xeque duplo'; @override - String get rating => 'Rating'; + String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; @override - String get ratingStats => 'Estatísticas de classificação'; + String get puzzleThemeEndgame => 'Finais'; @override - String get username => 'Nome de usuário'; + String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; @override - String get usernameOrEmail => 'Nome ou email do usuário'; + String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; @override - String get changeUsername => 'Alterar nome de usuário'; + String get puzzleThemeExposedKing => 'Rei exposto'; @override - String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; + String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; @override - String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; + String get puzzleThemeFork => 'Garfo (ou duplo)'; @override - String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; + String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; @override - String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; + String get puzzleThemeHangingPiece => 'Peça pendurada'; @override - String get password => 'Senha'; + String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; @override - String get changePassword => 'Alterar senha'; + String get puzzleThemeHookMate => 'Xeque gancho'; @override - String get changeEmail => 'Alterar email'; + String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; @override - String get email => 'E-mail'; + String get puzzleThemeInterference => 'Interferência'; @override - String get passwordReset => 'Redefinição de senha'; + String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; @override - String get forgotPassword => 'Esqueceu sua senha?'; + String get puzzleThemeIntermezzo => 'Lance intermediário'; @override - String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; + String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; @override - String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; + String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; @override - String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; + String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; @override - String get youAreLeavingLichess => 'Você está saindo do Lichess'; + String get puzzleThemeLong => 'Quebra-cabeças longo'; @override - String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; + String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; @override - String proceedToX(String param) { - return 'Ir para $param'; - } + String get puzzleThemeMaster => 'Partidas de mestres'; @override - String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; + String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; @override - String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; + String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; @override - String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; + String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; @override - String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; + String get puzzleThemeMate => 'Xeque-mate'; @override - String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; + String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; @override - String usernameNotFound(String param) { - return 'Não foi possível encontrar nenhum usuário com este nome: $param.'; - } + String get puzzleThemeMateIn1 => 'Mate em 1'; @override - String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; + String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; @override - String emailSent(String param) { - return 'Enviamos um e-mail para $param.'; - } + String get puzzleThemeMateIn2 => 'Mate em 2'; @override - String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; + String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; @override - String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; + String get puzzleThemeMateIn3 => 'Mate em 3'; @override - String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; + String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; @override - String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; + String get puzzleThemeMateIn4 => 'Mate em 4'; @override - String copyTextToEmail(String param) { - return 'Copie e cole o texto acima e envie-o para $param'; - } + String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; @override - String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; + String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; @override - String accountConfirmed(String param) { - return 'O usuário $param foi confirmado com sucesso.'; - } + String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; @override - String accountCanLogin(String param) { - return 'Você pode acessar agora como $param.'; - } + String get puzzleThemeMiddlegame => 'Meio-jogo'; @override - String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; + String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; @override - String accountClosed(String param) { - return 'A conta $param está encerrada.'; - } + String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; @override - String accountRegisteredWithoutEmail(String param) { - return 'A conta $param foi registrada sem um e-mail.'; - } + String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; @override - String get rank => 'Rank'; + String get puzzleThemeOpening => 'Abertura'; @override - String rankX(String param) { - return 'Classificação: $param'; - } + String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; @override - String get gamesPlayed => 'Partidas realizadas'; + String get puzzleThemePawnEndgame => 'Finais de peões'; @override - String get cancel => 'Cancelar'; + String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; @override - String get whiteTimeOut => 'Tempo das brancas esgotado'; + String get puzzleThemePin => 'Cravada'; @override - String get blackTimeOut => 'Tempo das pretas esgotado'; + String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; @override - String get drawOfferSent => 'Proposta de empate enviada'; + String get puzzleThemePromotion => 'Promoção'; @override - String get drawOfferAccepted => 'Proposta de empate aceita'; + String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; @override - String get drawOfferCanceled => 'Proposta de empate cancelada'; + String get puzzleThemeQueenEndgame => 'Finais de Dama'; @override - String get whiteOffersDraw => 'Brancas oferecem empate'; + String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; @override - String get blackOffersDraw => 'Pretas oferecem empate'; + String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; @override - String get whiteDeclinesDraw => 'Brancas recusam empate'; + String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; @override - String get blackDeclinesDraw => 'Pretas recusam empate'; + String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; @override - String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; + String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; @override - String get accept => 'Aceitar'; + String get puzzleThemeQuietMove => 'Lance de preparação'; @override - String get decline => 'Recusar'; + String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; @override - String get playingRightNow => 'Jogando agora'; + String get puzzleThemeRookEndgame => 'Finais de Torres'; @override - String get eventInProgress => 'Jogando agora'; + String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; @override - String get finished => 'Terminado'; + String get puzzleThemeSacrifice => 'Sacrifício'; @override - String get abortGame => 'Cancelar partida'; + String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; @override - String get gameAborted => 'Partida cancelada'; + String get puzzleThemeShort => 'Quebra-cabeças curto'; @override - String get standard => 'Padrão'; + String get puzzleThemeShortDescription => 'Vitória em dois lances.'; @override - String get customPosition => 'Posição personalizada'; + String get puzzleThemeSkewer => 'Raio X'; @override - String get unlimited => 'Ilimitado'; + String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; @override - String get mode => 'Modo'; + String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; @override - String get casual => 'Amistosa'; + String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; @override - String get rated => 'Ranqueada'; + String get puzzleThemeSuperGM => 'Super partidas de GMs'; @override - String get casualTournament => 'Amistoso'; + String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; @override - String get ratedTournament => 'Valendo pontos'; + String get puzzleThemeTrappedPiece => 'Peça presa'; @override - String get thisGameIsRated => 'Esta partida vale pontos'; + String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; @override - String get rematch => 'Revanche'; + String get puzzleThemeUnderPromotion => 'Subpromoção'; @override - String get rematchOfferSent => 'Oferta de revanche enviada'; + String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; @override - String get rematchOfferAccepted => 'Oferta de revanche aceita'; + String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; @override - String get rematchOfferCanceled => 'Oferta de revanche cancelada'; + String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; @override - String get rematchOfferDeclined => 'Oferta de revanche recusada'; + String get puzzleThemeXRayAttack => 'Ataque em raio X'; @override - String get cancelRematchOffer => 'Cancelar oferta de revanche'; + String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; @override - String get viewRematch => 'Ver revanche'; + String get puzzleThemeZugzwang => 'Zugzwang'; @override - String get confirmMove => 'Confirmar lance'; + String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; @override - String get play => 'Jogar'; + String get puzzleThemeMix => 'Combinação saudável'; @override - String get inbox => 'Mensagens'; + String get puzzleThemeMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; @override - String get chatRoom => 'Sala de chat'; + String get puzzleThemePlayerGames => 'Partidas de jogadores'; @override - String get loginToChat => 'Faça login para conversar'; + String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; @override - String get youHaveBeenTimedOut => 'Sua sessão expirou.'; + String puzzleThemePuzzleDownloadInformation(String param) { + return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; + } @override - String get spectatorRoom => 'Sala do espectador'; + String get searchSearch => 'Buscar'; @override - String get composeMessage => 'Escrever mensagem'; + String get settingsSettings => 'Configurações'; @override - String get subject => 'Assunto'; + String get settingsCloseAccount => 'Encerrar conta'; @override - String get send => 'Enviar'; + String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; @override - String get incrementInSeconds => 'Acréscimo em segundos'; + String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; @override - String get freeOnlineChess => 'Xadrez Online Gratuito'; + String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; @override - String get exportGames => 'Exportar partidas'; + String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; @override - String get ratingRange => 'Rating entre'; + String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; @override - String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; + String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; @override - String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; + String get playWithAFriend => 'Jogar contra um amigo'; @override - String get takeback => 'Voltar jogada'; + String get playWithTheMachine => 'Jogar contra o computador'; @override - String get proposeATakeback => 'Propor voltar jogada'; + String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; @override - String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; + String get gameOver => 'Fim da partida'; @override - String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; + String get waitingForOpponent => 'Aguardando oponente'; @override - String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; + String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este código QR'; @override - String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; + String get waiting => 'Aguardando'; @override - String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; + String get yourTurn => 'Sua vez'; @override - String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1 nível $param2'; + } @override - String get tournament => 'Torneio'; + String get level => 'Nível'; @override - String get tournaments => 'Torneios'; + String get strength => 'Nível'; @override - String get tournamentPoints => 'Pontos de torneios'; + String get toggleTheChat => 'Ativar/Desativar chat'; @override - String get viewTournament => 'Ver torneio'; + String get chat => 'Chat'; @override - String get backToTournament => 'Voltar ao torneio'; + String get resign => 'Desistir'; @override - String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; + String get checkmate => 'Xeque-mate'; @override - String get thematic => 'Temático'; + String get stalemate => 'Rei afogado'; @override - String yourPerfRatingIsProvisional(String param) { - return 'Seu rating $param é provisório'; - } + String get white => 'Brancas'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu $param1 rating ($param2) é muito alta'; - } + String get black => 'Pretas'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; - } + String get asWhite => 'de brancas'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return 'Sua $param1 pontuação ($param2) é muito baixa'; - } + String get asBlack => 'de pretas'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return 'Pontuação ≥ $param1 em $param2'; - } + String get randomColor => 'Cor aleatória'; @override - String ratedLessThanInPerf(String param1, String param2) { - return 'Pontuação ≤ $param1 em $param2'; - } + String get createAGame => 'Criar uma partida'; @override - String mustBeInTeam(String param) { - return 'Precisa estar na equipe $param'; - } + String get whiteIsVictorious => 'Brancas vencem'; @override - String youAreNotInTeam(String param) { - return 'Você não está na equipe $param'; - } + String get blackIsVictorious => 'Pretas vencem'; @override - String get backToGame => 'Retorne à partida'; + String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; @override - String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; + String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1 juntou-se à equipe $param2'; - } + String get itsYourTurn => 'É a sua vez!'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1 criou a equipe $param2'; - } + String get cheatDetected => 'Trapaça Detectada'; @override - String get startedStreaming => 'começou uma transmissão ao vivo'; + String get kingInTheCenter => 'Rei no centro'; @override - String xStartedStreaming(String param) { - return '$param começou a transmitir'; - } + String get threeChecks => 'Três xeques'; @override - String get averageElo => 'Média de rating'; + String get raceFinished => 'Corrida terminada'; @override - String get location => 'Localização'; + String get variantEnding => 'Fim da variante'; @override - String get filterGames => 'Filtrar partidas'; + String get newOpponent => 'Novo oponente'; @override - String get reset => 'Reiniciar'; + String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; @override - String get apply => 'Aplicar'; + String get joinTheGame => 'Entrar no jogo'; @override - String get save => 'Salvar'; + String get whitePlays => 'Brancas jogam'; @override - String get leaderboard => 'Classificação'; + String get blackPlays => 'Pretas jogam'; @override - String get screenshotCurrentPosition => 'Captura de tela da posição atual'; + String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; @override - String get gameAsGIF => 'Salvar a partida como GIF'; + String get forceResignation => 'Reivindicar vitória'; @override - String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; + String get forceDraw => 'Reivindicar empate'; @override - String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; + String get talkInChat => 'Por favor, seja gentil no chat!'; @override - String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; @override - String get fromPosition => 'A partir da posição'; + String get whiteResigned => 'Brancas desistiram'; @override - String get continueFromHere => 'Continuar daqui'; + String get blackResigned => 'Pretas desistiram'; @override - String get toStudy => 'Estudo'; + String get whiteLeftTheGame => 'Brancas deixaram a partida'; @override - String get importGame => 'Importar partida'; + String get blackLeftTheGame => 'Pretas deixaram a partida'; @override - String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; + String get whiteDidntMove => 'Brancas não moveram'; @override - String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; + String get blackDidntMove => 'Pretas não moveram'; @override - String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; + String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; @override - String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; + String get computerAnalysis => 'Análise do computador'; @override - String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; + String get computerAnalysisAvailable => 'Análise de computador disponível'; @override - String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; + String get computerAnalysisDisabled => 'Análise de computador desativada'; @override - String get notACheckmate => 'Não é xeque-mate'; + String get analysis => 'Análise'; @override - String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; + String depthX(String param) { + return 'Profundidade $param'; + } @override - String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; + String get usingServerAnalysis => 'Análise de servidor em uso'; @override - String get retry => 'Tentar novamente'; + String get loadingEngine => 'Carregando ...'; @override - String get reconnecting => 'Reconectando'; + String get calculatingMoves => 'Calculando jogadas...'; @override - String get noNetwork => 'Sem conexão'; + String get engineFailed => 'Erro ao carregar o engine'; @override - String get favoriteOpponents => 'Adversários favoritos'; + String get cloudAnalysis => 'Análise na nuvem'; @override - String get follow => 'Seguir'; + String get goDeeper => 'Detalhar'; @override - String get following => 'Seguindo'; + String get showThreat => 'Mostrar ameaça'; @override - String get unfollow => 'Parar de seguir'; + String get inLocalBrowser => 'no navegador local'; @override - String followX(String param) { - return 'Seguir $param'; - } + String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; @override - String unfollowX(String param) { - return 'Deixar de seguir $param'; - } + String get promoteVariation => 'Promover variante'; @override - String get block => 'Bloquear'; + String get makeMainLine => 'Transformar em linha principal'; @override - String get blocked => 'Bloqueado'; + String get deleteFromHere => 'Excluir a partir daqui'; @override - String get unblock => 'Desbloquear'; + String get collapseVariations => 'Recolher variações'; @override - String get followsYou => 'Segue você'; + String get expandVariations => 'Expandir variações'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1 começou a seguir $param2'; - } + String get forceVariation => 'Variante forçada'; @override - String get more => 'Mais'; + String get copyVariationPgn => 'Copiar PGN da variante'; @override - String get memberSince => 'Membro desde'; + String get move => 'Movimentos'; @override - String lastSeenActive(String param) { - return 'Ativo $param'; - } + String get variantLoss => 'Derrota da variante'; @override - String get player => 'Jogador'; + String get variantWin => 'Vitória da variante'; @override - String get list => 'Lista'; + String get insufficientMaterial => 'Material insuficiente'; @override - String get graph => 'Gráfico'; + String get pawnMove => 'Movimento de peão'; @override - String get required => 'Obrigatório.'; + String get capture => 'Captura'; @override - String get openTournaments => 'Torneios abertos'; + String get close => 'Fechar'; @override - String get duration => 'Duração'; + String get winning => 'Vencendo'; @override - String get winner => 'Vencedor'; + String get losing => 'Perdendo'; @override - String get standing => 'Colocação'; + String get drawn => 'Empate'; @override - String get createANewTournament => 'Criar novo torneio'; + String get unknown => 'Posição desconhecida'; @override - String get tournamentCalendar => 'Calendário do torneio'; + String get database => 'Banco de Dados'; @override - String get conditionOfEntry => 'Condições de participação:'; + String get whiteDrawBlack => 'Brancas / Empate / Pretas'; @override - String get advancedSettings => 'Configurações avançadas'; + String averageRatingX(String param) { + return 'Classificação média: $param'; + } @override - String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; + String get recentGames => 'Partidas recentes'; @override - String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; + String get topGames => 'Melhores partidas'; @override - String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; + String masterDbExplanation(String param1, String param2, String param3) { + return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; + } @override - String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; + String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de lances até a próxima captura ou movimento de peão'; @override - String get join => 'Entrar'; + String get noGameFound => 'Nenhuma partida encontrada'; @override - String get withdraw => 'Sair'; + String get maxDepthReached => 'Profundidade máxima alcançada!'; @override - String get points => 'Pontos'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; @override - String get wins => 'Vitórias'; + String get openings => 'Aberturas'; @override - String get losses => 'Derrotas'; + String get openingExplorer => 'Explorador de aberturas'; @override - String get createdBy => 'Criado por'; + String get openingEndgameExplorer => 'Explorador de Aberturas/Finais'; @override - String get tournamentIsStarting => 'O torneio está começando'; + String xOpeningExplorer(String param) { + return '$param Explorador de aberturas'; + } @override - String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; + String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; @override - String standByX(String param) { - return '$param, aguarde: o pareamento está em andamento, prepare-se!'; - } + String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; @override - String get pause => 'Pausar'; + String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; @override - String get resume => 'Continuar'; + String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; @override - String get youArePlaying => 'Você está participando!'; + String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; @override - String get winRate => 'Taxa de vitórias'; + String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; @override - String get berserkRate => 'Taxa Berserk'; + String get allSet => 'Tudo pronto!'; @override - String get performance => 'Desempenho'; + String get importPgn => 'Importar PGN'; @override - String get tournamentComplete => 'Torneio completo'; + String get delete => 'Excluir'; @override - String get movesPlayed => 'Movimentos realizados'; + String get deleteThisImportedGame => 'Excluir este jogo importado?'; @override - String get whiteWins => 'Brancas venceram'; + String get replayMode => 'Rever a partida'; @override - String get blackWins => 'Pretas venceram'; + String get realtimeReplay => 'Tempo Real'; @override - String get drawRate => 'Taxa de empates'; + String get byCPL => 'Por erros'; @override - String get draws => 'Empates'; + String get enable => 'Ativar'; @override - String nextXTournament(String param) { - return 'Próximo torneio $param:'; - } + String get bestMoveArrow => 'Seta de melhor movimento'; @override - String get averageOpponent => 'Pontuação média adversários'; + String get showVariationArrows => 'Mostrar setas das variantes'; @override - String get boardEditor => 'Editor de tabuleiro'; + String get evaluationGauge => 'Escala de avaliação'; @override - String get setTheBoard => 'Defina a posição'; + String get multipleLines => 'Linhas de análise'; @override - String get popularOpenings => 'Aberturas populares'; + String get cpus => 'CPUs'; @override - String get endgamePositions => 'Posições de final'; + String get memory => 'Memória'; @override - String chess960StartPosition(String param) { - return 'Posição inicial do Xadrez960: $param'; - } + String get infiniteAnalysis => 'Análise infinita'; @override - String get startPosition => 'Posição inicial'; + String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; @override - String get clearBoard => 'Limpar tabuleiro'; + String get blunder => 'Capivarada'; @override - String get loadPosition => 'Carregar posição'; + String get mistake => 'Erro'; @override - String get isPrivate => 'Privado'; + String get inaccuracy => 'Imprecisão'; @override - String reportXToModerators(String param) { - return 'Reportar $param aos moderadores'; - } + String get moveTimes => 'Tempo por movimento'; @override - String profileCompletion(String param) { - return 'Conclusão do perfil: $param'; - } + String get flipBoard => 'Girar o tabuleiro'; @override - String xRating(String param) { - return 'Rating $param'; - } + String get threefoldRepetition => 'Tripla repetição'; @override - String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; + String get claimADraw => 'Reivindicar empate'; @override - String get profile => 'Perfil'; + String get offerDraw => 'Propor empate'; @override - String get editProfile => 'Editar perfil'; + String get draw => 'Empate'; @override - String get realName => 'Nome real'; + String get drawByMutualAgreement => 'Empate por acordo mútuo'; @override - String get setFlair => 'Escolha seu emote'; + String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; @override - String get flair => 'Estilo'; + String get currentGames => 'Partidas atuais'; @override - String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; + String get viewInFullSize => 'Ver em tela cheia'; @override - String get biography => 'Biografia'; + String get logOut => 'Sair'; @override - String get countryRegion => 'País ou região'; + String get signIn => 'Entrar'; @override - String get thankYou => 'Obrigado!'; + String get rememberMe => 'Lembrar de mim'; @override - String get socialMediaLinks => 'Links de mídia social'; + String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; @override - String get oneUrlPerLine => 'Uma URL por linha.'; + String get signUp => 'Registrar'; @override - String get inlineNotation => 'Notação em linha'; + String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; @override - String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; + String get games => 'Partidas'; @override - String get clearSavedMoves => 'Limpar lances'; + String get forum => 'Fórum'; @override - String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; + String xPostedInForumY(String param1, String param2) { + return '$param1 publicou no tópico $param2'; + } @override - String get onlinePlayers => 'Jogadores online'; + String get latestForumPosts => 'Últimas publicações no fórum'; @override - String get activePlayers => 'Jogadores ativos'; + String get players => 'Jogadores'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; + String get friends => 'Amigos'; @override - String get success => 'Sucesso'; + String get otherPlayers => 'outros jogadores'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; + String get discussions => 'Discussões'; @override - String get autoSwitch => 'Alternar automaticamente'; + String get today => 'Hoje'; @override - String get puzzles => 'Quebra-cabeças'; + String get yesterday => 'Ontem'; @override - String get onlineBots => 'Bots online'; + String get minutesPerSide => 'Minutos por jogador'; @override - String get name => 'Nome'; + String get variant => 'Variante'; @override - String get description => 'Descrição'; + String get variants => 'Variantes'; @override - String get descPrivate => 'Descrição privada'; + String get timeControl => 'Ritmo'; @override - String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; + String get realTime => 'Tempo real'; @override - String get no => 'Não'; + String get correspondence => 'Correspondência'; @override - String get yes => 'Sim'; + String get daysPerTurn => 'Dias por lance'; @override - String get website => 'Site'; + String get oneDay => 'Um dia'; @override - String get mobile => 'Celular'; + String get time => 'Tempo'; @override - String get help => 'Ajuda:'; + String get rating => 'Rating'; @override - String get createANewTopic => 'Criar novo tópico'; + String get ratingStats => 'Estatísticas de classificação'; @override - String get topics => 'Tópicos'; + String get username => 'Nome de usuário'; @override - String get posts => 'Publicações'; + String get usernameOrEmail => 'Nome ou email do usuário'; @override - String get lastPost => 'Última postagem'; + String get changeUsername => 'Alterar nome de usuário'; @override - String get views => 'Visualizações'; + String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; @override - String get replies => 'Respostas'; + String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; @override - String get replyToThisTopic => 'Responder a este tópico'; + String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; @override - String get reply => 'Responder'; + String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; @override - String get message => 'Mensagem'; + String get password => 'Senha'; @override - String get createTheTopic => 'Criar tópico'; + String get changePassword => 'Alterar senha'; @override - String get reportAUser => 'Reportar um usuário'; + String get changeEmail => 'Alterar email'; @override - String get user => 'Usuário'; + String get email => 'E-mail'; @override - String get reason => 'Motivo'; + String get passwordReset => 'Redefinição de senha'; @override - String get whatIsIheMatter => 'Qual é o motivo?'; + String get forgotPassword => 'Esqueceu sua senha?'; @override - String get cheat => 'Trapaça'; + String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; @override - String get troll => 'Troll'; + String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; @override - String get other => 'Outro'; + String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; @override - String get reportDescriptionHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.'; + String get youAreLeavingLichess => 'Você está saindo do Lichess'; @override - String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; + String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; @override - String by(String param) { - return 'por $param'; + String proceedToX(String param) { + return 'Ir para $param'; } @override - String importedByX(String param) { - return 'Importado por $param'; - } + String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get thisTopicIsNowClosed => 'O tópico foi fechado.'; + String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get blog => 'Blog'; + String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; @override - String get notes => 'Notas'; + String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; @override - String get typePrivateNotesHere => 'Digite notas pessoais aqui'; + String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; @override - String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; + String usernameNotFound(String param) { + return 'Não foi possível encontrar nenhum usuário com este nome: $param.'; + } @override - String get noNoteYet => 'Nenhuma nota'; + String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; @override - String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; + String emailSent(String param) { + return 'Enviamos um e-mail para $param.'; + } @override - String get incorrectPassword => 'Senha incorreta'; + String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; @override - String get invalidAuthenticationCode => 'Código de verificação inválido'; + String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; @override - String get emailMeALink => 'Me envie um link'; + String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; @override - String get currentPassword => 'Senha atual'; + String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; @override - String get newPassword => 'Nova senha'; + String copyTextToEmail(String param) { + return 'Copie e cole o texto acima e envie-o para $param'; + } @override - String get newPasswordAgain => 'Nova senha (novamente)'; + String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; @override - String get newPasswordsDontMatch => 'As novas senhas não correspondem'; + String accountConfirmed(String param) { + return 'O usuário $param foi confirmado com sucesso.'; + } @override - String get newPasswordStrength => 'Senha forte'; + String accountCanLogin(String param) { + return 'Você pode acessar agora como $param.'; + } @override - String get clockInitialTime => 'Tempo de relógio'; + String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; @override - String get clockIncrement => 'Incremento do relógio'; + String accountClosed(String param) { + return 'A conta $param está encerrada.'; + } @override - String get privacy => 'Privacidade'; + String accountRegisteredWithoutEmail(String param) { + return 'A conta $param foi registrada sem um e-mail.'; + } @override - String get privacyPolicy => 'Política de privacidade'; + String get rank => 'Rank'; @override - String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; + String rankX(String param) { + return 'Classificação: $param'; + } @override - String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; + String get gamesPlayed => 'Partidas realizadas'; @override - String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; + String get ok => 'OK'; @override - String get sound => 'Som'; + String get cancel => 'Cancelar'; @override - String get none => 'Nenhum'; + String get whiteTimeOut => 'Tempo das brancas esgotado'; @override - String get fast => 'Rápido'; + String get blackTimeOut => 'Tempo das pretas esgotado'; @override - String get normal => 'Normal'; + String get drawOfferSent => 'Proposta de empate enviada'; @override - String get slow => 'Lento'; + String get drawOfferAccepted => 'Proposta de empate aceita'; @override - String get insideTheBoard => 'Dentro do tabuleiro'; + String get drawOfferCanceled => 'Proposta de empate cancelada'; @override - String get outsideTheBoard => 'Fora do tabuleiro'; + String get whiteOffersDraw => 'Brancas oferecem empate'; @override - String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; + String get blackOffersDraw => 'Pretas oferecem empate'; @override - String get onSlowGames => 'Em partidas lentas'; + String get whiteDeclinesDraw => 'Brancas recusam empate'; @override - String get always => 'Sempre'; + String get blackDeclinesDraw => 'Pretas recusam empate'; @override - String get never => 'Nunca'; + String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; @override - String xCompetesInY(String param1, String param2) { - return '$param1 compete em $param2'; - } + String get accept => 'Aceitar'; @override - String get victory => 'Vitória'; + String get decline => 'Recusar'; @override - String get defeat => 'Derrota'; + String get playingRightNow => 'Jogando agora'; @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get eventInProgress => 'Jogando agora'; @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get finished => 'Terminado'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get abortGame => 'Cancelar partida'; @override - String get timeline => 'Linha do tempo'; + String get gameAborted => 'Partida cancelada'; @override - String get starting => 'Iniciando:'; + String get standard => 'Padrão'; @override - String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; + String get customPosition => 'Posição personalizada'; @override - String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; + String get unlimited => 'Ilimitado'; @override - String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; + String get mode => 'Modo'; @override - String get human => 'Humano'; + String get casual => 'Amistosa'; @override - String get computer => 'Computador'; + String get rated => 'Ranqueada'; @override - String get side => 'Cor'; + String get casualTournament => 'Amistoso'; @override - String get clock => 'Relógio'; + String get ratedTournament => 'Valendo pontos'; @override - String get opponent => 'Adversário'; + String get thisGameIsRated => 'Esta partida vale pontos'; @override - String get learnMenu => 'Aprender'; + String get rematch => 'Revanche'; @override - String get studyMenu => 'Estudar'; + String get rematchOfferSent => 'Oferta de revanche enviada'; @override - String get practice => 'Praticar'; + String get rematchOfferAccepted => 'Oferta de revanche aceita'; @override - String get community => 'Comunidade'; + String get rematchOfferCanceled => 'Oferta de revanche cancelada'; @override - String get tools => 'Ferramentas'; + String get rematchOfferDeclined => 'Oferta de revanche recusada'; @override - String get increment => 'Incremento'; + String get cancelRematchOffer => 'Cancelar oferta de revanche'; @override - String get error_unknown => 'Valor inválido'; + String get viewRematch => 'Ver revanche'; @override - String get error_required => 'Este campo deve ser preenchido'; + String get confirmMove => 'Confirmar lance'; @override - String get error_email => 'Este endereço de e-mail é inválido'; + String get play => 'Jogar'; @override - String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; + String get inbox => 'Mensagens'; @override - String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; + String get chatRoom => 'Sala de chat'; @override - String get error_email_different => 'Este já é o seu endereço de e-mail'; + String get loginToChat => 'Faça login para conversar'; @override - String error_minLength(String param) { - return 'O mínimo de caracteres é $param'; - } + String get youHaveBeenTimedOut => 'Sua sessão expirou.'; @override - String error_maxLength(String param) { - return 'O máximo de caracteres é $param'; - } + String get spectatorRoom => 'Sala do espectador'; @override - String error_min(String param) { - return 'Deve ser maior ou igual a $param'; - } + String get composeMessage => 'Escrever mensagem'; @override - String error_max(String param) { - return 'Deve ser menor ou igual a $param'; - } + String get subject => 'Assunto'; @override - String ifRatingIsPlusMinusX(String param) { - return 'Se o rating for ± $param'; - } + String get send => 'Enviar'; @override - String get ifRegistered => 'Se registrado'; + String get incrementInSeconds => 'Acréscimo em segundos'; @override - String get onlyExistingConversations => 'Apenas conversas iniciadas'; + String get freeOnlineChess => 'Xadrez Online Gratuito'; @override - String get onlyFriends => 'Apenas amigos'; + String get exportGames => 'Exportar partidas'; @override - String get menu => 'Menu'; + String get ratingRange => 'Rating entre'; @override - String get castling => 'Roque'; + String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; @override - String get whiteCastlingKingside => 'O-O das brancas'; + String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; @override - String get blackCastlingKingside => 'O-O das pretas'; + String get takeback => 'Voltar jogada'; @override - String tpTimeSpentPlaying(String param) { - return 'Tempo jogando: $param'; - } + String get proposeATakeback => 'Propor voltar jogada'; @override - String get watchGames => 'Assistir partidas'; + String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; @override - String tpTimeSpentOnTV(String param) { - return 'Tempo na TV: $param'; - } + String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; @override - String get watch => 'Assistir'; + String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; @override - String get videoLibrary => 'Vídeos'; + String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; @override - String get streamersMenu => 'Streamers'; + String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; @override - String get mobileApp => 'Aplicativo Móvel'; + String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; @override - String get webmasters => 'Webmasters'; + String get tournament => 'Torneio'; @override - String get about => 'Sobre'; + String get tournaments => 'Torneios'; @override - String aboutX(String param) { - return 'Sobre o $param'; - } + String get tournamentPoints => 'Pontos de torneios'; @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; - } + String get viewTournament => 'Ver torneio'; @override - String get really => 'realmente'; + String get backToTournament => 'Voltar ao torneio'; @override - String get contribute => 'Contribuir'; + String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; @override - String get termsOfService => 'Termos de serviço'; + String get thematic => 'Temático'; @override - String get sourceCode => 'Código-fonte'; + String yourPerfRatingIsProvisional(String param) { + return 'Seu rating $param é provisório'; + } @override - String get simultaneousExhibitions => 'Exibição simultânea'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return 'Seu $param1 rating ($param2) é muito alta'; + } @override - String get host => 'Simultanista'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; + } @override - String hostColorX(String param) { - return 'Cor do simultanista: $param'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return 'Sua $param1 pontuação ($param2) é muito baixa'; } @override - String get yourPendingSimuls => 'Suas simultâneas pendentes'; + String ratedMoreThanInPerf(String param1, String param2) { + return 'Pontuação ≥ $param1 em $param2'; + } @override - String get createdSimuls => 'Simultâneas criadas recentemente'; + String ratedLessThanInPerf(String param1, String param2) { + return 'Pontuação ≤ $param1 em $param2'; + } @override - String get hostANewSimul => 'Iniciar nova simultânea'; + String mustBeInTeam(String param) { + return 'Precisa estar na equipe $param'; + } @override - String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; + String youAreNotInTeam(String param) { + return 'Você não está na equipe $param'; + } @override - String get noSimulFound => 'Simultânea não encontrada'; + String get backToGame => 'Retorne à partida'; @override - String get noSimulExplanation => 'Esta exibição simultânea não existe.'; + String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; @override - String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; + String xJoinedTeamY(String param1, String param2) { + return '$param1 juntou-se à equipe $param2'; + } @override - String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; + String xCreatedTeamY(String param1, String param2) { + return '$param1 criou a equipe $param2'; + } @override - String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + String get startedStreaming => 'começou uma transmissão ao vivo'; @override - String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; + String xStartedStreaming(String param) { + return '$param começou a transmitir'; + } @override - String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; + String get averageElo => 'Média de rating'; @override - String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; + String get location => 'Localização'; @override - String get create => 'Criar'; + String get filterGames => 'Filtrar partidas'; @override - String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; + String get reset => 'Reiniciar'; @override - String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; + String get apply => 'Aplicar'; @override - String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; + String get save => 'Salvar'; @override - String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; + String get leaderboard => 'Classificação'; @override - String get simulHostExtraTime => 'Tempo adicional do simultanista'; + String get screenshotCurrentPosition => 'Captura de tela da posição atual'; @override - String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; + String get gameAsGIF => 'Salvar a partida como GIF'; @override - String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; + String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; @override - String get lichessTournaments => 'Torneios do Lichess'; + String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; @override - String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; + String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; @override - String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; + String get fromPosition => 'A partir da posição'; @override - String get averageCentipawnLoss => 'Perda média em centipeões'; + String get continueFromHere => 'Continuar daqui'; @override - String get accuracy => 'Precisão'; + String get toStudy => 'Estudar'; @override - String get keyboardShortcuts => 'Atalhos de teclado'; + String get importGame => 'Importar partida'; @override - String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; + String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; @override - String get keyGoToStartOrEnd => 'ir para início/fim'; + String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; @override - String get keyCycleSelectedVariation => 'Alternar entre as variantes'; + String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; @override - String get keyShowOrHideComments => 'mostrar/ocultar comentários'; + String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; @override - String get keyEnterOrExitVariation => 'entrar/sair da variante'; + String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; @override - String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; + String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; @override - String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; + String get notACheckmate => 'Não é xeque-mate'; @override - String get keyNextBlunder => 'Próximo erro grave'; + String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; @override - String get keyNextMistake => 'Próximo erro'; + String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; @override - String get keyNextInaccuracy => 'Próxima imprecisão'; + String get retry => 'Tentar novamente'; @override - String get keyPreviousBranch => 'Branch anterior'; + String get reconnecting => 'Reconectando'; @override - String get keyNextBranch => 'Próximo branch'; + String get noNetwork => 'Sem conexão'; @override - String get toggleVariationArrows => 'Ativar/desativar setas'; + String get favoriteOpponents => 'Adversários favoritos'; @override - String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; + String get follow => 'Seguir'; @override - String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; + String get following => 'Seguindo'; @override - String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; + String get unfollow => 'Parar de seguir'; @override - String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; + String followX(String param) { + return 'Seguir $param'; + } @override - String get playSelectedMove => 'jogar movimento selecionado'; + String unfollowX(String param) { + return 'Deixar de seguir $param'; + } @override - String get newTournament => 'Novo torneio'; + String get block => 'Bloquear'; @override - String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; + String get blocked => 'Bloqueado'; @override - String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; + String get unblock => 'Desbloquear'; @override - String get tournamentNotFound => 'Torneio não encontrado'; + String xStartedFollowingY(String param1, String param2) { + return '$param1 começou a seguir $param2'; + } @override - String get tournamentDoesNotExist => 'Este torneio não existe.'; + String get more => 'Mais'; @override - String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; + String get memberSince => 'Membro desde'; @override - String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; + String lastSeenActive(String param) { + return 'Ativo $param'; + } @override - String weeklyPerfTypeRatingDistribution(String param) { - return 'Distribuição mensal de rating em $param'; - } + String get player => 'Jogador'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'Seu rating em $param1 é $param2.'; - } + String get list => 'Lista'; @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'Você é melhor que $param1 dos jogadores de $param2.'; - } + String get graph => 'Gráfico'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 é melhor que $param2 dos $param3 jogadores.'; - } + String get required => 'Obrigatório.'; @override - String betterThanPercentPlayers(String param1, String param2) { - return 'Melhor que $param1 dos jogadores de $param2'; - } + String get openTournaments => 'Torneios abertos'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'Você não tem rating definido em $param.'; - } + String get duration => 'Duração'; @override - String get yourRating => 'Seu rating'; + String get winner => 'Vencedor'; @override - String get cumulative => 'Cumulativo'; + String get standing => 'Colocação'; @override - String get glicko2Rating => 'Rating Glicko-2'; + String get createANewTournament => 'Criar novo torneio'; @override - String get checkYourEmail => 'Verifique seu e-mail'; + String get tournamentCalendar => 'Calendário do torneio'; @override - String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; + String get conditionOfEntry => 'Condições de participação:'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; + String get advancedSettings => 'Configurações avançadas'; @override - String weHaveSentYouAnEmailTo(String param) { - return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; - } + String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'Ao registrar, você concorda em se comprometer com nossa $param.'; - } + String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; @override - String readAboutOur(String param) { - return 'Leia sobre a nossa $param.'; - } + String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; @override - String get networkLagBetweenYouAndLichess => 'Atraso na rede'; + String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; @override - String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; + String get join => 'Entrar'; @override - String get downloadAnnotated => 'Baixar anotação'; + String get withdraw => 'Sair'; @override - String get downloadRaw => 'Baixar texto'; + String get points => 'Pontos'; @override - String get downloadImported => 'Baixar partida importada'; + String get wins => 'Vitórias'; @override - String get crosstable => 'Tabela'; + String get losses => 'Derrotas'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; + String get createdBy => 'Criado por'; @override - String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; + String get tournamentIsStarting => 'O torneio está começando'; @override - String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; + String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; @override - String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; + String standByX(String param) { + return '$param, aguarde: o pareamento está em andamento, prepare-se!'; + } @override - String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; + String get pause => 'Pausar'; @override - String get shareYourInsightsData => 'Compartilhe seus dados da análise'; + String get resume => 'Continuar'; @override - String get withNobody => 'Com ninguém'; + String get youArePlaying => 'Você está participando!'; @override - String get withFriends => 'Com amigos'; + String get winRate => 'Taxa de vitórias'; @override - String get withEverybody => 'Com todos'; + String get berserkRate => 'Taxa Berserk'; @override - String get kidMode => 'Modo infantil'; + String get performance => 'Desempenho'; @override - String get kidModeIsEnabled => 'O modo infantil está ativado.'; + String get tournamentComplete => 'Torneio completo'; @override - String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; + String get movesPlayed => 'Movimentos realizados'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; - } + String get whiteWins => 'Brancas venceram'; @override - String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; + String get blackWins => 'Pretas venceram'; @override - String get enableKidMode => 'Habilitar o modo infantil'; + String get drawRate => 'Taxa de empates'; @override - String get disableKidMode => 'Desabilitar o modo infantil'; + String get draws => 'Empates'; @override - String get security => 'Segurança'; + String nextXTournament(String param) { + return 'Próximo torneio de $param:'; + } @override - String get sessions => 'Sessões'; + String get averageOpponent => 'Pontuação média adversários'; @override - String get revokeAllSessions => 'revogar todas as sessões'; + String get boardEditor => 'Editor de tabuleiro'; @override - String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; + String get setTheBoard => 'Defina a posição'; @override - String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; + String get popularOpenings => 'Aberturas populares'; @override - String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; + String get endgamePositions => 'Posições de final'; @override - String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; + String chess960StartPosition(String param) { + return 'Posição inicial do Xadrez960: $param'; + } @override - String get zeroAdvertisement => 'Zero anúncios'; + String get startPosition => 'Posição inicial'; @override - String get fullFeatured => 'Cheio de recursos'; + String get clearBoard => 'Limpar tabuleiro'; @override - String get phoneAndTablet => 'Celular e tablet'; + String get loadPosition => 'Carregar posição'; @override - String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + String get isPrivate => 'Privado'; @override - String get correspondenceChess => 'Xadrez por correspondência'; + String reportXToModerators(String param) { + return 'Reportar $param aos moderadores'; + } @override - String get onlineAndOfflinePlay => 'Jogue online e offline'; + String profileCompletion(String param) { + return 'Conclusão do perfil: $param'; + } @override - String get viewTheSolution => 'Ver solução'; + String xRating(String param) { + return 'Rating $param'; + } @override - String get followAndChallengeFriends => 'Siga e desafie amigos'; + String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; @override - String get gameAnalysis => 'Análise da partida'; + String get profile => 'Perfil'; @override - String xHostsY(String param1, String param2) { - return '$param1 criou $param2'; - } + String get editProfile => 'Editar perfil'; + + @override + String get realName => 'Nome real'; + + @override + String get setFlair => 'Escolha seu emote'; + + @override + String get flair => 'Estilo'; + + @override + String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; + + @override + String get biography => 'Biografia'; + + @override + String get countryRegion => 'País ou região'; + + @override + String get thankYou => 'Obrigado!'; + + @override + String get socialMediaLinks => 'Links de mídia social'; + + @override + String get oneUrlPerLine => 'Uma URL por linha.'; + + @override + String get inlineNotation => 'Notação em linha'; + + @override + String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; + + @override + String get clearSavedMoves => 'Limpar lances'; + + @override + String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; + + @override + String get onlinePlayers => 'Jogadores online'; + + @override + String get activePlayers => 'Jogadores ativos'; + + @override + String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; + + @override + String get success => 'Sucesso'; + + @override + String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; + + @override + String get autoSwitch => 'Alternar automaticamente'; + + @override + String get puzzles => 'Quebra-cabeças'; + + @override + String get onlineBots => 'Bots online'; + + @override + String get name => 'Nome'; + + @override + String get description => 'Descrição'; + + @override + String get descPrivate => 'Descrição privada'; + + @override + String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; + + @override + String get no => 'Não'; + + @override + String get yes => 'Sim'; + + @override + String get website => 'Site'; + + @override + String get mobile => 'Celular'; + + @override + String get help => 'Ajuda:'; + + @override + String get createANewTopic => 'Criar novo tópico'; + + @override + String get topics => 'Tópicos'; + + @override + String get posts => 'Publicações'; + + @override + String get lastPost => 'Última postagem'; + + @override + String get views => 'Visualizações'; + + @override + String get replies => 'Respostas'; + + @override + String get replyToThisTopic => 'Responder a este tópico'; + + @override + String get reply => 'Responder'; + + @override + String get message => 'Mensagem'; + + @override + String get createTheTopic => 'Criar tópico'; + + @override + String get reportAUser => 'Reportar um usuário'; + + @override + String get user => 'Usuário'; + + @override + String get reason => 'Motivo'; + + @override + String get whatIsIheMatter => 'Qual é o motivo?'; + + @override + String get cheat => 'Trapaça'; + + @override + String get troll => 'Troll'; + + @override + String get other => 'Outro'; + + @override + String get reportCheatBoostHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão.'; + + @override + String get reportUsernameHelp => 'Explique porque este nome de usuário é ofensivo. Não diga apenas \"é ofensivo/inapropriado\", mas nos diga como chegou a essa conclusão especialmente se o insulto for ofuscado, não estiver em inglês, for uma gíria, ou for uma referência histórica/cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'A sua denúncia será processada mais rápido se for escrita em inglês.'; + + @override + String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; + + @override + String by(String param) { + return 'por $param'; + } + + @override + String importedByX(String param) { + return 'Importado por $param'; + } + + @override + String get thisTopicIsNowClosed => 'O tópico foi fechado.'; + + @override + String get blog => 'Blog'; + + @override + String get notes => 'Notas'; + + @override + String get typePrivateNotesHere => 'Digite notas pessoais aqui'; + + @override + String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; + + @override + String get noNoteYet => 'Nenhuma nota'; + + @override + String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; + + @override + String get incorrectPassword => 'Senha incorreta'; + + @override + String get invalidAuthenticationCode => 'Código de verificação inválido'; + + @override + String get emailMeALink => 'Me envie um link'; + + @override + String get currentPassword => 'Senha atual'; + + @override + String get newPassword => 'Nova senha'; + + @override + String get newPasswordAgain => 'Nova senha (novamente)'; + + @override + String get newPasswordsDontMatch => 'As novas senhas não correspondem'; + + @override + String get newPasswordStrength => 'Senha forte'; + + @override + String get clockInitialTime => 'Tempo inicial no relógio'; + + @override + String get clockIncrement => 'Incremento do relógio'; + + @override + String get privacy => 'Privacidade'; + + @override + String get privacyPolicy => 'Política de privacidade'; + + @override + String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; + + @override + String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; + + @override + String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; + + @override + String get sound => 'Som'; + + @override + String get none => 'Nenhum'; + + @override + String get fast => 'Rápido'; + + @override + String get normal => 'Normal'; + + @override + String get slow => 'Lento'; + + @override + String get insideTheBoard => 'Dentro do tabuleiro'; + + @override + String get outsideTheBoard => 'Fora do tabuleiro'; + + @override + String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; + + @override + String get onSlowGames => 'Em partidas lentas'; + + @override + String get always => 'Sempre'; + + @override + String get never => 'Nunca'; + + @override + String xCompetesInY(String param1, String param2) { + return '$param1 compete em $param2'; + } + + @override + String get victory => 'Vitória'; + + @override + String get defeat => 'Derrota'; + + @override + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } + + @override + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } + + @override + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } + + @override + String get timeline => 'Linha do tempo'; + + @override + String get starting => 'Iniciando:'; + + @override + String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; + + @override + String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; + + @override + String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; + + @override + String get human => 'Humano'; + + @override + String get computer => 'Computador'; + + @override + String get side => 'Cor'; + + @override + String get clock => 'Relógio'; + + @override + String get opponent => 'Adversário'; + + @override + String get learnMenu => 'Aprender'; + + @override + String get studyMenu => 'Estudar'; + + @override + String get practice => 'Praticar'; + + @override + String get community => 'Comunidade'; + + @override + String get tools => 'Ferramentas'; + + @override + String get increment => 'Incremento'; + + @override + String get error_unknown => 'Valor inválido'; + + @override + String get error_required => 'Este campo deve ser preenchido'; + + @override + String get error_email => 'Este endereço de e-mail é inválido'; + + @override + String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; + + @override + String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; + + @override + String get error_email_different => 'Este já é o seu endereço de e-mail'; + + @override + String error_minLength(String param) { + return 'O mínimo de caracteres é $param'; + } + + @override + String error_maxLength(String param) { + return 'O máximo de caracteres é $param'; + } + + @override + String error_min(String param) { + return 'Deve ser maior ou igual a $param'; + } + + @override + String error_max(String param) { + return 'Deve ser menor ou igual a $param'; + } + + @override + String ifRatingIsPlusMinusX(String param) { + return 'Se o rating for ± $param'; + } + + @override + String get ifRegistered => 'Se registrado'; + + @override + String get onlyExistingConversations => 'Apenas conversas iniciadas'; + + @override + String get onlyFriends => 'Apenas amigos'; + + @override + String get menu => 'Menu'; + + @override + String get castling => 'Roque'; + + @override + String get whiteCastlingKingside => 'O-O das brancas'; + + @override + String get blackCastlingKingside => 'O-O das pretas'; + + @override + String tpTimeSpentPlaying(String param) { + return 'Tempo jogando: $param'; + } + + @override + String get watchGames => 'Assistir partidas'; + + @override + String tpTimeSpentOnTV(String param) { + return 'Tempo na TV: $param'; + } + + @override + String get watch => 'Assistir'; + + @override + String get videoLibrary => 'Vídeos'; + + @override + String get streamersMenu => 'Streamers'; + + @override + String get mobileApp => 'Aplicativo Móvel'; + + @override + String get webmasters => 'Webmasters'; + + @override + String get about => 'Sobre'; + + @override + String aboutX(String param) { + return 'Sobre o $param'; + } + + @override + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; + } + + @override + String get really => 'realmente'; + + @override + String get contribute => 'Contribuir'; + + @override + String get termsOfService => 'Termos de serviço'; + + @override + String get sourceCode => 'Código-fonte'; + + @override + String get simultaneousExhibitions => 'Exibição simultânea'; + + @override + String get host => 'Simultanista'; + + @override + String hostColorX(String param) { + return 'Cor do simultanista: $param'; + } + + @override + String get yourPendingSimuls => 'Suas simultâneas pendentes'; + + @override + String get createdSimuls => 'Simultâneas criadas recentemente'; + + @override + String get hostANewSimul => 'Iniciar nova simultânea'; + + @override + String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; + + @override + String get noSimulFound => 'Simultânea não encontrada'; + + @override + String get noSimulExplanation => 'Esta exibição simultânea não existe.'; + + @override + String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; + + @override + String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; + + @override + String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + + @override + String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; + + @override + String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; + + @override + String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; + + @override + String get create => 'Criar'; + + @override + String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; + + @override + String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; + + @override + String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; + + @override + String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; + + @override + String get simulHostExtraTime => 'Tempo adicional do simultanista'; + + @override + String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; + + @override + String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; + + @override + String get lichessTournaments => 'Torneios do Lichess'; + + @override + String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; + + @override + String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; + + @override + String get averageCentipawnLoss => 'Perda média em centipeões'; + + @override + String get accuracy => 'Precisão'; + + @override + String get keyboardShortcuts => 'Atalhos de teclado'; + + @override + String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; + + @override + String get keyGoToStartOrEnd => 'ir para início/fim'; + + @override + String get keyCycleSelectedVariation => 'Alternar entre as variantes'; + + @override + String get keyShowOrHideComments => 'mostrar/ocultar comentários'; + + @override + String get keyEnterOrExitVariation => 'entrar/sair da variante'; + + @override + String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; + + @override + String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; + + @override + String get keyNextBlunder => 'Próximo erro grave'; + + @override + String get keyNextMistake => 'Próximo erro'; + + @override + String get keyNextInaccuracy => 'Próxima imprecisão'; + + @override + String get keyPreviousBranch => 'Branch anterior'; + + @override + String get keyNextBranch => 'Próximo branch'; + + @override + String get toggleVariationArrows => 'Ativar/desativar setas'; + + @override + String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; + + @override + String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; + + @override + String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; + + @override + String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; + + @override + String get playSelectedMove => 'jogar movimento selecionado'; + + @override + String get newTournament => 'Novo torneio'; + + @override + String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; + + @override + String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; + + @override + String get tournamentNotFound => 'Torneio não encontrado'; + + @override + String get tournamentDoesNotExist => 'Este torneio não existe.'; + + @override + String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; + + @override + String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return 'Distribuição mensal de rating em $param'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return 'Seu rating em $param1 é $param2.'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return 'Você é melhor que $param1 dos jogadores de $param2.'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1 é melhor que $param2 dos $param3 jogadores.'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return 'Melhor que $param1 dos jogadores de $param2'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return 'Você não tem rating definido em $param.'; + } + + @override + String get yourRating => 'Seu rating'; + + @override + String get cumulative => 'Cumulativo'; + + @override + String get glicko2Rating => 'Rating Glicko-2'; + + @override + String get checkYourEmail => 'Verifique seu e-mail'; + + @override + String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return 'Ao registrar, você concorda em se comprometer com nossa $param.'; + } + + @override + String readAboutOur(String param) { + return 'Leia sobre a nossa $param.'; + } + + @override + String get networkLagBetweenYouAndLichess => 'Atraso na rede'; + + @override + String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; + + @override + String get downloadAnnotated => 'Baixar anotação'; + + @override + String get downloadRaw => 'Baixar texto'; + + @override + String get downloadImported => 'Baixar partida importada'; + + @override + String get crosstable => 'Tabela'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; + + @override + String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; + + @override + String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; + + @override + String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; + + @override + String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; + + @override + String get shareYourInsightsData => 'Compartilhe seus dados da análise'; + + @override + String get withNobody => 'Com ninguém'; + + @override + String get withFriends => 'Com amigos'; + + @override + String get withEverybody => 'Com todos'; + + @override + String get kidMode => 'Modo infantil'; + + @override + String get kidModeIsEnabled => 'O modo infantil está ativado.'; + + @override + String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; + + @override + String get enableKidMode => 'Habilitar o modo infantil'; + + @override + String get disableKidMode => 'Desabilitar o modo infantil'; + + @override + String get security => 'Segurança'; + + @override + String get sessions => 'Sessões'; + + @override + String get revokeAllSessions => 'revogar todas as sessões'; + + @override + String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; + + @override + String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; + + @override + String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; + + @override + String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; + + @override + String get zeroAdvertisement => 'Zero anúncios'; + + @override + String get fullFeatured => 'Cheio de recursos'; + + @override + String get phoneAndTablet => 'Celular e tablet'; + + @override + String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + + @override + String get correspondenceChess => 'Xadrez por correspondência'; + + @override + String get onlineAndOfflinePlay => 'Jogue online e offline'; + + @override + String get viewTheSolution => 'Ver solução'; + + @override + String get followAndChallengeFriends => 'Siga e desafie amigos'; + + @override + String get gameAnalysis => 'Análise da partida'; + + @override + String xHostsY(String param1, String param2) { + return '$param1 criou $param2'; + } @override String xJoinsY(String param1, String param2) { @@ -8154,1301 +9363,1988 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String xLikesY(String param1, String param2) { - return '$param1 gostou de $param2'; + String xLikesY(String param1, String param2) { + return '$param1 gostou de $param2'; + } + + @override + String get quickPairing => 'Pareamento rápido'; + + @override + String get lobby => 'Salão'; + + @override + String get anonymous => 'Anônimo'; + + @override + String yourScore(String param) { + return 'Sua pontuação:$param'; + } + + @override + String get language => 'Idioma'; + + @override + String get background => 'Cor tema'; + + @override + String get light => 'Claro'; + + @override + String get dark => 'Escuro'; + + @override + String get transparent => 'Transparente'; + + @override + String get deviceTheme => 'Tema do dispositivo'; + + @override + String get backgroundImageUrl => 'URL da imagem de fundo:'; + + @override + String get board => 'Tabuleiro'; + + @override + String get size => 'Tamanho'; + + @override + String get opacity => 'Opacidade'; + + @override + String get brightness => 'Brilho'; + + @override + String get hue => 'Tom'; + + @override + String get boardReset => 'Restaurar as cores padrão'; + + @override + String get pieceSet => 'Estilo das peças'; + + @override + String get embedInYourWebsite => 'Incorporar no seu site'; + + @override + String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; + + @override + String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; + + @override + String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; + + @override + String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; + + @override + String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; + + @override + String get playChessInStyle => 'Jogue xadrez com estilo'; + + @override + String get chessBasics => 'Básicos do xadrez'; + + @override + String get coaches => 'Treinadores'; + + @override + String get invalidPgn => 'PGN inválido'; + + @override + String get invalidFen => 'FEN inválido'; + + @override + String get custom => 'Personalizado'; + + @override + String get notifications => 'Notificações'; + + @override + String notificationsX(String param1) { + return 'Notificações: $param1'; + } + + @override + String perfRatingX(String param) { + return 'Rating: $param'; + } + + @override + String get practiceWithComputer => 'Pratique com o computador'; + + @override + String anotherWasX(String param) { + return 'Um outro lance seria $param'; + } + + @override + String bestWasX(String param) { + return 'Melhor seria $param'; + } + + @override + String get youBrowsedAway => 'Você navegou para longe'; + + @override + String get resumePractice => 'Retornar à prática'; + + @override + String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; + + @override + String get theGameIsADraw => 'A partida terminou em empate.'; + + @override + String get computerThinking => 'Computador pensando ...'; + + @override + String get seeBestMove => 'Veja o melhor lance'; + + @override + String get hideBestMove => 'Esconder o melhor lance'; + + @override + String get getAHint => 'Obter uma dica'; + + @override + String get evaluatingYourMove => 'Avaliando o seu movimento ...'; + + @override + String get whiteWinsGame => 'Brancas vencem'; + + @override + String get blackWinsGame => 'Pretas vencem'; + + @override + String get learnFromYourMistakes => 'Aprenda com seus erros'; + + @override + String get learnFromThisMistake => 'Aprenda com este erro'; + + @override + String get skipThisMove => 'Pular esse lance'; + + @override + String get next => 'Próximo'; + + @override + String xWasPlayed(String param) { + return '$param foi jogado'; + } + + @override + String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; + + @override + String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; + + @override + String get resumeLearning => 'Continuar a aprendizagem'; + + @override + String get youCanDoBetter => 'Você pode fazer melhor'; + + @override + String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; + + @override + String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; + + @override + String get solution => 'Solução'; + + @override + String get waitingForAnalysis => 'Aguardando análise'; + + @override + String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; + + @override + String get noMistakesFoundForBlack => 'Nenhum erro encontrado para as Pretas'; + + @override + String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; + + @override + String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; + + @override + String get doItAgain => 'Faça novamente'; + + @override + String get reviewWhiteMistakes => 'Rever erros das Brancas'; + + @override + String get reviewBlackMistakes => 'Rever erros das Pretas'; + + @override + String get advantage => 'Vantagem'; + + @override + String get opening => 'Abertura'; + + @override + String get middlegame => 'Meio-jogo'; + + @override + String get endgame => 'Finais'; + + @override + String get conditionalPremoves => 'Pré-lances condicionais'; + + @override + String get addCurrentVariation => 'Adicionar a variação atual'; + + @override + String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; + + @override + String get noConditionalPremoves => 'Sem pré-lances condicionais'; + + @override + String playX(String param) { + return 'Jogar $param'; + } + + @override + String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; + + @override + String get clickHereToReadIt => 'Clique aqui para ler'; + + @override + String get sorry => 'Desculpa :('; + + @override + String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; + + @override + String get why => 'Por quê?'; + + @override + String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; + + @override + String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; + + @override + String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; + + @override + String get howToAvoidThis => 'Como evitar isso?'; + + @override + String get playEveryGame => 'Jogue todos os jogos que inicia.'; + + @override + String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; + + @override + String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; + + @override + String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; + + @override + String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; + + @override + String get thankYouForReading => 'Obrigado pela leitura!'; + + @override + String get lifetimeScore => 'Pontuação de todo o período'; + + @override + String get currentMatchScore => 'Pontuação da partida atual'; + + @override + String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; + + @override + String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; + + @override + String agreementMultipleAccounts(String param) { + return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; + } + + @override + String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; + + @override + String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; + + @override + String get edit => 'Editar'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => 'Rápida'; + + @override + String get classical => 'Clássico'; + + @override + String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; + + @override + String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; + + @override + String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; + + @override + String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; + + @override + String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; + + @override + String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; + + @override + String get puzzleDesc => 'Treinador de táticas de xadrez'; + + @override + String get important => 'Importante'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return 'A sua pergunta pode já ter sido respondida $param1'; + } + + @override + String get inTheFAQ => 'no F.A.Q.'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; + } + + @override + String get useTheReportForm => 'use o formulário de denúncia'; + + @override + String toRequestSupport(String param1) { + return 'Para solicitar ajuda, $param1'; + } + + @override + String get tryTheContactPage => 'tente a página de contato'; + + @override + String makeSureToRead(String param1) { + return 'Certifique-se de ler $param1'; + } + + @override + String get theForumEtiquette => 'as regras do fórum'; + + @override + String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; + + @override + String joinTheTeamXToPost(String param1) { + return 'Junte-se a $param1 para publicar neste fórum'; + } + + @override + String teamNamedX(String param1) { + return 'Equipe $param1'; + } + + @override + String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; + + @override + String get subscribe => 'Seguir publicações'; + + @override + String get unsubscribe => 'Deixar de seguir publicações'; + + @override + String mentionedYouInX(String param1) { + return 'mencionou você em \"$param1\".'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 mencionou você em \"$param2\".'; + } + + @override + String invitedYouToX(String param1) { + return 'convidou você para \"$param1\".'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 convidou você para \"$param2\".'; + } + + @override + String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; + + @override + String youHaveJoinedTeamX(String param1) { + return 'Você ingressou em \"$param1\".'; + } + + @override + String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; + + @override + String get congratsYouWon => 'Parabéns, você venceu!'; + + @override + String gameVsX(String param1) { + return 'Jogo vs $param1'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return 'Reembolso: $param1 $param2 pontos de rating.'; + } + + @override + String get timeAlmostUp => 'O tempo está quase acabando!'; + + @override + String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; + + @override + String get download => 'Baixar'; + + @override + String get coachManager => 'Configurações para professores'; + + @override + String get streamerManager => 'Configurações para streamers'; + + @override + String get cancelTournament => 'Cancelar o torneio'; + + @override + String get tournDescription => 'Descrição do torneio'; + + @override + String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; + + @override + String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; + + @override + String get onlyMembersOfTeam => 'Apenas membros da equipe'; + + @override + String get noRestriction => 'Sem restrição'; + + @override + String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; + + @override + String get minimumRating => 'Rating mínimo'; + + @override + String get maximumWeeklyRating => 'Rating máxima da semana'; + + @override + String positionInputHelp(String param) { + return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; + } + + @override + String get cancelSimul => 'Cancelar a simultânea'; + + @override + String get simulHostcolor => 'Cor do simultanista em cada jogo'; + + @override + String get estimatedStart => 'Tempo de início estimado'; + + @override + String simulFeatured(String param) { + return 'Compartilhar em $param'; + } + + @override + String simulFeaturedHelp(String param) { + return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; + } + + @override + String get simulDescription => 'Descrição da simultânea'; + + @override + String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; + + @override + String markdownAvailable(String param) { + return '$param está disponível para opções de formatação adicionais.'; + } + + @override + String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; + + @override + String get inYourLocalTimezone => 'No seu próprio fuso horário'; + + @override + String get tournChat => 'Chat do torneio'; + + @override + String get noChat => 'Sem chat'; + + @override + String get onlyTeamLeaders => 'Apenas líderes de equipe'; + + @override + String get onlyTeamMembers => 'Apenas membros da equipe'; + + @override + String get navigateMoveTree => 'Navegar pela notação de movimentos'; + + @override + String get mouseTricks => 'Funcionalidades do mouse'; + + @override + String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; + + @override + String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; + + @override + String get playComputerMove => 'Jogar o melhor lance de computador'; + + @override + String get analysisOptions => 'Opções de análise'; + + @override + String get focusChat => 'Focar texto'; + + @override + String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; + + @override + String get reopenYourAccount => 'Reabra sua conta'; + + @override + String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; + + @override + String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; + + @override + String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; + + @override + String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; + + @override + String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; + + @override + String get tournamentEntryCode => 'Código de entrada do torneio'; + + @override + String get hangOn => 'Espere!'; + + @override + String gameInProgress(String param) { + return 'Você tem uma partida em andamento com $param.'; + } + + @override + String get abortTheGame => 'Cancelar a partida'; + + @override + String get resignTheGame => 'Abandonar a partida'; + + @override + String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; + + @override + String get since => 'Desde'; + + @override + String get until => 'Até'; + + @override + String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; + + @override + String get switchSides => 'Trocar de lado'; + + @override + String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; + + @override + String get ourEventTips => 'Nossas dicas para organização de eventos'; + + @override + String get instructions => 'Instruções'; + + @override + String get showMeEverything => 'Mostrar tudo'; + + @override + String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; + + @override + String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + + @override + String get stats => 'Estatísticas'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', + one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Mate em $count lances', + one: 'Mate em $count lance', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capivaradas', + one: '$count capivarada', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count erros', + one: '$count erro', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count imprecisões', + one: '$count imprecisão', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogadores conectados', + one: '$count jogadores conectados', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas', + one: '$count partida', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Rating $count após $param2 partidas', + one: 'Rating $count após $param2 partida', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Favoritos', + one: '$count Favoritos', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dias', + one: '$count dias', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas', + one: '$count horas', + ); + return '$_temp0'; + } + + @override + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos', + one: '$count minuto', + ); + return '$_temp0'; + } + + @override + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'O ranking é atualizado a cada $count minutos', + one: 'O ranking é atualizado a cada $count minuto', + ); + return '$_temp0'; + } + + @override + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count problemas', + one: '$count quebra-cabeça', + ); + return '$_temp0'; + } + + @override + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas contra você', + one: '$count partidas contra você', + ); + return '$_temp0'; + } + + @override + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count valendo pontos', + one: '$count valendo pontos', + ); + return '$_temp0'; + } + + @override + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vitórias', + one: '$count vitória', + ); + return '$_temp0'; + } + + @override + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count derrotas', + one: '$count derrota', + ); + return '$_temp0'; } @override - String get quickPairing => 'Pareamento rápido'; + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count empates', + one: '$count empates', + ); + return '$_temp0'; + } @override - String get lobby => 'Salão'; + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogando', + one: '$count jogando', + ); + return '$_temp0'; + } @override - String get anonymous => 'Anônimo'; + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dar $count segundos', + one: 'Dar $count segundos', + ); + return '$_temp0'; + } @override - String yourScore(String param) { - return 'Sua pontuação:$param'; + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos de torneio', + one: '$count ponto de torneio', + ); + return '$_temp0'; } @override - String get language => 'Idioma'; + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count estudos', + one: '$count estudo', + ); + return '$_temp0'; + } @override - String get background => 'Cor tema'; + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count simultâneas', + one: '$count simultânea', + ); + return '$_temp0'; + } @override - String get light => 'Claro'; + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count jogos valendo pontos', + one: '≥ $count jogos valendo pontos', + ); + return '$_temp0'; + } @override - String get dark => 'Escuro'; + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 partidas valendo pontos', + one: '≥ $count partida $param2 valendo pontos', + ); + return '$_temp0'; + } @override - String get transparent => 'Transparente'; + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', + one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', + ); + return '$_temp0'; + } @override - String get deviceTheme => 'Tema do dispositivo'; + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Você precisa jogar ainda $count partidas valendo pontos', + one: 'Você precisa jogar ainda $count partidas valendo pontos', + ); + return '$_temp0'; + } @override - String get backgroundImageUrl => 'URL da imagem de fundo:'; + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count de partidas importadas', + one: '$count de partidas importadas', + ); + return '$_temp0'; + } @override - String get board => 'Tabuleiro'; + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count amigos online', + one: '$count amigo online', + ); + return '$_temp0'; + } @override - String get size => 'Tamanho'; + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seguidores', + one: '$count seguidores', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seguidos', + one: '$count seguidos', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Menos que $count minutos', + one: 'Menos que $count minutos', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas em andamento', + one: '$count partidas em andamento', + ); + return '$_temp0'; + } + + @override + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Máximo: $count caracteres.', + one: 'Máximo: $count caractere.', + ); + return '$_temp0'; + } + + @override + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bloqueados', + one: '$count bloqueado', + ); + return '$_temp0'; + } + + @override + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count publicações no fórum', + one: '$count publicação no fórum', + ); + return '$_temp0'; + } + + @override + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 jogadores nesta semana.', + one: '$count $param2 jogador nesta semana.', + ); + return '$_temp0'; + } + + @override + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Disponível em $count idiomas!', + one: 'Disponível em $count idiomas!', + ); + return '$_temp0'; + } + + @override + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundos para fazer o primeiro lance', + one: '$count segundo para fazer o primeiro lance', + ); + return '$_temp0'; + } + + @override + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundos', + one: '$count segundo', + ); + return '$_temp0'; + } + + @override + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'e salvar as linhas de pré-lance de $count', + one: 'e salvar a linha de pré-lance de $count', + ); + return '$_temp0'; + } @override - String get opacity => 'Opacidade'; + String get stormMoveToStart => 'Mova para começar'; @override - String get brightness => 'Brilho'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; @override - String get hue => 'Tom'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; @override - String get boardReset => 'Restaurar as cores padrão'; + String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; @override - String get pieceSet => 'Estilo das peças'; + String get stormNewDailyHighscore => 'Novo recorde diário!'; @override - String get embedInYourWebsite => 'Incorporar no seu site'; + String get stormNewWeeklyHighscore => 'Novo recorde semanal!'; @override - String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; + String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; @override - String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; + String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; @override - String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; + String stormPreviousHighscoreWasX(String param) { + return 'Recorde anterior era $param'; + } @override - String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; + String get stormPlayAgain => 'Jogar novamente'; @override - String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; + String stormHighscoreX(String param) { + return 'Recorde: $param'; + } @override - String get playChessInStyle => 'Jogue xadrez com estilo'; + String get stormScore => 'Pontuação'; @override - String get chessBasics => 'Básicos do xadrez'; + String get stormMoves => 'Lances'; @override - String get coaches => 'Treinadores'; + String get stormAccuracy => 'Precisão'; @override - String get invalidPgn => 'PGN inválido'; + String get stormCombo => 'Combo'; @override - String get invalidFen => 'FEN inválido'; + String get stormTime => 'Tempo'; @override - String get custom => 'Personalizado'; + String get stormTimePerMove => 'Tempo por lance'; @override - String get notifications => 'Notificações'; + String get stormHighestSolved => 'Classificação mais alta'; @override - String notificationsX(String param1) { - return 'Notificações: $param1'; - } + String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; @override - String perfRatingX(String param) { - return 'Rating: $param'; - } + String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; @override - String get practiceWithComputer => 'Pratique com o computador'; + String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; @override - String anotherWasX(String param) { - return 'Um outro lance seria $param'; - } + String get stormHighscores => 'Melhores pontuações'; @override - String bestWasX(String param) { - return 'Melhor seria $param'; - } + String get stormViewBestRuns => 'Ver melhores séries'; @override - String get youBrowsedAway => 'Você navegou para longe'; + String get stormBestRunOfDay => 'Melhor série do dia'; @override - String get resumePractice => 'Retornar à prática'; + String get stormRuns => 'Séries'; @override - String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; + String get stormGetReady => 'Prepare-se!'; @override - String get theGameIsADraw => 'A partida terminou em empate.'; + String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; @override - String get computerThinking => 'Computador pensando ...'; + String get stormRaceComplete => 'Corrida concluída!'; @override - String get seeBestMove => 'Veja o melhor lance'; + String get stormSpectating => 'Espectando'; @override - String get hideBestMove => 'Esconder o melhor lance'; + String get stormJoinTheRace => 'Entre na corrida!'; @override - String get getAHint => 'Obter uma dica'; + String get stormStartTheRace => 'Começar a corrida'; @override - String get evaluatingYourMove => 'Avaliando o seu movimento ...'; + String stormYourRankX(String param) { + return 'Sua classificação: $param'; + } @override - String get whiteWinsGame => 'Brancas vencem'; + String get stormWaitForRematch => 'Esperando por revanche'; @override - String get blackWinsGame => 'Pretas vencem'; + String get stormNextRace => 'Próxima corrida'; @override - String get learnFromYourMistakes => 'Aprenda com seus erros'; + String get stormJoinRematch => 'Junte-se a revanche'; @override - String get learnFromThisMistake => 'Aprenda com este erro'; + String get stormWaitingToStart => 'Esperando para começar'; @override - String get skipThisMove => 'Pular esse lance'; + String get stormCreateNewGame => 'Criar um novo jogo'; @override - String get next => 'Próximo'; + String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; @override - String xWasPlayed(String param) { - return '$param foi jogado'; - } + String get stormRaceYourFriends => 'Corra contra seus amigos'; @override - String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; + String get stormSkip => 'pular'; @override - String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; + String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; @override - String get resumeLearning => 'Continuar a aprendizagem'; + String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; @override - String get youCanDoBetter => 'Você pode fazer melhor'; + String get stormFailedPuzzles => 'Quebra-cabeças falhados'; @override - String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; + String get stormSlowPuzzles => 'Quebra-cabeças lentos'; @override - String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; + String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; @override - String get solution => 'Solução'; + String get stormThisWeek => 'Essa semana'; @override - String get waitingForAnalysis => 'Aguardando análise'; + String get stormThisMonth => 'Esse mês'; @override - String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; + String get stormAllTime => 'Desde o início'; @override - String get noMistakesFoundForBlack => 'Nenhum erro encontrado para as Pretas'; + String get stormClickToReload => 'Clique para recarregar'; @override - String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; + String get stormThisRunHasExpired => 'Esta corrida acabou!'; @override - String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; + String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; @override - String get doItAgain => 'Faça novamente'; + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count séries', + one: '1 tentativa', + ); + return '$_temp0'; + } @override - String get reviewWhiteMistakes => 'Rever erros das Brancas'; + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count tentativas de $param2', + one: 'Jogou uma tentativa de $param2', + ); + return '$_temp0'; + } @override - String get reviewBlackMistakes => 'Rever erros das Pretas'; + String get streamerLichessStreamers => 'Streamers do Lichess'; @override - String get advantage => 'Vantagem'; + String get studyPrivate => 'Privado'; @override - String get opening => 'Abertura'; + String get studyMyStudies => 'Meus estudos'; @override - String get middlegame => 'Meio-jogo'; + String get studyStudiesIContributeTo => 'Estudos para os quais contribuí'; @override - String get endgame => 'Finais'; + String get studyMyPublicStudies => 'Meus estudos públicos'; @override - String get conditionalPremoves => 'Pré-lances condicionais'; + String get studyMyPrivateStudies => 'Meus estudos privados'; @override - String get addCurrentVariation => 'Adicionar a variação atual'; + String get studyMyFavoriteStudies => 'Meus estudos favoritos'; @override - String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; + String get studyWhatAreStudies => 'O que são estudos?'; @override - String get noConditionalPremoves => 'Sem pré-lances condicionais'; + String get studyAllStudies => 'Todos os estudos'; @override - String playX(String param) { - return 'Jogar $param'; + String studyStudiesCreatedByX(String param) { + return 'Estudos criados por $param'; } @override - String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; - - @override - String get clickHereToReadIt => 'Clique aqui para ler'; + String get studyNoneYet => 'Nenhum ainda.'; @override - String get sorry => 'Desculpa :('; + String get studyHot => 'Em alta'; @override - String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; + String get studyDateAddedNewest => 'Data de criação (mais recente)'; @override - String get why => 'Por quê?'; + String get studyDateAddedOldest => 'Data de criação (mais antiga)'; @override - String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; + String get studyRecentlyUpdated => 'Atualizado recentemente'; @override - String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; + String get studyMostPopular => 'Mais populares'; @override - String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; + String get studyAlphabetical => 'Em ordem alfabética'; @override - String get howToAvoidThis => 'Como evitar isso?'; + String get studyAddNewChapter => 'Adicionar um novo capítulo'; @override - String get playEveryGame => 'Jogue todos os jogos que inicia.'; + String get studyAddMembers => 'Adicionar membros'; @override - String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; + String get studyInviteToTheStudy => 'Convidar para o estudo'; @override - String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convide apenas pessoas que você conhece e que queiram participar efetivamente deste estudo.'; @override - String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; + String get studySearchByUsername => 'Pesquisar por nome de usuário'; @override - String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; + String get studySpectator => 'Espectador'; @override - String get thankYouForReading => 'Obrigado pela leitura!'; + String get studyContributor => 'Colaborador'; @override - String get lifetimeScore => 'Pontuação de todo o período'; + String get studyKick => 'Expulsar'; @override - String get currentMatchScore => 'Pontuação da partida atual'; + String get studyLeaveTheStudy => 'Sair deste estudo'; @override - String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; + String get studyYouAreNowAContributor => 'Agora você é um(a) colaborador(a)'; @override - String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; + String get studyYouAreNowASpectator => 'Você agora é um(a) espectador(a)'; @override - String agreementMultipleAccounts(String param) { - return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; - } + String get studyPgnTags => 'Etiquetas PGN'; @override - String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; + String get studyLike => 'Gostei'; @override - String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; + String get studyUnlike => 'Não gostei'; @override - String get edit => 'Editar'; + String get studyNewTag => 'Nova etiqueta'; @override - String get bullet => 'Bullet'; + String get studyCommentThisPosition => 'Comente sobre esta posição'; @override - String get blitz => 'Blitz'; + String get studyCommentThisMove => 'Comente sobre este lance'; @override - String get rapid => 'Rápida'; + String get studyAnnotateWithGlyphs => 'Anotar com símbolos'; @override - String get classical => 'Clássico'; + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é muito curto para ser analisado.'; @override - String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; + String get studyOnlyContributorsCanRequestAnalysis => 'Apenas os colaboradores de um estudo podem solicitar uma análise de computador.'; @override - String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; + String get studyGetAFullComputerAnalysis => 'Obter uma análise completa dos movimentos pelo servidor.'; @override - String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; + String get studyMakeSureTheChapterIsComplete => 'Certifique-se de que o capítulo está completo. Você só pode solicitar análise uma vez.'; @override - String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros em SYNC veem a mesma posição'; @override - String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; + String get studyShareChanges => 'Compartilhar as alterações com os espectadores e salvá-las no servidor'; @override - String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; + String get studyPlaying => 'Jogando'; @override - String get puzzleDesc => 'Treinador de táticas de xadrez'; + String get studyShowEvalBar => 'Barras de avaliação'; @override - String get important => 'Importante'; + String get studyFirst => 'Primeiro'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return 'A sua pergunta pode já ter sido respondida $param1'; - } + String get studyPrevious => 'Anterior'; @override - String get inTheFAQ => 'no F.A.Q.'; + String get studyNext => 'Próximo'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; - } + String get studyLast => 'Último'; @override - String get useTheReportForm => 'use o formulário de denúncia'; + String get studyShareAndExport => 'Compartilhar & exportar'; @override - String toRequestSupport(String param1) { - return 'Para solicitar ajuda, $param1'; - } + String get studyCloneStudy => 'Duplicar'; @override - String get tryTheContactPage => 'tente a página de contato'; + String get studyStudyPgn => 'PGN de estudo'; @override - String makeSureToRead(String param1) { - return 'Certifique-se de ler $param1'; - } + String get studyDownloadAllGames => 'Baixar todas as partidas'; @override - String get theForumEtiquette => 'as regras do fórum'; + String get studyChapterPgn => 'PGN do capítulo'; @override - String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; + String get studyCopyChapterPgn => 'Copiar PGN'; @override - String joinTheTeamXToPost(String param1) { - return 'Junte-se a $param1 para publicar neste fórum'; - } + String get studyDownloadGame => 'Baixar partida'; @override - String teamNamedX(String param1) { - return 'Equipe $param1'; - } + String get studyStudyUrl => 'URL de estudo'; @override - String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; + String get studyCurrentChapterUrl => 'URL do capítulo atual'; @override - String get subscribe => 'Seguir publicações'; + String get studyYouCanPasteThisInTheForumToEmbed => 'Você pode colar isso no fórum para incluir o estudo na publicação'; @override - String get unsubscribe => 'Deixar de seguir publicações'; + String get studyStartAtInitialPosition => 'Começar na posição inicial'; @override - String mentionedYouInX(String param1) { - return 'mencionou você em \"$param1\".'; + String studyStartAtX(String param) { + return 'Começar em $param'; } @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 mencionou você em \"$param2\".'; - } + String get studyEmbedInYourWebsite => 'Incorporar em seu site ou blog'; @override - String invitedYouToX(String param1) { - return 'convidou você para \"$param1\".'; - } + String get studyReadMoreAboutEmbedding => 'Leia mais sobre como incorporar'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 convidou você para \"$param2\".'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Apenas os estudos públicos podem ser incorporados!'; @override - String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; + String get studyOpen => 'Abertura'; @override - String youHaveJoinedTeamX(String param1) { - return 'Você ingressou em \"$param1\".'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, disponibilizado por $param2'; } @override - String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; + String get studyStudyNotFound => 'Estudo não encontrado'; @override - String get congratsYouWon => 'Parabéns, você venceu!'; + String get studyEditChapter => 'Editar capítulo'; @override - String gameVsX(String param1) { - return 'Jogo vs $param1'; - } + String get studyNewChapter => 'Novo capítulo'; @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; + String studyImportFromChapterX(String param) { + return 'Importar de $param'; } @override - String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; + String get studyOrientation => 'Orientação'; @override - String refundXpointsTimeControlY(String param1, String param2) { - return 'Reembolso: $param1 $param2 pontos de rating.'; - } + String get studyAnalysisMode => 'Modo de análise'; @override - String get timeAlmostUp => 'O tempo está quase acabando!'; + String get studyPinnedChapterComment => 'Comentário de capítulo afixado'; @override - String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; + String get studySaveChapter => 'Salvar capítulo'; @override - String get download => 'Baixar'; + String get studyClearAnnotations => 'Remover anotações'; @override - String get coachManager => 'Configurações para professores'; + String get studyClearVariations => 'Limpar variantes'; @override - String get streamerManager => 'Configurações para streamers'; + String get studyDeleteChapter => 'Excluir capítulo'; @override - String get cancelTournament => 'Cancelar o torneio'; + String get studyDeleteThisChapter => 'Excluir este capítulo? Não há volta!'; @override - String get tournDescription => 'Descrição do torneio'; + String get studyClearAllCommentsInThisChapter => 'Remover todos os comentários e formas deste capítulo?'; @override - String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; + String get studyRightUnderTheBoard => 'Logo abaixo do tabuleiro'; @override - String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; + String get studyNoPinnedComment => 'Nenhum'; @override - String get onlyMembersOfTeam => 'Apenas membros da equipe'; + String get studyNormalAnalysis => 'Análise normal'; @override - String get noRestriction => 'Sem restrição'; + String get studyHideNextMoves => 'Ocultar próximos movimentos'; @override - String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; + String get studyInteractiveLesson => 'Lição interativa'; @override - String get minimumRating => 'Rating mínimo'; + String studyChapterX(String param) { + return 'Capítulo $param'; + } @override - String get maximumWeeklyRating => 'Rating máxima da semana'; + String get studyEmpty => 'Vazio'; @override - String positionInputHelp(String param) { - return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; - } + String get studyStartFromInitialPosition => 'Reiniciar para posição inicial'; @override - String get cancelSimul => 'Cancelar a simultânea'; + String get studyEditor => 'Editor'; @override - String get simulHostcolor => 'Cor do simultanista em cada jogo'; + String get studyStartFromCustomPosition => 'Iniciar com posição personalizada'; @override - String get estimatedStart => 'Tempo de início estimado'; + String get studyLoadAGameByUrl => 'Carregar um jogo por URL'; @override - String simulFeatured(String param) { - return 'Compartilhar em $param'; - } + String get studyLoadAPositionFromFen => 'Carregar uma posição com FEN'; @override - String simulFeaturedHelp(String param) { - return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; - } + String get studyLoadAGameFromPgn => 'Carregar um jogo com PGN'; @override - String get simulDescription => 'Descrição da simultânea'; + String get studyAutomatic => 'Automático'; @override - String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; + String get studyUrlOfTheGame => 'URL do jogo'; @override - String markdownAvailable(String param) { - return '$param está disponível para opções de formatação adicionais.'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar um jogo de $param1 ou $param2'; } @override - String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; + String get studyCreateChapter => 'Criar capítulo'; @override - String get inYourLocalTimezone => 'No seu próprio fuso horário'; + String get studyCreateStudy => 'Criar estudo'; @override - String get tournChat => 'Chat do torneio'; + String get studyEditStudy => 'Editar estudo'; @override - String get noChat => 'Sem chat'; + String get studyVisibility => 'Visibilidade'; @override - String get onlyTeamLeaders => 'Apenas líderes de equipe'; + String get studyPublic => 'Público'; @override - String get onlyTeamMembers => 'Apenas membros da equipe'; + String get studyUnlisted => 'Não listado'; @override - String get navigateMoveTree => 'Navegar pela notação de movimentos'; + String get studyInviteOnly => 'Apenas por convite'; @override - String get mouseTricks => 'Funcionalidades do mouse'; + String get studyAllowCloning => 'Permitir clonagem'; @override - String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; + String get studyNobody => 'Ninguém'; @override - String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; + String get studyOnlyMe => 'Apenas eu'; @override - String get playComputerMove => 'Jogar o melhor lance de computador'; + String get studyContributors => 'Colaboradores'; @override - String get analysisOptions => 'Opções de análise'; + String get studyMembers => 'Membros'; @override - String get focusChat => 'Focar texto'; + String get studyEveryone => 'Todos'; @override - String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; + String get studyEnableSync => 'Ativar sincronização'; @override - String get reopenYourAccount => 'Reabra sua conta'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Sim: mantenha todos na mesma posição'; @override - String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; + String get studyNoLetPeopleBrowseFreely => 'Não: deixe as pessoas navegarem livremente'; @override - String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; + String get studyPinnedStudyComment => 'Comentário de estudo afixado'; @override - String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; + String get studyStart => 'Iniciar'; @override - String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; + String get studySave => 'Salvar'; @override - String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; + String get studyClearChat => 'Limpar conversação'; @override - String get tournamentEntryCode => 'Código de entrada do torneio'; + String get studyDeleteTheStudyChatHistory => 'Excluir o histórico de conversação do estudo? Não há volta!'; @override - String get hangOn => 'Espere!'; + String get studyDeleteStudy => 'Excluir estudo'; @override - String gameInProgress(String param) { - return 'Você tem uma partida em andamento com $param.'; + String studyConfirmDeleteStudy(String param) { + return 'Excluir todo o estudo? Não há volta! Digite o nome do estudo para confirmar: $param'; } @override - String get abortTheGame => 'Cancelar a partida'; + String get studyWhereDoYouWantToStudyThat => 'Onde você quer estudar?'; @override - String get resignTheGame => 'Abandonar a partida'; + String get studyGoodMove => 'Boa jogada'; @override - String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; + String get studyMistake => 'Erro'; @override - String get since => 'Desde'; + String get studyBrilliantMove => 'Jogada excelente'; @override - String get until => 'Até'; + String get studyBlunder => 'Erro grave'; @override - String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; + String get studyInterestingMove => 'Jogada interessante'; @override - String get switchSides => 'Trocar de lado'; + String get studyDubiousMove => 'Lance questionável'; @override - String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; + String get studyOnlyMove => 'Única jogada'; @override - String get ourEventTips => 'Nossas dicas para organização de eventos'; + String get studyZugzwang => 'Zugzwang'; @override - String get instructions => 'Instruções'; + String get studyEqualPosition => 'Posição igual'; @override - String get showMeEverything => 'Mostrar tudo'; + String get studyUnclearPosition => 'Posição incerta'; @override - String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; + String get studyWhiteIsSlightlyBetter => 'Brancas estão um pouco melhor'; @override - String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + String get studyBlackIsSlightlyBetter => 'Pretas estão um pouco melhor'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', - one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', - ); - return '$_temp0'; - } + String get studyWhiteIsBetter => 'Brancas estão melhor'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Mate em $count lances', - one: 'Mate em $count lance', - ); - return '$_temp0'; - } + String get studyBlackIsBetter => 'Pretas estão melhor'; @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count capivaradas', - one: '$count capivarada', - ); - return '$_temp0'; - } + String get studyWhiteIsWinning => 'Brancas estão ganhando'; @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count erros', - one: '$count erro', - ); - return '$_temp0'; - } + String get studyBlackIsWinning => 'Pretas estão ganhando'; @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count imprecisões', - one: '$count imprecisão', - ); - return '$_temp0'; - } + String get studyNovelty => 'Novidade'; @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogadores conectados', - one: '$count jogadores conectados', - ); - return '$_temp0'; - } + String get studyDevelopment => 'Desenvolvimento'; @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count partidas', - one: '$count partida', - ); - return '$_temp0'; - } + String get studyInitiative => 'Iniciativa'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Rating $count após $param2 partidas', - one: 'Rating $count em $param2 jogo', - ); - return '$_temp0'; - } + String get studyAttack => 'Ataque'; @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count Favoritos', - one: '$count Favoritos', - ); - return '$_temp0'; - } + String get studyCounterplay => 'Contra-ataque'; @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count dias', - one: '$count dias', - ); - return '$_temp0'; - } + String get studyTimeTrouble => 'Problema de tempo'; @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count horas', - one: '$count horas', - ); - return '$_temp0'; - } + String get studyWithCompensation => 'Com compensação'; @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count minutos', - one: '$count minuto', - ); - return '$_temp0'; - } + String get studyWithTheIdea => 'Com a ideia'; @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'O ranking é atualizado a cada $count minutos', - one: 'O ranking é atualizado a cada $count minutos', - ); - return '$_temp0'; - } + String get studyNextChapter => 'Próximo capítulo'; @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count problemas', - one: '$count quebra-cabeça', - ); - return '$_temp0'; - } + String get studyPrevChapter => 'Capítulo anterior'; @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count partidas contra você', - one: '$count partidas contra você', - ); - return '$_temp0'; - } + String get studyStudyActions => 'Opções de estudo'; @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count valendo pontos', - one: '$count valendo pontos', - ); - return '$_temp0'; - } + String get studyTopics => 'Tópicos'; @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count vitórias', - one: '$count vitória', - ); - return '$_temp0'; - } + String get studyMyTopics => 'Meus tópicos'; @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count derrotas', - one: '$count derrota', - ); - return '$_temp0'; - } + String get studyPopularTopics => 'Tópicos populares'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count empates', - one: '$count empates', - ); - return '$_temp0'; - } + String get studyManageTopics => 'Gerenciar tópicos'; @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogando', - one: '$count jogando', - ); - return '$_temp0'; - } + String get studyBack => 'Voltar'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Dar $count segundos', - one: 'Dar $count segundos', - ); - return '$_temp0'; - } + String get studyPlayAgain => 'Jogar novamente'; - @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos de torneio', - one: '$count ponto de torneio', - ); - return '$_temp0'; - } + @override + String get studyWhatWouldYouPlay => 'O que você jogaria nessa posição?'; @override - String nbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count estudos', - one: '$count estudo', - ); - return '$_temp0'; - } + String get studyYouCompletedThisLesson => 'Parabéns! Você completou essa lição.'; @override - String nbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count simultâneas', - one: '$count simultânea', - ); - return '$_temp0'; + String studyPerPage(String param) { + return '$param por página'; } @override - String moreThanNbRatedGames(int count) { + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count jogos valendo pontos', - one: '≥ $count jogos valendo pontos', + other: '$count Capítulos', + one: '$count Capítulo', ); return '$_temp0'; } @override - String moreThanNbPerfRatedGames(int count, String param2) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count $param2 partidas valendo pontos', - one: '≥ $count partida $param2 valendo pontos', + other: '$count Jogos', + one: '$count Jogo', ); return '$_temp0'; } @override - String needNbMorePerfGames(int count, String param2) { + String studyNbMembers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', - one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', + other: '$count Membros', + one: '$count Membro', ); return '$_temp0'; } @override - String needNbMoreGames(int count) { + String studyPasteYourPgnTextHereUpToNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Você precisa jogar ainda $count partidas valendo pontos', - one: 'Você precisa jogar ainda $count partidas valendo pontos', + other: 'Cole seu texto PGN aqui, até $count jogos', + one: 'Cole seu texto PGN aqui, até $count jogo', ); return '$_temp0'; } @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count de partidas importadas', - one: '$count de partidas importadas', - ); - return '$_temp0'; - } + String get timeagoJustNow => 'agora há pouco'; @override - String nbFriendsOnline(int count) { + String get timeagoRightNow => 'agora mesmo'; + + @override + String get timeagoCompleted => 'concluído'; + + @override + String timeagoInNbSeconds(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count amigos online', - one: '$count amigo online', + other: 'em $count segundos', + one: 'em $count segundo', ); return '$_temp0'; } @override - String nbFollowers(int count) { + String timeagoInNbMinutes(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count seguidores', - one: '$count seguidores', + other: 'em $count minutos', + one: 'em $count minuto', ); return '$_temp0'; } @override - String nbFollowing(int count) { + String timeagoInNbHours(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count seguidos', - one: '$count seguidos', + other: 'em $count horas', + one: 'em $count hora', ); return '$_temp0'; } @override - String lessThanNbMinutes(int count) { + String timeagoInNbDays(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Menos que $count minutos', - one: 'Menos que $count minutos', + other: 'em $count dias', + one: 'em $count dia', ); return '$_temp0'; } @override - String nbGamesInPlay(int count) { + String timeagoInNbWeeks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas em andamento', - one: '$count partidas em andamento', + other: 'em $count semanas', + one: 'em $count semana', ); return '$_temp0'; } @override - String maximumNbCharacters(int count) { + String timeagoInNbMonths(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Máximo: $count caracteres.', - one: 'Máximo: $count caractere.', + other: 'em $count meses', + one: 'em $count mês', ); return '$_temp0'; } @override - String blocks(int count) { + String timeagoInNbYears(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count bloqueados', - one: '$count bloqueado', + other: 'em $count anos', + one: 'em $count ano', ); return '$_temp0'; } @override - String nbForumPosts(int count) { + String timeagoNbMinutesAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count publicações no fórum', - one: '$count publicação no fórum', + other: '$count minutos atrás', + one: '$count minuto atrás', ); return '$_temp0'; } @override - String nbPerfTypePlayersThisWeek(int count, String param2) { + String timeagoNbHoursAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count $param2 jogadores nesta semana.', - one: '$count $param2 jogador nesta semana.', + other: '$count horas atrás', + one: '$count hora atrás', ); return '$_temp0'; } @override - String availableInNbLanguages(int count) { + String timeagoNbDaysAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Disponível em $count idiomas!', - one: 'Disponível em $count idiomas!', + other: '$count dias atrás', + one: '$count dia atrás', ); return '$_temp0'; } @override - String nbSecondsToPlayTheFirstMove(int count) { + String timeagoNbWeeksAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count segundos para fazer o primeiro lance', - one: '$count segundo para fazer o primeiro lance', + other: '$count semanas atrás', + one: '$count semana atrás', ); return '$_temp0'; } @override - String nbSeconds(int count) { + String timeagoNbMonthsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count segundos', - one: '$count segundo', + other: '$count meses atrás', + one: '$count mês atrás', ); return '$_temp0'; } @override - String andSaveNbPremoveLines(int count) { + String timeagoNbYearsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'e salvar as linhas de pré-lance de $count', - one: 'e salvar a linha de pré-lance de $count', + other: '$count anos atrás', + one: '$count ano atrás', ); return '$_temp0'; } @override - String get stormMoveToStart => 'Mova para começar'; - - @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; - - @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; - - @override - String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; - - @override - String get stormNewDailyHighscore => 'Novo recorde diário!'; - - @override - String get stormNewWeeklyHighscore => 'Novo recorde semanal!'; - - @override - String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; - - @override - String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; - - @override - String stormPreviousHighscoreWasX(String param) { - return 'Recorde anterior era $param'; - } - - @override - String get stormPlayAgain => 'Jogar novamente'; - - @override - String stormHighscoreX(String param) { - return 'Recorde: $param'; - } - - @override - String get stormScore => 'Pontuação'; - - @override - String get stormMoves => 'Lances'; - - @override - String get stormAccuracy => 'Precisão'; - - @override - String get stormCombo => 'Combo'; - - @override - String get stormTime => 'Tempo'; - - @override - String get stormTimePerMove => 'Tempo por lance'; - - @override - String get stormHighestSolved => 'Classificação mais alta'; - - @override - String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; - - @override - String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; - - @override - String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; - - @override - String get stormHighscores => 'Melhores pontuações'; - - @override - String get stormViewBestRuns => 'Ver melhores séries'; - - @override - String get stormBestRunOfDay => 'Melhor série do dia'; - - @override - String get stormRuns => 'Séries'; - - @override - String get stormGetReady => 'Prepare-se!'; - - @override - String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; - - @override - String get stormRaceComplete => 'Corrida concluída!'; - - @override - String get stormSpectating => 'Espectando'; - - @override - String get stormJoinTheRace => 'Entre na corrida!'; - - @override - String get stormStartTheRace => 'Começar a corrida'; - - @override - String stormYourRankX(String param) { - return 'Sua classificação: $param'; - } - - @override - String get stormWaitForRematch => 'Esperando por revanche'; - - @override - String get stormNextRace => 'Próxima corrida'; - - @override - String get stormJoinRematch => 'Junte-se a revanche'; - - @override - String get stormWaitingToStart => 'Esperando para começar'; - - @override - String get stormCreateNewGame => 'Criar um novo jogo'; - - @override - String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; - - @override - String get stormRaceYourFriends => 'Corra contra seus amigos'; - - @override - String get stormSkip => 'pular'; - - @override - String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; - - @override - String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; - - @override - String get stormFailedPuzzles => 'Quebra-cabeças falhados'; - - @override - String get stormSlowPuzzles => 'Quebra-cabeças lentos'; - - @override - String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; - - @override - String get stormThisWeek => 'Essa semana'; - - @override - String get stormThisMonth => 'Esse mês'; - - @override - String get stormAllTime => 'Desde o início'; - - @override - String get stormClickToReload => 'Clique para recarregar'; - - @override - String get stormThisRunHasExpired => 'Esta corrida acabou!'; - - @override - String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; - - @override - String stormXRuns(int count) { + String timeagoNbMinutesRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count séries', - one: '1 tentativa', + other: '$count minutos restantes', + one: '$count minuto restante', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String timeagoNbHoursRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count tentativas de $param2', - one: 'Jogou uma tentativa de $param2', + other: '$count horas restantes', + one: '$count hora restante', ); return '$_temp0'; } - - @override - String get streamerLichessStreamers => 'Streamers do Lichess'; - - @override - String get studyShareAndExport => 'Compartilhar & exportar'; - - @override - String get studyStart => 'Iniciar'; } diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 6fc00078fb..fd17d69044 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsRo extends AppLocalizations { AppLocalizationsRo([String locale = 'ro']) : super(locale); @override - String get mobileHomeTab => 'Acasă'; + String get mobileAllGames => 'Toate jocurile'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Ești sigur?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Anulați propunerea de revanșă'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Resetare'; @override - String get mobileSettingsTab => 'Setări'; + String get mobileCorrespondenceClearSavedMove => 'Șterge mutarea salvată'; @override - String get mobileMustBeLoggedIn => 'Trebuie să te autentifici pentru a accesa această pagină.'; + String get mobileCustomGameJoinAGame => 'Alătură-te unui joc'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Salut, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Salut'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Ascunde variațiile'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Acasă'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Fluxuri live'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'Trebuie să te autentifici pentru a accesa această pagină.'; @override - String get mobileAllGames => 'Toate jocurile'; + String get mobileNoSearchResults => 'Niciun rezultat'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'Nu urmărești niciun utilizator.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Jucători cu \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Mărește piesa trasă'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Vrei să termini acest run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nimic de afișat, vă rugăm să schimbați filtrele'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Nimic de arătat. Jucați câteva partide de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; - - @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Rezolvă cât mai multe puzzle-uri în 3 minute.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'Îți vei pierde streak-ul actual iar scorul va fi salvat.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Puzzle-uri'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Căutări recente'; @override - String get mobileShowComments => 'Afişează сomentarii'; + String get mobileSettingsHapticFeedback => 'Control tactil'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Mod imersiv'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Setări'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGamePGN => 'Distribuie PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Distribuie URL-ul jocului'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Distribuie poziția ca FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Distribuie acest puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Afişează сomentarii'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Arată rezultatul'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Arată variațiile'; @override - String get mobileShowResult => 'Arată rezultatul'; + String get mobileSomethingWentWrong => 'Ceva nu a mers bine. :('; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Culori sistem'; @override - String get mobilePuzzleStormSubtitle => 'Rezolvă cât mai multe puzzle-uri în 3 minute.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Salut, $param'; - } + String get mobileToolsTab => 'Unelte'; @override - String get mobileGreetingWithoutName => 'Salut'; + String get mobileWaitingForOpponentToJoin => 'În așteptarea unui jucător...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Vizionează'; @override String get activityActivity => 'Activitate'; @@ -254,6 +251,18 @@ class AppLocalizationsRo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Terminat $count jocuri de tip $param2 de corespondență', + few: 'Terminat $count jocuri de tip $param2 de corespondență', + one: 'Terminat $count joc de tip $param2 de corespondență', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +374,257 @@ class AppLocalizationsRo extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmisiuni'; + @override + String get broadcastMyBroadcasts => 'Transmisiile mele'; + @override String get broadcastLiveBroadcasts => 'Difuzări de turnee în direct'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'O nouă difuzare în direct'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmisii abonate'; + + @override + String get broadcastAboutBroadcasts => 'Despre emisiuni'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Cum să utilizați emisiunile Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Runda noua va avea aceiași membri și contribuitori ca cea anterioară.'; + + @override + String get broadcastAddRound => 'Adaugă o rundă'; + + @override + String get broadcastOngoing => 'În desfășurare'; + + @override + String get broadcastUpcoming => 'Următoare'; + + @override + String get broadcastCompleted => 'Terminate'; + + @override + String get broadcastCompletedHelp => 'Lichess detectează finalizarea rundei pe baza jocurilor sursă. Utilizați această comutare dacă nu există nicio sursă.'; + + @override + String get broadcastRoundName => 'Numele rundei'; + + @override + String get broadcastRoundNumber => 'Număr rotund'; + + @override + String get broadcastTournamentName => 'Numele turneului'; + + @override + String get broadcastTournamentDescription => 'O descriere scurtă a turneului'; + + @override + String get broadcastFullDescription => 'Întreaga descriere a evenimentului'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descriere lungă, opțională, a difuzării. $param1 este disponibil. Lungimea trebuie să fie mai mică decât $param2 caractere.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL sursă PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL-ul pe care Lichess îl va verifica pentru a obține actualizări al PGN-ului. Trebuie să fie public accesibil pe Internet.'; + + @override + String get broadcastSourceGameIds => 'Până la 64 de ID-uri de joc Lichess, separate prin spații.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opțional, dacă știi când va începe evenimentul'; + + @override + String get broadcastCurrentGameUrl => 'URL-ul partidei curente'; + + @override + String get broadcastDownloadAllRounds => 'Descarcă toate rundele'; + + @override + String get broadcastResetRound => 'Resetează această rundă'; + + @override + String get broadcastDeleteRound => 'Șterge această rundă'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Șterge definitiv runda și jocurile sale.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Șterge toate jocurile din această rundă. Sursa va trebui să fie activă pentru a le recrea.'; + + @override + String get broadcastEditRoundStudy => 'Editare rundă de studiu'; + + @override + String get broadcastDeleteTournament => 'Șterge acest turneu'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Sigur doresc să ștergeți întregul turneu, toate rundele și toate jocurile sale.'; + + @override + String get broadcastShowScores => 'Arată scorurile jucătorilor pe baza rezultatelor jocului'; + + @override + String get broadcastReplacePlayerTags => 'Opțional: înlocuiește numele jucătorilor, ratingurile și titlurile'; + + @override + String get broadcastFideFederations => 'Federații FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 evaluări'; + + @override + String get broadcastFidePlayers => 'Jucători FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jucătorul FIDE nu a fost găsit'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federație'; + + @override + String get broadcastAgeThisYear => 'Vârsta în acest an'; + + @override + String get broadcastUnrated => 'Fără rating'; + + @override + String get broadcastRecentTournaments => 'Turnee recente'; + + @override + String get broadcastOpenLichess => 'Deschide în Lichess'; + + @override + String get broadcastTeams => 'Echipe'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Clasament'; + + @override + String get broadcastOfficialStandings => 'Clasament oficial'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Scor'; + + @override + String get broadcastAllTeams => 'Toate echipele'; + + @override + String get broadcastTournamentFormat => 'Format turneu'; + + @override + String get broadcastTournamentLocation => 'Locație turneu'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Fus orar'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count de transmisiuni', + few: '$count transmisiuni', + one: '$count transmisiune', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Provocări: $param1'; @@ -626,6 +883,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get preferencesInGameOnly => 'Doar în joc'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ceasul de șah'; @@ -767,6 +1027,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Sunet de notificare'; + @override + String get preferencesBlindfold => 'Legat la ochi'; + @override String get puzzlePuzzles => 'Probleme de șah'; @@ -1412,10 +1675,10 @@ class AppLocalizationsRo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.'; @override - String get puzzleThemeHealthyMix => 'Amestec sănătos'; + String get puzzleThemeMix => 'Mixt'; @override - String get puzzleThemeHealthyMixDescription => 'Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.'; + String get puzzleThemeMixDescription => 'Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.'; @override String get puzzleThemePlayerGames => 'Partide jucători'; @@ -1658,10 +1921,10 @@ class AppLocalizationsRo extends AppLocalizations { String get deleteFromHere => 'Șterge de aici'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Restrânge variațiile'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Extinde variațiile'; @override String get forceVariation => 'Forțează variația'; @@ -1789,9 +2052,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get byCPL => 'După CPL'; - @override - String get openStudy => 'Studiu deschis'; - @override String get enable => 'Activează'; @@ -1819,9 +2079,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimină limita de adâncime (și încălzește computerul)'; - @override - String get engineManager => 'Manager de motor'; - @override String get blunder => 'Gafă'; @@ -2085,6 +2342,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get gamesPlayed => 'Partide jucate'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anulare'; @@ -2459,9 +2719,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get unblock => 'Deblocare'; - @override - String get followsYou => 'Vă urmărește'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 a început să vă urmărească $param2'; @@ -2656,13 +2913,13 @@ class AppLocalizationsRo extends AppLocalizations { String get realName => 'Nume real'; @override - String get setFlair => 'Set your flair'; + String get setFlair => 'Arată-ți stilul'; @override String get flair => 'Pictograma personalizată'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => 'Există o setare pentru a ascunde flair-ul utilizatorilor pe întregului site.'; @override String get biography => 'Biografie'; @@ -2713,7 +2970,7 @@ class AppLocalizationsRo extends AppLocalizations { String get puzzles => 'Probleme de șah'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Boți online'; @override String get name => 'Nume'; @@ -2734,10 +2991,10 @@ class AppLocalizationsRo extends AppLocalizations { String get yes => 'Da'; @override - String get website => 'Website'; + String get website => 'Pagină web'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Ajutor:'; @@ -2794,7 +3051,13 @@ class AppLocalizationsRo extends AppLocalizations { String get other => 'Altceva'; @override - String get reportDescriptionHelp => 'Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar ”trișează”, ci spune-ne cum ai ajuns la această concluzie. Raportul tău va fi procesat mai rapid dacă este scris în engleză.'; + String get reportCheatBoostHelp => 'Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar \"trișează\", ci spune-ne cum ai ajuns la această concluzie.'; + + @override + String get reportUsernameHelp => 'Explică de ce acest nume de utilizator este jignitor. Nu spune doar \"e ofensiv/inadecvat\", ci spune-ne cum ai ajuns la această concluzie, mai ales în cazul în care insulta este obscură, nu este în engleză, este jargon sau este o referință istorică/culturală.'; + + @override + String get reportProcessedFasterInEnglish => 'Raportul tău va fi procesat mai rapid dacă este scris în engleză.'; @override String get error_provideOneCheatedGameLink => 'Te rugăm să furnizezi cel puțin un link către un joc în care s-a trișat.'; @@ -2897,7 +3160,7 @@ class AppLocalizationsRo extends AppLocalizations { String get outsideTheBoard => 'În afara tablei'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Toate pătratele de pe tablă'; @override String get onSlowGames => 'În jocurile lente'; @@ -3228,13 +3491,13 @@ class AppLocalizationsRo extends AppLocalizations { String get toggleVariationArrows => 'Comută săgețile de variație'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => 'Ciclu de variație precedentă/următoare'; @override String get toggleGlyphAnnotations => 'Comută adnotările gilfelor'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => 'Activează/Dezactivează adnotările pozițiilor'; @override String get variationArrowsInfo => 'Săgețile de variație vă permit să navigați fără a utiliza lista de mutare.'; @@ -4097,7 +4360,10 @@ class AppLocalizationsRo extends AppLocalizations { String get lichessPatronInfo => 'Lichess este o asociație non-profit și un software gratuit și open-source.\nToate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Nimic de văzut aici momentan.'; + + @override + String get stats => 'Statistici'; @override String opponentLeftCounter(int count) { @@ -4790,8 +5056,711 @@ class AppLocalizationsRo extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streameri'; @override - String get studyShareAndExport => 'Împărtășește & exportă'; + String get studyPrivate => 'Privat'; @override - String get studyStart => 'Începe'; + String get studyMyStudies => 'Studiile mele'; + + @override + String get studyStudiesIContributeTo => 'Studiile la care contribui'; + + @override + String get studyMyPublicStudies => 'Studiile mele publice'; + + @override + String get studyMyPrivateStudies => 'Studiile mele private'; + + @override + String get studyMyFavoriteStudies => 'Studiile mele preferate'; + + @override + String get studyWhatAreStudies => 'Ce sunt studiile?'; + + @override + String get studyAllStudies => 'Toate studiile'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studii create de $param'; + } + + @override + String get studyNoneYet => 'Niciunul încă.'; + + @override + String get studyHot => 'Populare'; + + @override + String get studyDateAddedNewest => 'Data adăugată (cele mai noi)'; + + @override + String get studyDateAddedOldest => 'Data adăugată (cele mai vechi)'; + + @override + String get studyRecentlyUpdated => 'Încărcate recent'; + + @override + String get studyMostPopular => 'Cele mai populare'; + + @override + String get studyAlphabetical => 'Alfabetic'; + + @override + String get studyAddNewChapter => 'Adaugă un nou capitol'; + + @override + String get studyAddMembers => 'Adaugă membri'; + + @override + String get studyInviteToTheStudy => 'Invită la studiu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Vă rugăm să invitați doar persoanele pe care le cunoașteți și care vor în mod activ să se alăture studiului.'; + + @override + String get studySearchByUsername => 'Caută după numele de utilizator'; + + @override + String get studySpectator => 'Spectator'; + + @override + String get studyContributor => 'Contribuitor'; + + @override + String get studyKick => 'Înlătură'; + + @override + String get studyLeaveTheStudy => 'Părăsește studiul'; + + @override + String get studyYouAreNowAContributor => 'Acum ești un contribuitor'; + + @override + String get studyYouAreNowASpectator => 'Acum ești un spectator'; + + @override + String get studyPgnTags => 'Etichete PGN'; + + @override + String get studyLike => 'Apreciază'; + + @override + String get studyUnlike => 'Nu îmi mai place'; + + @override + String get studyNewTag => 'Etichetă nouă'; + + @override + String get studyCommentThisPosition => 'Comentează această poziție'; + + @override + String get studyCommentThisMove => 'Comentează această mutare'; + + @override + String get studyAnnotateWithGlyphs => 'Adnotează cu simboluri'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Capitolul este prea mic pentru a fi analizat.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Numai contribuitorii studiului pot solicita o analiză a computerului.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obțineți o întreagă analiză server-side a computerului a variației principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asigurați-vă că acest capitol este complet. Puteți solicita o analiză doar o singură dată.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Toți membri sincronizați rămân la aceeași poziție'; + + @override + String get studyShareChanges => 'Împărtășește modificările cu spectatorii și salvează-le pe server'; + + @override + String get studyPlaying => 'În desfășurare'; + + @override + String get studyShowEvalBar => 'Bară de evaluare'; + + @override + String get studyFirst => 'Prima'; + + @override + String get studyPrevious => 'Precedentă'; + + @override + String get studyNext => 'Următoarea'; + + @override + String get studyLast => 'Ultima'; + + @override + String get studyShareAndExport => 'Împărtășește & exportă'; + + @override + String get studyCloneStudy => 'Clonează'; + + @override + String get studyStudyPgn => 'PGN-ul studiului'; + + @override + String get studyDownloadAllGames => 'Descarcă toate partidele'; + + @override + String get studyChapterPgn => 'PGN-ul capitolului'; + + @override + String get studyCopyChapterPgn => 'Copiază PGN'; + + @override + String get studyDownloadGame => 'Descarcă partida'; + + @override + String get studyStudyUrl => 'URL-ul studiului'; + + @override + String get studyCurrentChapterUrl => 'URL-ul capitolului curent'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Poți lipi acest cod în forum pentru a îngloba'; + + @override + String get studyStartAtInitialPosition => 'Începeți de la poziția inițială'; + + @override + String studyStartAtX(String param) { + return 'Începeți la $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Înglobează pe site-ul sau blog-ul tău'; + + @override + String get studyReadMoreAboutEmbedding => 'Citește mai multe despre înglobare'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Numai studii publice pot fi înglobate!'; + + @override + String get studyOpen => 'Deschideți'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, oferit pentru dvs. de $param2'; + } + + @override + String get studyStudyNotFound => 'Studiul nu a fost găsit'; + + @override + String get studyEditChapter => 'Editează capitolul'; + + @override + String get studyNewChapter => 'Capitol nou'; + + @override + String studyImportFromChapterX(String param) { + return 'Importă din $param'; + } + + @override + String get studyOrientation => 'Orientare'; + + @override + String get studyAnalysisMode => 'Tip de analiză'; + + @override + String get studyPinnedChapterComment => 'Comentariu fixat'; + + @override + String get studySaveChapter => 'Salvează capitolul'; + + @override + String get studyClearAnnotations => 'Curățați adnotările'; + + @override + String get studyClearVariations => 'Curățați variațiile'; + + @override + String get studyDeleteChapter => 'Ștergeți capitolul'; + + @override + String get studyDeleteThisChapter => 'Ștergeți acest capitol? Nu există cale de întoarcere!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Ștergeți toate comentariile, simbolurile și figurile desenate din acest capitol?'; + + @override + String get studyRightUnderTheBoard => 'Fix sub tablă'; + + @override + String get studyNoPinnedComment => 'Niciunul'; + + @override + String get studyNormalAnalysis => 'Analiză normală'; + + @override + String get studyHideNextMoves => 'Ascunde următoarele mutări'; + + @override + String get studyInteractiveLesson => 'Lecție interactivă'; + + @override + String studyChapterX(String param) { + return 'Capitolul $param'; + } + + @override + String get studyEmpty => 'Gol'; + + @override + String get studyStartFromInitialPosition => 'Începeți de la poziția inițială'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Începeți de la o poziție personalizată'; + + @override + String get studyLoadAGameByUrl => 'Încărcați meciul din URL'; + + @override + String get studyLoadAPositionFromFen => 'Încărcați o poziție din FEN'; + + @override + String get studyLoadAGameFromPgn => 'Încărcați un joc din PGN'; + + @override + String get studyAutomatic => 'Automată'; + + @override + String get studyUrlOfTheGame => 'URL-ul jocului'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Încărcați un joc de pe $param1 sau $param2'; + } + + @override + String get studyCreateChapter => 'Creați capitolul'; + + @override + String get studyCreateStudy => 'Creați studiul'; + + @override + String get studyEditStudy => 'Editați studiul'; + + @override + String get studyVisibility => 'Vizibilitate'; + + @override + String get studyPublic => 'Public'; + + @override + String get studyUnlisted => 'Nelistat'; + + @override + String get studyInviteOnly => 'Doar invitați'; + + @override + String get studyAllowCloning => 'Permiteți clonarea'; + + @override + String get studyNobody => 'Nimeni'; + + @override + String get studyOnlyMe => 'Doar eu'; + + @override + String get studyContributors => 'Contribuitori'; + + @override + String get studyMembers => 'Membri'; + + @override + String get studyEveryone => 'Toată lumea'; + + @override + String get studyEnableSync => 'Activați sincronizarea'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: menține-i pe toți la aceeași poziție'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nu: permite navigarea liberă'; + + @override + String get studyPinnedStudyComment => 'Comentariu fixat'; + + @override + String get studyStart => 'Începe'; + + @override + String get studySave => 'Salvează'; + + @override + String get studyClearChat => 'Șterge conversația'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ștergeți istoricul chatului? Nu există cale de întoarcere!'; + + @override + String get studyDeleteStudy => 'Ștergeți studiul'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ștergeți întregul studiu? Nu există cale de întoarcere! Introduceți numele studiului pentru a confirma: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Unde vreți s-o studiați?'; + + @override + String get studyGoodMove => 'Mutare bună'; + + @override + String get studyMistake => 'Greșeală'; + + @override + String get studyBrilliantMove => 'Mișcare genială'; + + @override + String get studyBlunder => 'Gafă'; + + @override + String get studyInterestingMove => 'Mișcare interesantă'; + + @override + String get studyDubiousMove => 'Mutare dubioasă'; + + @override + String get studyOnlyMove => 'Singura mișcare posibilă'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Poziție egală'; + + @override + String get studyUnclearPosition => 'Poziție neclară'; + + @override + String get studyWhiteIsSlightlyBetter => 'Albul este puțin mai bun'; + + @override + String get studyBlackIsSlightlyBetter => 'Negrul este puțin mai bun'; + + @override + String get studyWhiteIsBetter => 'Albul este mai bun'; + + @override + String get studyBlackIsBetter => 'Negrul este mai bun'; + + @override + String get studyWhiteIsWinning => 'Albul câștigă'; + + @override + String get studyBlackIsWinning => 'Negrul câștigă'; + + @override + String get studyNovelty => 'Noutate'; + + @override + String get studyDevelopment => 'Dezvoltare'; + + @override + String get studyInitiative => 'Inițiativă'; + + @override + String get studyAttack => 'Atac'; + + @override + String get studyCounterplay => 'Contraatac'; + + @override + String get studyTimeTrouble => 'Probleme de timp'; + + @override + String get studyWithCompensation => 'Cu compensații'; + + @override + String get studyWithTheIdea => 'Cu ideea'; + + @override + String get studyNextChapter => 'Capitolul următor'; + + @override + String get studyPrevChapter => 'Capitolul precedent'; + + @override + String get studyStudyActions => 'Acţiuni de studiu'; + + @override + String get studyTopics => 'Subiecte'; + + @override + String get studyMyTopics => 'Subiectele mele'; + + @override + String get studyPopularTopics => 'Subiecte populare'; + + @override + String get studyManageTopics => 'Gestionează subiecte'; + + @override + String get studyBack => 'Înapoi'; + + @override + String get studyPlayAgain => 'Joacă din nou'; + + @override + String get studyWhatWouldYouPlay => 'Ce ai juca în această poziție?'; + + @override + String get studyYouCompletedThisLesson => 'Felicitări! Ai terminat această lecție.'; + + @override + String studyPerPage(String param) { + return '$param pe pagină'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capitole', + few: '$count capitole', + one: '$count capitol', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partide', + few: '$count partide', + one: '$count partidă', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membri', + few: '$count membri', + one: '$count membru', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Lipiți textul PGN aici, până la $count meciuri', + few: 'Lipiți textul PGN aici, până la $count meciuri', + one: 'Lipiți textul PGN aici, până la $count meci', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'chiar acum'; + + @override + String get timeagoRightNow => 'chiar acum'; + + @override + String get timeagoCompleted => 'completat'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de secunde', + few: 'în $count secunde', + one: 'în $count secundă', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de minute', + few: 'în $count minute', + one: 'în $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de ore', + few: 'în $count ore', + one: 'în $count oră', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de zile', + few: 'în $count zile', + one: 'în $count zi', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de săptămâni', + few: 'în $count săptămâni', + one: 'în $count săptămână', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de luni', + few: 'în $count luni', + one: 'în $count lună', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de ani', + few: 'în $count ani', + one: 'în $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de minute în urmă', + few: 'cu $count minute în urmă', + one: 'cu $count minut în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de ore în urmă', + few: 'cu $count ore în urmă', + one: 'cu $count oră în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de zile în urmă', + few: 'cu $count zile în urmă', + one: 'cu $count zi în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de săptămâni în urmă', + few: 'cu $count săptămâni în urmă', + one: 'cu $count săptămână în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de luni în urmă', + few: 'cu $count luni în urmă', + one: 'cu $count lună în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de ani în urmă', + few: 'cu $count ani în urmă', + one: 'cu $count an în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minute rămase', + few: '$count minute rămase', + one: '$count minut rămas', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore rămase', + few: '$count ore rămase', + one: '$count oră rămasă', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index cda5bef248..ae91030a3a 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); @override - String get mobileHomeTab => 'Главная'; + String get mobileAllGames => 'Все игры'; @override - String get mobilePuzzlesTab => 'Задачи'; + String get mobileAreYouSure => 'Вы уверены?'; @override - String get mobileToolsTab => 'Анализ'; + String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; @override - String get mobileWatchTab => 'Просмотр'; + String get mobileClearButton => 'Очистить'; @override - String get mobileSettingsTab => 'Настройки'; + String get mobileCorrespondenceClearSavedMove => 'Очистить сохранённый ход'; @override - String get mobileMustBeLoggedIn => 'Вы должны войти для просмотра этой страницы.'; + String get mobileCustomGameJoinAGame => 'Присоединиться к игре'; @override - String get mobileSystemColors => 'Цвет интерфейса'; + String get mobileFeedbackButton => 'Отзыв'; @override - String get mobileFeedbackButton => 'Отзыв'; + String mobileGreeting(String param) { + return 'Привет, $param'; + } @override - String get mobileOkButton => 'ОК'; + String get mobileGreetingWithoutName => 'Привет'; @override - String get mobileSettingsHapticFeedback => 'Виброотклик'; + String get mobileHideVariation => 'Скрыть варианты'; @override - String get mobileSettingsImmersiveMode => 'Полноэкранный режим'; + String get mobileHomeTab => 'Главная'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.'; + String get mobileLiveStreamers => 'Стримеры в эфире'; @override - String get mobileNotFollowingAnyUser => 'Вы не подписаны на других пользователей.'; + String get mobileMustBeLoggedIn => 'Вы должны войти для просмотра этой страницы.'; @override - String get mobileAllGames => 'Все игры'; + String get mobileNoSearchResults => 'Ничего не найденo'; @override - String get mobileRecentSearches => 'Последние запросы'; + String get mobileNotFollowingAnyUser => 'Вы не подписаны на других пользователей.'; @override - String get mobileClearButton => 'Очистить'; + String get mobileOkButton => 'ОК'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ничего не найденo'; + String get mobilePrefMagnifyDraggedPiece => 'Увеличивать перетаскиваемую фигуру'; @override - String get mobileAreYouSure => 'Вы уверены?'; + String get mobilePuzzleStormConfirmEndRun => 'Хотите закончить эту попытку?'; @override - String get mobilePuzzleStreakAbortWarning => 'Вы потеряете свою текущую серию, и результаты будут сохранены.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ничего не найдено, измените фильтры, пожалуйста'; @override String get mobilePuzzleStormNothingToShow => 'Ничего нет. Сыграйте несколько попыток.'; @override - String get mobileSharePuzzle => 'Поделиться задачей'; + String get mobilePuzzleStormSubtitle => 'Решите как можно больше задач за 3 минуты.'; @override - String get mobileShareGameURL => 'Поделиться ссылкой на игру'; + String get mobilePuzzleStreakAbortWarning => 'Вы потеряете свою текущую серию, и результаты будут сохранены.'; @override - String get mobileShareGamePGN => 'Поделиться PGN'; + String get mobilePuzzleThemesSubtitle => 'Решайте задачи по вашим любимым дебютам или выберите тему.'; @override - String get mobileSharePositionAsFEN => 'Поделиться FEN'; + String get mobilePuzzlesTab => 'Задачи'; @override - String get mobileShowVariations => 'Показывать варианты'; + String get mobileRecentSearches => 'Последние запросы'; @override - String get mobileHideVariation => 'Скрыть варианты'; + String get mobileSettingsHapticFeedback => 'Виброотклик'; @override - String get mobileShowComments => 'Показать комментарии'; + String get mobileSettingsImmersiveMode => 'Полноэкранный режим'; @override - String get mobilePuzzleStormConfirmEndRun => 'Хотите закончить эту попытку?'; + String get mobileSettingsImmersiveModeSubtitle => 'Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ничего не найдено, измените фильтры, пожалуйста'; + String get mobileSettingsTab => 'Настройки'; @override - String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; + String get mobileShareGamePGN => 'Поделиться PGN'; @override - String get mobileCancelDrawOffer => 'Отменить предложение ничьей'; + String get mobileShareGameURL => 'Поделиться ссылкой на игру'; @override - String get mobileWaitingForOpponentToJoin => 'Ожидание соперника...'; + String get mobileSharePositionAsFEN => 'Поделиться FEN'; @override - String get mobileBlindfoldMode => 'Игра вслепую'; + String get mobileSharePuzzle => 'Поделиться задачей'; @override - String get mobileLiveStreamers => 'Стримеры в эфире'; + String get mobileShowComments => 'Показать комментарии'; @override - String get mobileCustomGameJoinAGame => 'Присоединиться к игре'; + String get mobileShowResult => 'Показать результат'; @override - String get mobileCorrespondenceClearSavedMove => 'Очистить сохранённый ход'; + String get mobileShowVariations => 'Показывать варианты'; @override String get mobileSomethingWentWrong => 'Что-то пошло не так.'; @override - String get mobileShowResult => 'Показать результат'; - - @override - String get mobilePuzzleThemesSubtitle => 'Решайте задачи по вашим любимым дебютам или выберите тему.'; + String get mobileSystemColors => 'Цвет интерфейса'; @override - String get mobilePuzzleStormSubtitle => 'Решите как можно больше задач за 3 минуты.'; + String get mobileTheme => 'Оформление'; @override - String mobileGreeting(String param) { - return 'Привет, $param'; - } + String get mobileToolsTab => 'Анализ'; @override - String get mobileGreetingWithoutName => 'Привет'; + String get mobileWaitingForOpponentToJoin => 'Ожидание соперника...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Просмотр'; @override String get activityActivity => 'Активность'; @@ -262,6 +259,19 @@ class AppLocalizationsRu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Завершены $count $param2 игр по переписке', + many: 'Завершены $count $param2 игр по переписке', + few: 'Завершены $count $param2 игры по переписке', + one: 'Завершена $count $param2 игра по переписке', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsRu extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляции'; + @override + String get broadcastMyBroadcasts => 'Мои трансляции'; + @override String get broadcastLiveBroadcasts => 'Прямые трансляции турнира'; + @override + String get broadcastBroadcastCalendar => 'Календарь трансляций'; + + @override + String get broadcastNewBroadcast => 'Новая прямая трансляция'; + + @override + String get broadcastSubscribedBroadcasts => 'Подписанные рассылки'; + + @override + String get broadcastAboutBroadcasts => 'О трансляции'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Как пользоваться трансляциями Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'В новом туре примут участие те же участники и редакторы, что и в предыдущем туре.'; + + @override + String get broadcastAddRound => 'Добавить тур'; + + @override + String get broadcastOngoing => 'Текущие'; + + @override + String get broadcastUpcoming => 'Предстоящие'; + + @override + String get broadcastCompleted => 'Завершённые'; + + @override + String get broadcastCompletedHelp => 'Lichess определяет завершение тура на основе источника партий. Используйте этот переключатель, если нет источника.'; + + @override + String get broadcastRoundName => 'Название тура'; + + @override + String get broadcastRoundNumber => 'Номер тура'; + + @override + String get broadcastTournamentName => 'Название турнира'; + + @override + String get broadcastTournamentDescription => 'Краткое описание турнира'; + + @override + String get broadcastFullDescription => 'Полное описание события'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Необязательное полное описание трансляции. Доступна разметка $param1. Длина должна быть меньше $param2 символов.'; + } + + @override + String get broadcastSourceSingleUrl => 'Исходный URL PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL-адрес, с которого Lichess будет получать обновление PGN. Он должен быть доступен для получения из Интернета.'; + + @override + String get broadcastSourceGameIds => 'До 64 идентификаторов (ID) игр Lichess, разделённых пробелами.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Дата начала турнира в местном часовом поясе: $param'; + } + + @override + String get broadcastStartDateHelp => 'Дополнительно, если вы знаете, когда событие начнётся'; + + @override + String get broadcastCurrentGameUrl => 'URL-адрес текущей партии'; + + @override + String get broadcastDownloadAllRounds => 'Скачать все туры'; + + @override + String get broadcastResetRound => 'Сбросить тур'; + + @override + String get broadcastDeleteRound => 'Удалить этот тур'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Определенно удалить тур и его партии.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Удалить все партии этого тура. Для их пересоздания потребуется активный источник.'; + + @override + String get broadcastEditRoundStudy => 'Редактировать студию тура'; + + @override + String get broadcastDeleteTournament => 'Удалить этот турнир'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Окончательно удалить весь турнир, его туры и партии.'; + + @override + String get broadcastShowScores => 'Показать очки игроков по результатам партий'; + + @override + String get broadcastReplacePlayerTags => 'Необязательно: заменить имена игроков, рейтинги и звания'; + + @override + String get broadcastFideFederations => 'Федерации FIDE'; + + @override + String get broadcastTop10Rating => 'Топ-10'; + + @override + String get broadcastFidePlayers => 'Игроки FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Профиль FIDE не найден'; + + @override + String get broadcastFideProfile => 'Профиль FIDE'; + + @override + String get broadcastFederation => 'Федерация'; + + @override + String get broadcastAgeThisYear => 'Возраст в этом году'; + + @override + String get broadcastUnrated => 'Без рейтинга'; + + @override + String get broadcastRecentTournaments => 'Недавние турниры'; + + @override + String get broadcastOpenLichess => 'Открыть в Lichess'; + + @override + String get broadcastTeams => 'Клубы'; + + @override + String get broadcastBoards => 'Доски'; + + @override + String get broadcastOverview => 'Обзор'; + + @override + String get broadcastSubscribeTitle => 'Подпишитесь, чтобы получать уведомления о начале каждого раунда. Вы можете включить звуковое или пуш-уведомление для трансляций в своих настройках.'; + + @override + String get broadcastUploadImage => 'Загрузить изображение турнира'; + + @override + String get broadcastNoBoardsYet => 'Пока нет досок. Они появятся после загрузки партий.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Доски могут быть загружены из источника или с помощью $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Начало после $param'; + } + + @override + String get broadcastStartVerySoon => 'Трансляция начнётся совсем скоро.'; + + @override + String get broadcastNotYetStarted => 'Трансляция ещё не началась.'; + + @override + String get broadcastOfficialWebsite => 'Официальный веб-сайт'; + + @override + String get broadcastStandings => 'Турнирная таблица'; + + @override + String get broadcastOfficialStandings => 'Официальная турнирная таблица'; + + @override + String broadcastIframeHelp(String param) { + return 'Больше опций на $param'; + } + + @override + String get broadcastWebmastersPage => 'странице веб-мастера'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Публичный PGN-источник для этого раунда в реальном времени. Мы также предлагаем $param для более быстрой и эффективной синхронизации.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Встройте эту трансляцию на ваш сайт'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Встроить $param на свой сайт'; + } + + @override + String get broadcastRatingDiff => 'Разница в рейтингах'; + + @override + String get broadcastGamesThisTournament => 'Партии этого турнира'; + + @override + String get broadcastScore => 'Очки'; + + @override + String get broadcastAllTeams => 'Все клубы'; + + @override + String get broadcastTournamentFormat => 'Формат турнира'; + + @override + String get broadcastTournamentLocation => 'Местоположение турнира'; + + @override + String get broadcastTopPlayers => 'Лучшие игроки'; + + @override + String get broadcastTimezone => 'Часовой пояс'; + + @override + String get broadcastFideRatingCategory => 'Категория рейтинга FIDE'; + + @override + String get broadcastOptionalDetails => 'Необязательные данные'; + + @override + String get broadcastPastBroadcasts => 'Завершённые трансляции'; + + @override + String get broadcastAllBroadcastsByMonth => 'Просмотр всех трансляций за месяц'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count трансляций', + many: '$count трансляций', + few: '$count трансляции', + one: '$count трансляция', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Вызовов: $param1'; @@ -643,6 +902,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get preferencesInGameOnly => 'Только в игре'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматные часы'; @@ -773,7 +1035,7 @@ class AppLocalizationsRu extends AppLocalizations { String get preferencesNotifyBell => 'Звуковое оповещение на Личесс'; @override - String get preferencesNotifyPush => 'Оповещение на устройстве, когда Вы не находитесь на сайте Личесс'; + String get preferencesNotifyPush => 'Оповещение на устройстве, когда вы не находитесь на сайте Lichess'; @override String get preferencesNotifyWeb => 'Браузер'; @@ -784,6 +1046,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Звук колокольчика уведомлений'; + @override + String get preferencesBlindfold => 'Игра вслепую'; + @override String get puzzlePuzzles => 'Задачи'; @@ -1045,7 +1310,7 @@ class AppLocalizationsRu extends AppLocalizations { other: '$count баллов выше вашего рейтинга в задачах', many: '$count баллов выше вашего рейтинга в задачах', few: '$count баллов выше вашего рейтинга в задачах', - one: 'Один балл выше вашего рейтинга в пазлах', + one: 'Один балл выше вашего рейтинга в задачах', ); return '$_temp0'; } @@ -1434,10 +1699,10 @@ class AppLocalizationsRu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Противник вынужден сделать один из немногих возможных ходов, но любой ход ведёт к ухудшению его положения.'; @override - String get puzzleThemeHealthyMix => 'Сборная солянка'; + String get puzzleThemeMix => 'Сборная солянка'; @override - String get puzzleThemeHealthyMixDescription => 'Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.'; + String get puzzleThemeMixDescription => 'Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.'; @override String get puzzleThemePlayerGames => 'Партии игрока'; @@ -1773,7 +2038,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get playFirstOpeningEndgameExplorerMove => 'Играть первый ход изучателя дебютов/эндшпилей'; + String get playFirstOpeningEndgameExplorerMove => 'Играть первый ход изучения дебютов/эндшпилей'; @override String get winPreventedBy50MoveRule => 'Не удаётся победить из-за правила 50 ходов'; @@ -1788,7 +2053,7 @@ class AppLocalizationsRu extends AppLocalizations { String get lossOr50MovesByPriorMistake => 'Поражение или 50 ходов после последней ошибки'; @override - String get unknownDueToRounding => 'Победа/поражение гарантируется только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.'; + String get unknownDueToRounding => 'Победа/поражение гарантируется, только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.'; @override String get allSet => 'Готово!'; @@ -1811,9 +2076,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get byCPL => 'По ошибкам'; - @override - String get openStudy => 'Открыть в студии'; - @override String get enable => 'Включить'; @@ -1841,9 +2103,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get removesTheDepthLimit => 'Снимает ограничение на глубину анализа, но заставляет поработать ваш компьютер'; - @override - String get engineManager => 'Менеджер движка'; - @override String get blunder => 'Зевок'; @@ -2107,6 +2366,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get gamesPlayed => 'Сыграно партий'; + @override + String get ok => 'ОК'; + @override String get cancel => 'Отменить'; @@ -2481,9 +2743,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get unblock => 'Разблокировать'; - @override - String get followsYou => 'Подписан на вас'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 подписался на $param2'; @@ -2747,7 +3006,7 @@ class AppLocalizationsRu extends AppLocalizations { String get descPrivate => 'Описание для членов команды'; @override - String get descPrivateHelp => 'Текст, который будут видеть только члены команды(добавленный текст заменит публичное описание для членов команды).'; + String get descPrivateHelp => 'Описание, которое будут видеть только члены клуба. Если установлено, то заменяет публичное описание для всех членов клуба.'; @override String get no => 'Нет'; @@ -2816,7 +3075,13 @@ class AppLocalizationsRu extends AppLocalizations { String get other => 'Другое'; @override - String get reportDescriptionHelp => 'Поделитесь с нами ссылками на игры, где, как вам кажется, были нарушены правила, и опишите, в чём дело. Недостаточно просто написать «он мухлюет», пожалуйста, опишите, как вы пришли к такому выводу. Мы сработаем оперативнее, если вы напишете на английском языке.'; + String get reportCheatBoostHelp => 'Вставьте ссылку на игру (или несколько игр) и объясните, что не так в поведении этого пользователя. Не надо просто писать «он жульничал», лучше распишите, как вы пришли к такому выводу.'; + + @override + String get reportUsernameHelp => 'Объясните, что в этом имени пользователя является оскорбительным. Не надо просто писать «оно оскорбительно или неподобающе», лучше расскажите, как вы пришли к такому выводу, особенно если оскорбление завуалировано, не на английском языке, является сленгом, или же является исторической или культурной отсылкой.'; + + @override + String get reportProcessedFasterInEnglish => 'Ваша жалоба будет рассмотрена быстрее, если она будет написана на английском языке.'; @override String get error_provideOneCheatedGameLink => 'Пожалуйста, добавьте ссылку хотя бы на одну игру, где по вашему мнению были нарушены правила.'; @@ -4121,6 +4386,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get nothingToSeeHere => 'Здесь ничего нет пока.'; + @override + String get stats => 'Статистика'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4271,8 +4539,8 @@ class AppLocalizationsRu extends AppLocalizations { locale: localeName, other: '$count минут', many: '$count минут', - few: '$count минуты', - one: '$count минута', + few: '$count Минуты', + one: '$count Одна минута', ); return '$_temp0'; } @@ -4855,9 +5123,731 @@ class AppLocalizationsRu extends AppLocalizations { @override String get streamerLichessStreamers => 'Стримеры Lichess'; + @override + String get studyPrivate => 'Частная'; + + @override + String get studyMyStudies => 'Мои студии'; + + @override + String get studyStudiesIContributeTo => 'Студии с моим участием'; + + @override + String get studyMyPublicStudies => 'Мои публичные студии'; + + @override + String get studyMyPrivateStudies => 'Мои частные студии'; + + @override + String get studyMyFavoriteStudies => 'Мои отмеченные студии'; + + @override + String get studyWhatAreStudies => 'Что такое «студии»?'; + + @override + String get studyAllStudies => 'Все студии'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Студии, созданные $param'; + } + + @override + String get studyNoneYet => 'Пока ничего.'; + + @override + String get studyHot => 'Самые активные'; + + @override + String get studyDateAddedNewest => 'Недавно добавленные'; + + @override + String get studyDateAddedOldest => 'Давно добавленные'; + + @override + String get studyRecentlyUpdated => 'Недавно обновлённые'; + + @override + String get studyMostPopular => 'Самые популярные'; + + @override + String get studyAlphabetical => 'По алфавиту'; + + @override + String get studyAddNewChapter => 'Добавить новую главу'; + + @override + String get studyAddMembers => 'Добавить участников'; + + @override + String get studyInviteToTheStudy => 'Пригласить в студию'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Приглашайте только тех участников, которых вы знаете, и кто активно желает участвовать в этой студии.'; + + @override + String get studySearchByUsername => 'Поиск по имени'; + + @override + String get studySpectator => 'Зритель'; + + @override + String get studyContributor => 'Редактор'; + + @override + String get studyKick => 'Выгнать'; + + @override + String get studyLeaveTheStudy => 'Покинуть студию'; + + @override + String get studyYouAreNowAContributor => 'Теперь вы редактор'; + + @override + String get studyYouAreNowASpectator => 'Теперь вы зритель'; + + @override + String get studyPgnTags => 'Теги PGN'; + + @override + String get studyLike => 'Нравится'; + + @override + String get studyUnlike => 'Не нравится'; + + @override + String get studyNewTag => 'Новый тег'; + + @override + String get studyCommentThisPosition => 'Комментировать эту позицию'; + + @override + String get studyCommentThisMove => 'Комментировать этот ход'; + + @override + String get studyAnnotateWithGlyphs => 'Добавить символьную аннотацию'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Глава слишком короткая для анализа.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Только редакторы студии могут запросить компьютерный анализ.'; + + @override + String get studyGetAFullComputerAnalysis => 'Получить с сервера полный компьютерный анализ главной линии.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Убедитесь, что глава завершена. Вы можете запросить анализ только один раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Все синхронизированные участники остаются на той же позиции'; + + @override + String get studyShareChanges => 'Поделиться изменениями со зрителями и сохранить их на сервере'; + + @override + String get studyPlaying => 'Активные'; + + @override + String get studyShowEvalBar => 'Шкалы оценки'; + + @override + String get studyFirst => 'Первая'; + + @override + String get studyPrevious => 'Назад'; + + @override + String get studyNext => 'Дальше'; + + @override + String get studyLast => 'Последняя'; + @override String get studyShareAndExport => 'Поделиться и экспортировать'; + @override + String get studyCloneStudy => 'Клонировать'; + + @override + String get studyStudyPgn => 'PGN студии'; + + @override + String get studyDownloadAllGames => 'Скачать все партии'; + + @override + String get studyChapterPgn => 'PGN главы'; + + @override + String get studyCopyChapterPgn => 'Копировать PGN'; + + @override + String get studyDownloadGame => 'Скачать партию'; + + @override + String get studyStudyUrl => 'Ссылка на студию'; + + @override + String get studyCurrentChapterUrl => 'Ссылка на эту главу'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Вставьте этот код на форум для вставки'; + + @override + String get studyStartAtInitialPosition => 'Открыть в начальной позиции'; + + @override + String studyStartAtX(String param) { + return 'Начать с $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вставить в свой сайт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Подробнее о вставке на сайт'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Вставлять на сайт можно только публичные студии!'; + + @override + String get studyOpen => 'Открыть'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 на $param2'; + } + + @override + String get studyStudyNotFound => 'Студия не найдена'; + + @override + String get studyEditChapter => 'Редактировать главу'; + + @override + String get studyNewChapter => 'Новая глава'; + + @override + String studyImportFromChapterX(String param) { + return 'Импорт из $param'; + } + + @override + String get studyOrientation => 'Ориентация'; + + @override + String get studyAnalysisMode => 'Режим анализа'; + + @override + String get studyPinnedChapterComment => 'Закреплённый комментарий главы'; + + @override + String get studySaveChapter => 'Сохранить главу'; + + @override + String get studyClearAnnotations => 'Очистить аннотацию'; + + @override + String get studyClearVariations => 'Очистить варианты'; + + @override + String get studyDeleteChapter => 'Удалить главу'; + + @override + String get studyDeleteThisChapter => 'Удалить эту главу? Её нельзя будет вернуть!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Очистить все комментарии и обозначения этой главы?'; + + @override + String get studyRightUnderTheBoard => 'Прямо под доской'; + + @override + String get studyNoPinnedComment => 'Нет'; + + @override + String get studyNormalAnalysis => 'Обычный анализ'; + + @override + String get studyHideNextMoves => 'Скрыть последующие ходы'; + + @override + String get studyInteractiveLesson => 'Интерактивный урок'; + + @override + String studyChapterX(String param) { + return 'Глава $param'; + } + + @override + String get studyEmpty => 'Пусто'; + + @override + String get studyStartFromInitialPosition => 'Начать с исходной позиции'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Начать со своей позиции'; + + @override + String get studyLoadAGameByUrl => 'Загрузить игру по URL'; + + @override + String get studyLoadAPositionFromFen => 'Загрузить позицию из FEN'; + + @override + String get studyLoadAGameFromPgn => 'Загрузить игру из PGN'; + + @override + String get studyAutomatic => 'Автоматически'; + + @override + String get studyUrlOfTheGame => 'URL игры'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Загрузить игру из $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Создать главу'; + + @override + String get studyCreateStudy => 'Создать студию'; + + @override + String get studyEditStudy => 'Изменить студию'; + + @override + String get studyVisibility => 'Доступно к просмотру'; + + @override + String get studyPublic => 'Публичная'; + + @override + String get studyUnlisted => 'Доступ по ссылке'; + + @override + String get studyInviteOnly => 'Только по приглашению'; + + @override + String get studyAllowCloning => 'Разрешить копирование'; + + @override + String get studyNobody => 'Никто'; + + @override + String get studyOnlyMe => 'Только я'; + + @override + String get studyContributors => 'Соавторы'; + + @override + String get studyMembers => 'Участники'; + + @override + String get studyEveryone => 'Все'; + + @override + String get studyEnableSync => 'Включить синхронизацию'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: устанавливать всем одинаковую позицию'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Нет: позволить участникам свободно изучать все позиции'; + + @override + String get studyPinnedStudyComment => 'Закреплённый комментарий студии'; + @override String get studyStart => 'Начать'; + + @override + String get studySave => 'Сохранить'; + + @override + String get studyClearChat => 'Очистить чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Удалить чат студии? Восстановить будет невозможно!'; + + @override + String get studyDeleteStudy => 'Удалить студию'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Удалить всю студию? Удаление необратимо! Введите название студии для подтверждения: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Где вы хотите создать студию?'; + + @override + String get studyGoodMove => 'Хороший ход'; + + @override + String get studyMistake => 'Ошибка'; + + @override + String get studyBrilliantMove => 'Отличный ход'; + + @override + String get studyBlunder => 'Зевок'; + + @override + String get studyInterestingMove => 'Интересный ход'; + + @override + String get studyDubiousMove => 'Сомнительный ход'; + + @override + String get studyOnlyMove => 'Единственный ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Равная позиция'; + + @override + String get studyUnclearPosition => 'Неясная позиция'; + + @override + String get studyWhiteIsSlightlyBetter => 'У белых немного лучше'; + + @override + String get studyBlackIsSlightlyBetter => 'У чёрных немного лучше'; + + @override + String get studyWhiteIsBetter => 'У белых лучше'; + + @override + String get studyBlackIsBetter => 'У чёрных лучше'; + + @override + String get studyWhiteIsWinning => 'Белые побеждают'; + + @override + String get studyBlackIsWinning => 'Чёрные побеждают'; + + @override + String get studyNovelty => 'Новинка'; + + @override + String get studyDevelopment => 'Развитие'; + + @override + String get studyInitiative => 'Инициатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контригра'; + + @override + String get studyTimeTrouble => 'Цейтнот'; + + @override + String get studyWithCompensation => 'С компенсацией'; + + @override + String get studyWithTheIdea => 'С идеей'; + + @override + String get studyNextChapter => 'Следующая глава'; + + @override + String get studyPrevChapter => 'Предыдущая глава'; + + @override + String get studyStudyActions => 'Действия в студии'; + + @override + String get studyTopics => 'Темы'; + + @override + String get studyMyTopics => 'Мои темы'; + + @override + String get studyPopularTopics => 'Популярные темы'; + + @override + String get studyManageTopics => 'Управление темами'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Сыграть снова'; + + @override + String get studyWhatWouldYouPlay => 'Как бы вы сыграли в этой позиции?'; + + @override + String get studyYouCompletedThisLesson => 'Поздравляем! Вы прошли этот урок.'; + + @override + String studyPerPage(String param) { + return '$param на страницу'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count глав', + many: '$count глав', + few: '$count главы', + one: '$count глава', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count партий', + many: '$count партий', + few: '$count партии', + one: '$count партия', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count участников', + many: '$count участников', + few: '$count участника', + one: '$count участник', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Вставьте текст в формате PGN, не больше $count игр', + many: 'Вставьте текст в формате PGN, не больше $count игр', + few: 'Вставьте текст в формате PGN, не больше $count игр', + one: 'Вставьте текст в формате PGN, не больше $count игры', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'только что'; + + @override + String get timeagoRightNow => 'прямо сейчас'; + + @override + String get timeagoCompleted => 'завершено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count секунд', + many: 'через $count секунд', + few: 'через $count секунды', + one: 'через $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count минут', + many: 'через $count минут', + few: 'через $count минуты', + one: 'через $count минуту', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count часов', + many: 'через $count часов', + few: 'через $count часа', + one: 'через $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count дней', + many: 'через $count дней', + few: 'через $count дня', + one: 'через $count день', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count недель', + many: 'через $count недель', + few: 'через $count недели', + one: 'через $count неделю', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count месяцев', + many: 'через $count месяцев', + few: 'через $count месяца', + one: 'через $count месяц', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count лет', + many: 'через $count лет', + few: 'через $count года', + one: 'через $count год', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минут назад', + many: '$count минут назад', + few: '$count минуты назад', + one: '$count минуту назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count часов назад', + many: '$count часов назад', + few: '$count часа назад', + one: '$count час назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дней назад', + many: '$count дней назад', + few: '$count дня назад', + one: '$count день назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count недель назад', + many: '$count недель назад', + few: '$count недели назад', + one: '$count неделю назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count месяцев назад', + many: '$count месяцев назад', + few: '$count месяца назад', + one: '$count месяц назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count лет назад', + many: '$count лет назад', + few: '$count года назад', + one: '$count год назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'осталось $count минут', + many: 'осталось $count минут', + few: 'осталось $count минуты', + one: 'осталась $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'осталось $count часов', + many: 'осталось $count часов', + few: 'осталось $count часа', + one: 'остался $count час', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index ef0e4eec53..ffe2584cea 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsSk extends AppLocalizations { AppLocalizationsSk([String locale = 'sk']) : super(locale); @override - String get mobileHomeTab => 'Domov'; + String get mobileAllGames => 'Všetky partie'; @override - String get mobilePuzzlesTab => 'Úlohy'; + String get mobileAreYouSure => 'Ste si istý?'; @override - String get mobileToolsTab => 'Nástroje'; + String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; @override - String get mobileWatchTab => 'Sledovať'; + String get mobileClearButton => 'Odstrániť'; @override - String get mobileSettingsTab => 'Nastavenia'; + String get mobileCorrespondenceClearSavedMove => 'Vymazať uložený ťah'; @override - String get mobileMustBeLoggedIn => 'Na zobrazenie tejto stránky musíte byť prihlásený.'; + String get mobileCustomGameJoinAGame => 'Pripojiť sa k partii'; @override - String get mobileSystemColors => 'Farby operačného systému'; + String get mobileFeedbackButton => 'Spätná väzba'; @override - String get mobileFeedbackButton => 'Spätná väzba'; + String mobileGreeting(String param) { + return 'Ahoj, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Ahoj'; @override - String get mobileSettingsHapticFeedback => 'Vibrovanie zariadenia'; + String get mobileHideVariation => 'Skryť varianty'; @override - String get mobileSettingsImmersiveMode => 'Režim celej obrazovky'; + String get mobileHomeTab => 'Domov'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.'; + String get mobileLiveStreamers => 'Vysielajúci strímeri'; @override - String get mobileNotFollowingAnyUser => 'Nesledujete žiadneho používateľa.'; + String get mobileMustBeLoggedIn => 'Na zobrazenie tejto stránky musíte byť prihlásený.'; @override - String get mobileAllGames => 'Všetky partie'; + String get mobileNoSearchResults => 'Nič sa nenašlo'; @override - String get mobileRecentSearches => 'Posledné vyhľadávania'; + String get mobileNotFollowingAnyUser => 'Nesledujete žiadneho používateľa.'; @override - String get mobileClearButton => 'Odstrániť'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nič sa nenašlo'; + String get mobilePrefMagnifyDraggedPiece => 'Zväčšiť uchopenú figúrku'; @override - String get mobileAreYouSure => 'Ste si istý?'; + String get mobilePuzzleStormConfirmEndRun => 'Chcete ukončiť tento pokus?'; @override - String get mobilePuzzleStreakAbortWarning => 'Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niet čo zobraziť, prosím, zmeňte filtre'; @override String get mobilePuzzleStormNothingToShow => 'Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Zdieľať túto úlohu'; + String get mobilePuzzleStormSubtitle => 'Vyriešte čo najviac úloh za 3 minúty.'; @override - String get mobileShareGameURL => 'Zdieľať URL partie'; + String get mobilePuzzleStreakAbortWarning => 'Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.'; @override - String get mobileShareGamePGN => 'Zdieľať PGN'; + String get mobilePuzzleThemesSubtitle => 'Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.'; @override - String get mobileSharePositionAsFEN => 'Zdieľať pozíciu vo formáte FEN'; + String get mobilePuzzlesTab => 'Úlohy'; @override - String get mobileShowVariations => 'Zobraziť varianty'; + String get mobileRecentSearches => 'Posledné vyhľadávania'; @override - String get mobileHideVariation => 'Skryť varianty'; + String get mobileSettingsHapticFeedback => 'Vibrovanie zariadenia'; @override - String get mobileShowComments => 'Zobraziť komentáre'; + String get mobileSettingsImmersiveMode => 'Režim celej obrazovky'; @override - String get mobilePuzzleStormConfirmEndRun => 'Chcete ukončiť tento pokus?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Niet čo zobraziť, prosím, zmeňte filtre'; + String get mobileSettingsTab => 'Nastavenia'; @override - String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; + String get mobileShareGamePGN => 'Zdieľať PGN'; @override - String get mobileCancelDrawOffer => 'Zrušiť navrhnutie remízy'; + String get mobileShareGameURL => 'Zdieľať URL partie'; @override - String get mobileWaitingForOpponentToJoin => 'Čaká sa na pripojenie súpera...'; + String get mobileSharePositionAsFEN => 'Zdieľať pozíciu vo formáte FEN'; @override - String get mobileBlindfoldMode => 'Naslepo'; + String get mobileSharePuzzle => 'Zdieľať túto úlohu'; @override - String get mobileLiveStreamers => 'Vysielajúci strímeri'; + String get mobileShowComments => 'Zobraziť komentáre'; @override - String get mobileCustomGameJoinAGame => 'Pripojiť sa k partii'; + String get mobileShowResult => 'Zobraziť výsledok'; @override - String get mobileCorrespondenceClearSavedMove => 'Vymazať uložený ťah'; + String get mobileShowVariations => 'Zobraziť varianty'; @override String get mobileSomethingWentWrong => 'Došlo k chybe.'; @override - String get mobileShowResult => 'Zobraziť výsledok'; - - @override - String get mobilePuzzleThemesSubtitle => 'Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.'; + String get mobileSystemColors => 'Farby operačného systému'; @override - String get mobilePuzzleStormSubtitle => 'Vyriešte čo najviac úloh za 3 minúty.'; + String get mobileTheme => 'Vzhľad'; @override - String mobileGreeting(String param) { - return 'Ahoj, $param'; - } + String get mobileToolsTab => 'Nástroje'; @override - String get mobileGreetingWithoutName => 'Ahoj'; + String get mobileWaitingForOpponentToJoin => 'Čaká sa na pripojenie súpera...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Sledovať'; @override String get activityActivity => 'Aktivita'; @@ -262,6 +259,19 @@ class AppLocalizationsSk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Odohraných $count $param2 korešpondenčných partií', + many: 'Odohraných $count $param2 korešpondenčných partií', + few: 'Odohrané $count $param2 korešpondenčné partie', + one: 'Odohraná $count $param2 korešpondenčná partia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsSk extends AppLocalizations { @override String get broadcastBroadcasts => 'Vysielanie'; + @override + String get broadcastMyBroadcasts => 'Moje vysielania'; + @override String get broadcastLiveBroadcasts => 'Živé vysielanie turnaja'; + @override + String get broadcastBroadcastCalendar => 'Kalendár vysielaní'; + + @override + String get broadcastNewBroadcast => 'Nové živé vysielanie'; + + @override + String get broadcastSubscribedBroadcasts => 'Odoberané vysielania'; + + @override + String get broadcastAboutBroadcasts => 'O vysielaní'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Ako používať Lichess vysielanie.'; + + @override + String get broadcastTheNewRoundHelp => 'Nové kolo bude mať tých istých členov a prispievateľov ako to predchádzajúce.'; + + @override + String get broadcastAddRound => 'Pridať kolo'; + + @override + String get broadcastOngoing => 'Prebiehajúci'; + + @override + String get broadcastUpcoming => 'Blížiace sa'; + + @override + String get broadcastCompleted => 'Ukončené'; + + @override + String get broadcastCompletedHelp => 'Lichess rozpozná dokončenie kola, ale môže sa pomýliť. Pomocou tejto funkcie ho môžete nastaviť ručne.'; + + @override + String get broadcastRoundName => 'Názov kola'; + + @override + String get broadcastRoundNumber => 'Číslo kola'; + + @override + String get broadcastTournamentName => 'Názov turnaja'; + + @override + String get broadcastTournamentDescription => 'Krátky popis turnaja'; + + @override + String get broadcastFullDescription => 'Úplný popis turnaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Voliteľný dlhý popis vysielania. $param1 je dostupný. Dĺžka musí byť menej ako $param2 znakov.'; + } + + @override + String get broadcastSourceSingleUrl => 'Zdrojová URL pre PGN súbor'; + + @override + String get broadcastSourceUrlHelp => 'URL, ktorú bude Lichess kontrolovať, aby získal aktualizácie PGN. Musí byť verejne prístupná z internetu.'; + + @override + String get broadcastSourceGameIds => 'Až do 64 identifikátorov Lichess partií oddelených medzerami.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Dátum začiatku v miestnej časovej zóne turnaja: $param'; + } + + @override + String get broadcastStartDateHelp => 'Voliteľné, ak viete kedy sa udalosť začne'; + + @override + String get broadcastCurrentGameUrl => 'Adresa URL aktuálnej partie'; + + @override + String get broadcastDownloadAllRounds => 'Stiahnuť všetky kolá'; + + @override + String get broadcastResetRound => 'Resetovať toto kolo'; + + @override + String get broadcastDeleteRound => 'Vymazať toto kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitívne vymazať kolo a partie tohto kola.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Vymazať všetky partie tohto kola. K opätovnému vytvoreniu partií bude potrebné aby bol zdroj aktívny.'; + + @override + String get broadcastEditRoundStudy => 'Upraviť kolo štúdií'; + + @override + String get broadcastDeleteTournament => 'Vymazať tento turnaj'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitívne odstrániť celý turnaj so všetkými kolami a všetkými partiami.'; + + @override + String get broadcastShowScores => 'Zobraziť skóre hráčov na základe výsledkov partií'; + + @override + String get broadcastReplacePlayerTags => 'Voliteľné: nahradiť mená hráčov, hodnotenia a tituly'; + + @override + String get broadcastFideFederations => 'FIDE federácie'; + + @override + String get broadcastTop10Rating => '10 najlepšie hodnotených'; + + @override + String get broadcastFidePlayers => 'FIDE šachisti'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE šachista sa nenašiel'; + + @override + String get broadcastFideProfile => 'FIDE profil'; + + @override + String get broadcastFederation => 'Federácia'; + + @override + String get broadcastAgeThisYear => 'Vek tento rok'; + + @override + String get broadcastUnrated => 'Bez hodnotenia'; + + @override + String get broadcastRecentTournaments => 'Posledné turnaje'; + + @override + String get broadcastOpenLichess => 'Otvoriť na Lichess'; + + @override + String get broadcastTeams => 'Tímy'; + + @override + String get broadcastBoards => 'Šachovnice'; + + @override + String get broadcastOverview => 'Prehľad'; + + @override + String get broadcastSubscribeTitle => 'Prihláste sa, aby ste boli informovaní o začiatku každého kola. V nastaveniach účtu môžete prepnúť zvončekové alebo push upozornenia na vysielanie.'; + + @override + String get broadcastUploadImage => 'Nahrať obrázok pre turnaj'; + + @override + String get broadcastNoBoardsYet => 'Zatiaľ žiadne šachovnice. Objavia sa po nahratí partií.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Šachovnice možno načítať pomocou zdroja alebo pomocou $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Začína po $param'; + } + + @override + String get broadcastStartVerySoon => 'Vysielanie sa začne čoskoro.'; + + @override + String get broadcastNotYetStarted => 'Vysielanie sa ešte nezačalo.'; + + @override + String get broadcastOfficialWebsite => 'Oficiálna webstránka'; + + @override + String get broadcastStandings => 'Poradie'; + + @override + String get broadcastOfficialStandings => 'Oficiálne poradie'; + + @override + String broadcastIframeHelp(String param) { + return 'Viac možností nájdete na $param'; + } + + @override + String get broadcastWebmastersPage => 'stránke tvorcu'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Verejný zdroj PGN v reálnom čase pre toto kolo. Ponúkame tiež $param na rýchlejšiu a efektívnejšiu synchronizáciu.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Vložiť toto vysielanie na webovú stránku'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Vložiť $param na webovú stránku'; + } + + @override + String get broadcastRatingDiff => 'Ratingový rozdiel'; + + @override + String get broadcastGamesThisTournament => 'Partie tohto turnaja'; + + @override + String get broadcastScore => 'Skóre'; + + @override + String get broadcastAllTeams => 'Všetky tímy'; + + @override + String get broadcastTournamentFormat => 'Formát turnaja'; + + @override + String get broadcastTournamentLocation => 'Miesto konania turnaja'; + + @override + String get broadcastTopPlayers => 'Najlepší hráči'; + + @override + String get broadcastTimezone => 'Časové pásmo'; + + @override + String get broadcastFideRatingCategory => 'Kategória FIDE ratingu'; + + @override + String get broadcastOptionalDetails => 'Nepovinné údaje'; + + @override + String get broadcastPastBroadcasts => 'Predchádzajúce vysielania'; + + @override + String get broadcastAllBroadcastsByMonth => 'Zobraziť všetky vysielania podľa mesiacov'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vysielaní', + many: '$count vysielaní', + few: '$count vysielania', + one: '$count vysielanie', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Výzvy: $param1'; @@ -643,6 +902,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get preferencesInGameOnly => 'Iba pri partii'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šachové hodiny'; @@ -784,6 +1046,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvuk upozornenia'; + @override + String get preferencesBlindfold => 'Naslepo'; + @override String get puzzlePuzzles => 'Šachové úlohy'; @@ -1434,10 +1699,10 @@ class AppLocalizationsSk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Súper je limitovaný vo svojich ťahoch a každý ťah zhorší jeho pozíciu.'; @override - String get puzzleThemeHealthyMix => 'Zdravý mix'; + String get puzzleThemeMix => 'Zdravá zmes'; @override - String get puzzleThemeHealthyMixDescription => 'Zmes úloh. Neviete čo očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.'; + String get puzzleThemeMixDescription => 'Od všetkého trochu. Neviete, čo môžete očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.'; @override String get puzzleThemePlayerGames => 'Vaše partie'; @@ -1811,9 +2076,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get byCPL => 'CHYBY'; - @override - String get openStudy => 'Otvoriť štúdie'; - @override String get enable => 'Povoliť analýzu'; @@ -1841,9 +2103,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get removesTheDepthLimit => 'Odstráni obmedzenie hĺbky analýzy a spôsobí zahrievanie Vášho počítača'; - @override - String get engineManager => 'Správa motorov'; - @override String get blunder => 'Hrubá chyba'; @@ -2107,6 +2366,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get gamesPlayed => 'Odohraných partií'; + @override + String get ok => 'OK'; + @override String get cancel => 'Zrušiť'; @@ -2481,9 +2743,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get unblock => 'Odblokovať'; - @override - String get followsYou => 'Sleduje Vás'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 začal sledovať $param2'; @@ -2816,7 +3075,13 @@ class AppLocalizationsSk extends AppLocalizations { String get other => 'Iné'; @override - String get reportDescriptionHelp => 'Vložte odkaz na hru/y, a vysvetlite, čo je zlé na tomto správaní používateľa.'; + String get reportCheatBoostHelp => 'Vložte odkaz na partiu(/e) a vysvetlite, čo je na správaní tohto používateľa zlé. Nehovorte len „podvádza“, ale povedzte, ako ste k tomuto záveru dospeli.'; + + @override + String get reportUsernameHelp => 'Vysvetlite, čo je na tomto používateľskom mene urážlivé. Nehovorte len „je to urážlivé/nevhodné“, ale povedzte nám, ako ste k tomuto záveru dospeli, najmä ak je urážka významovo zahmlená, nie je v angličtine, je v slangu alebo odkazuje na niečo z historie/kultúry.'; + + @override + String get reportProcessedFasterInEnglish => 'Vaša správa bude spracovaná rýchlejšie, ak bude napísaná v angličtine.'; @override String get error_provideOneCheatedGameLink => 'Prosím, uveďte aspoň jeden odkaz na partiu, v ktorej sa podvádzalo.'; @@ -4121,6 +4386,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get nothingToSeeHere => 'Momentálne tu nie je nič k zobrazeniu.'; + @override + String get stats => 'Štatistiky'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5123,731 @@ class AppLocalizationsSk extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streameri'; + @override + String get studyPrivate => 'Súkromné'; + + @override + String get studyMyStudies => 'Moje štúdie'; + + @override + String get studyStudiesIContributeTo => 'Učivo, ku ktorému prispievam'; + + @override + String get studyMyPublicStudies => 'Moje verejné štúdie'; + + @override + String get studyMyPrivateStudies => 'Moje súkromné učivo'; + + @override + String get studyMyFavoriteStudies => 'Moje obľúbené štúdie'; + + @override + String get studyWhatAreStudies => 'Čo sú štúdie?'; + + @override + String get studyAllStudies => 'Všetko učivo'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Štúdie vytvorené $param'; + } + + @override + String get studyNoneYet => 'Zatiaľ žiadne.'; + + @override + String get studyHot => 'Teraz populárne'; + + @override + String get studyDateAddedNewest => 'Dátum pridania (najnovšie)'; + + @override + String get studyDateAddedOldest => 'Dátum pridania (najstaršie)'; + + @override + String get studyRecentlyUpdated => 'Nedávno aktualizované'; + + @override + String get studyMostPopular => 'Najpopulárnejšie'; + + @override + String get studyAlphabetical => 'Abecedne'; + + @override + String get studyAddNewChapter => 'Pridať novú kapitolu'; + + @override + String get studyAddMembers => 'Pridať členov'; + + @override + String get studyInviteToTheStudy => 'Pozvať k štúdii'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosím pozývajte iba ľudí ktorých poznáte a o ktorých viete, že chcú túto štúdiu vidieť.'; + + @override + String get studySearchByUsername => 'Hľadať podľa použív. mena'; + + @override + String get studySpectator => 'Divák'; + + @override + String get studyContributor => 'Prispievateľ'; + + @override + String get studyKick => 'Vyhodiť'; + + @override + String get studyLeaveTheStudy => 'Opustiť štúdiu'; + + @override + String get studyYouAreNowAContributor => 'Od teraz ste prispievateľom'; + + @override + String get studyYouAreNowASpectator => 'Od teraz ste divákom'; + + @override + String get studyPgnTags => 'PGN značka'; + + @override + String get studyLike => 'Páči sa mi'; + + @override + String get studyUnlike => 'Nepáči sa mi'; + + @override + String get studyNewTag => 'Nová značka'; + + @override + String get studyCommentThisPosition => 'Komentovať túto pozíciu'; + + @override + String get studyCommentThisMove => 'Komentovať tento ťah'; + + @override + String get studyAnnotateWithGlyphs => 'Anotovať pomocou glyphov'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitola je príliš krátka na analýzu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Oba prispievatelia k tejto štúdii môžu požiadať a počítačovú analýzu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Získajte úplnú počítačovú analýzu hlavného variantu na strane servera.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Uistite sa, že kapitola je kompletná. Požiadať o analýzu môžete iba raz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Všetci zosynchronizovaní členovia uvidia rovnakú pozíciu'; + + @override + String get studyShareChanges => 'Zdieľajte zmeny s divákmi a uložte ich na server'; + + @override + String get studyPlaying => 'Práve sa hrá'; + + @override + String get studyShowEvalBar => 'Ukazovatele hodnotenia'; + + @override + String get studyFirst => 'Prvá'; + + @override + String get studyPrevious => 'Späť'; + + @override + String get studyNext => 'Ďalej'; + + @override + String get studyLast => 'Posledná'; + @override String get studyShareAndExport => 'Zdielať & export'; + @override + String get studyCloneStudy => 'Naklonovať'; + + @override + String get studyStudyPgn => 'PGN štúdie'; + + @override + String get studyDownloadAllGames => 'Stiahnuť všetky partie'; + + @override + String get studyChapterPgn => 'PGN kapitoly'; + + @override + String get studyCopyChapterPgn => 'Kopírovať PGN'; + + @override + String get studyDownloadGame => 'Stiahnúť hru'; + + @override + String get studyStudyUrl => 'URL štúdie'; + + @override + String get studyCurrentChapterUrl => 'URL aktuálnej kapitoly'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vložte pre zobrazenie vo fóre'; + + @override + String get studyStartAtInitialPosition => 'Začať zo základného postavenia'; + + @override + String studyStartAtX(String param) { + return 'Začať na $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vložte na svoju webstránku alebo blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Prečítajte si viac o vkladaní'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vložené môžu byť iba verejné štúdie!'; + + @override + String get studyOpen => 'Otvoriť'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 Vám priniesol $param2'; + } + + @override + String get studyStudyNotFound => 'Štúdia sa nenašla'; + + @override + String get studyEditChapter => 'Upraviť kapitolu'; + + @override + String get studyNewChapter => 'Nová kapitola'; + + @override + String studyImportFromChapterX(String param) { + return 'Importovať z $param'; + } + + @override + String get studyOrientation => 'Orientácia'; + + @override + String get studyAnalysisMode => 'Mód analýzy'; + + @override + String get studyPinnedChapterComment => 'Pripnutý komentár ku kapitole'; + + @override + String get studySaveChapter => 'Uložiť kapitolu'; + + @override + String get studyClearAnnotations => 'Vymazať anotácie'; + + @override + String get studyClearVariations => 'Vymazať varianty'; + + @override + String get studyDeleteChapter => 'Vymazať kapitolu'; + + @override + String get studyDeleteThisChapter => 'Chcete vymazať túto kapitolu? Táto akcia sa nedá vratit späť!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vymazať všetky komentáre, piktogramy a nakreslené tvary v tejto kapitole?'; + + @override + String get studyRightUnderTheBoard => 'Pod hraciu dosku'; + + @override + String get studyNoPinnedComment => 'Nikam'; + + @override + String get studyNormalAnalysis => 'Normálna analýza'; + + @override + String get studyHideNextMoves => 'Skryť nasledujuce ťahy'; + + @override + String get studyInteractiveLesson => 'Interaktívna lekcia'; + + @override + String studyChapterX(String param) { + return 'Kapitola $param'; + } + + @override + String get studyEmpty => 'Prázdne'; + + @override + String get studyStartFromInitialPosition => 'Začať z počiatočnej pozície'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Začať z vlastnej pozície'; + + @override + String get studyLoadAGameByUrl => 'Načítať hru z URL'; + + @override + String get studyLoadAPositionFromFen => 'Načítať pozíciu z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Načítať hru z PGN'; + + @override + String get studyAutomatic => 'Automatická'; + + @override + String get studyUrlOfTheGame => 'URL hry'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Načítať hru z $param1 alebo z $param2'; + } + + @override + String get studyCreateChapter => 'Vytvoriť kapitolu'; + + @override + String get studyCreateStudy => 'Vytvoriť štúdiu'; + + @override + String get studyEditStudy => 'Upraviť učivo'; + + @override + String get studyVisibility => 'Viditeľnosť'; + + @override + String get studyPublic => 'Verejné'; + + @override + String get studyUnlisted => 'Nezapísané'; + + @override + String get studyInviteOnly => 'Iba na pozvanie'; + + @override + String get studyAllowCloning => 'Povoliť klonovanie'; + + @override + String get studyNobody => 'Nikto'; + + @override + String get studyOnlyMe => 'Iba ja'; + + @override + String get studyContributors => 'Prispievatelia'; + + @override + String get studyMembers => 'Členovia'; + + @override + String get studyEveryone => 'Všetci'; + + @override + String get studyEnableSync => 'Povoliť synchronizáciu'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ano: ponechajte každého na tej istej pozícii'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: povoľte ľudom voľne prehľadávať'; + + @override + String get studyPinnedStudyComment => 'Pripnutý komentár k učivu'; + @override String get studyStart => 'Štart'; + + @override + String get studySave => 'Uložiť'; + + @override + String get studyClearChat => 'Vymazať čet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Definitívne vymazať históriu četu k tejto štúdii? Táto akcia sa nedá vrátit späť!'; + + @override + String get studyDeleteStudy => 'Vymazať štúdiu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Definitívne vymazať štúdiu? Táto akcia sa nedá vrátit späť! Napíšte názov štúdie pre potvrdenie: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kde to chcete študovať?'; + + @override + String get studyGoodMove => 'Dobrý ťah'; + + @override + String get studyMistake => 'Chyba'; + + @override + String get studyBrilliantMove => 'Veľmi dobrý ťah'; + + @override + String get studyBlunder => 'Hrubá chyba'; + + @override + String get studyInterestingMove => 'Zaujímavý ťah'; + + @override + String get studyDubiousMove => 'Pochybný ťah'; + + @override + String get studyOnlyMove => 'Jediný možný ťah'; + + @override + String get studyZugzwang => 'Nevýhoda ťahu'; + + @override + String get studyEqualPosition => 'Rovnocenná pozícia'; + + @override + String get studyUnclearPosition => 'Nejasná pozícia'; + + @override + String get studyWhiteIsSlightlyBetter => 'Biely stojí o trochu lepšie'; + + @override + String get studyBlackIsSlightlyBetter => 'Čierny stojí o trochu lepšie'; + + @override + String get studyWhiteIsBetter => 'Biely stojí lepšie'; + + @override + String get studyBlackIsBetter => 'Čierny stojí lepšie'; + + @override + String get studyWhiteIsWinning => 'Biely stojí na výhru'; + + @override + String get studyBlackIsWinning => 'Čierny stojí na výhru'; + + @override + String get studyNovelty => 'Novinka'; + + @override + String get studyDevelopment => 'Vývin'; + + @override + String get studyInitiative => 'Iniciatíva'; + + @override + String get studyAttack => 'Útok'; + + @override + String get studyCounterplay => 'Protiútok'; + + @override + String get studyTimeTrouble => 'Časová tieseň'; + + @override + String get studyWithCompensation => 'S výhodou'; + + @override + String get studyWithTheIdea => 'S myšlienkou'; + + @override + String get studyNextChapter => 'Ďalšia kapitola'; + + @override + String get studyPrevChapter => 'Predchádzajúca kapitola'; + + @override + String get studyStudyActions => 'Úkony pri štúdii'; + + @override + String get studyTopics => 'Témy'; + + @override + String get studyMyTopics => 'Moje témy'; + + @override + String get studyPopularTopics => 'Populárne témy'; + + @override + String get studyManageTopics => 'Spravovať témy'; + + @override + String get studyBack => 'Späť'; + + @override + String get studyPlayAgain => 'Hrať znova'; + + @override + String get studyWhatWouldYouPlay => 'Čo by ste hrali v tejto pozícii?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulujeme! Túto lekciu ste ukončili.'; + + @override + String studyPerPage(String param) { + return '$param na stránku'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitol', + many: '$count Kapitol', + few: '$count Kapitoly', + one: '$count Kapitola', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partií', + many: '$count Partií', + few: '$count Partie', + one: '$count Partia', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Členov', + many: '$count Členov', + few: '$count Členovia', + one: '$count Člen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Váš PGN text vložte sem, maximálne $count partií', + many: 'Váš PGN text vložte sem, maximálne $count partií', + few: 'Váš PGN text vložte sem, maximálne $count partie', + one: 'Váš PGN text vložte sem, maximálne $count partiu', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'práve teraz'; + + @override + String get timeagoRightNow => 'práve teraz'; + + @override + String get timeagoCompleted => 'ukončené'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count sekúnd', + many: 'o $count sekúnd', + few: 'o $count sekundy', + one: 'o $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count minút', + many: 'o $count minút', + few: 'o $count minút', + one: 'o $count minútu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count hodín', + many: 'o $count hodín', + few: 'o $count hodiny', + one: 'o $count hodinu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count dní', + many: 'o $count dní', + few: 'o $count dni', + one: 'o $count deň', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count týždňov', + many: 'o $count týždňov', + few: 'o $count týždne', + one: 'o $count týždeň', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count mesiacov', + many: 'o $count mesiacov', + few: 'o $count mesiace', + one: 'o $count mesiac', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count rokov', + many: 'o $count rokov', + few: 'o $count roky', + one: 'o $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count minútami', + many: 'pred $count minútami', + few: 'pred $count minútami', + one: 'pred $count minútou', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count hodinami', + many: 'pred $count hodinami', + few: 'pred $count hodinami', + one: 'pred $count hodinou', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count dňami', + many: 'pred $count dňami', + few: 'pred $count dňami', + one: 'pred $count dňom', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count týždňami', + many: 'pred $count týždňami', + few: 'pred $count týždňami', + one: 'pred $count týždňom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count mesiacmi', + many: 'pred $count mesiacmi', + few: 'pred $count mesiacmi', + one: 'pred $count mesiacom', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count rokmi', + many: 'pred $count rokmi', + few: 'pred $count rokmi', + one: 'pred $count rokom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ostáva $count minút', + many: 'ostáva $count minút', + few: 'ostávajú $count minúty', + one: 'ostáva $count minúta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ostáva $count hodín', + many: 'ostáva $count hodín', + few: 'ostávajú $count hodiny', + one: 'ostáva $count hodina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index dfc348e8b8..d68604b378 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsSl extends AppLocalizations { AppLocalizationsSl([String locale = 'sl']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Povratne informacije'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Pozdravljeni $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Živjo'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Domov'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'Predenj lahko dostopaš do te strani, se je potrebno prijaviti.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Povečaj vlečeno figuro'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'V 3 minutah rešite čim več ugank.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Problemi'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Nastavitve'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Pokaži rezultat'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Pokaži rezultat'; - - @override - String get mobilePuzzleThemesSubtitle => 'Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.'; + String get mobileSystemColors => 'Barve sistema'; @override - String get mobilePuzzleStormSubtitle => 'V 3 minutah rešite čim več ugank.'; + String get mobileTheme => 'Tema'; @override - String mobileGreeting(String param) { - return 'Pozdravljeni $param'; - } + String get mobileToolsTab => 'Orodja'; @override - String get mobileGreetingWithoutName => 'Živjo'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Glej'; @override String get activityActivity => 'Aktivnost'; @@ -262,6 +259,19 @@ class AppLocalizationsSl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dokončanih $count $param2 korespondenčnih iger', + few: 'Dokončane $count $param2 korespondenčne igre', + two: 'Dokončani $count $param2 korespondenčni igri', + one: 'Dokončana $count $param2 korespondenčna igra', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsSl extends AppLocalizations { @override String get broadcastBroadcasts => 'Prenosi'; + @override + String get broadcastMyBroadcasts => 'Moje oddajanja'; + @override String get broadcastLiveBroadcasts => 'Prenos turnirjev v živo'; + @override + String get broadcastBroadcastCalendar => 'Koledar oddaj'; + + @override + String get broadcastNewBroadcast => 'Nov prenos v živo'; + + @override + String get broadcastSubscribedBroadcasts => 'Naročene oddaje'; + + @override + String get broadcastAboutBroadcasts => 'O oddaji'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kako uporabljati Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'Novi krog bo imel iste člane in sodelavce kot prejšnji.'; + + @override + String get broadcastAddRound => 'Dodajte krog'; + + @override + String get broadcastOngoing => 'V teku'; + + @override + String get broadcastUpcoming => 'Prihajajoči'; + + @override + String get broadcastCompleted => 'Zaključeno'; + + @override + String get broadcastCompletedHelp => 'Lichess zazna zaključek kroga na podlagi izvornih iger. Uporabite ta preklop, če ni vira.'; + + @override + String get broadcastRoundName => 'Ime kroga'; + + @override + String get broadcastRoundNumber => 'Številka kroga'; + + @override + String get broadcastTournamentName => 'Turnirsko ime'; + + @override + String get broadcastTournamentDescription => 'Kratek opis turnirja'; + + @override + String get broadcastFullDescription => 'Polni opis dogodka'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobvezen dolg opis prenosa. $param1 je na voljo. Dolžina mora biti manjša od $param2 znakov.'; + } + + @override + String get broadcastSourceSingleUrl => 'Vir partije v PGN formatu'; + + @override + String get broadcastSourceUrlHelp => 'URL, ki ga bo Lichess preveril, da bo prejel PGN posodobitve. Javno mora biti dostopen preko interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Začetni datum v lokalnem časovnem pasu turnirja: $param'; + } + + @override + String get broadcastStartDateHelp => 'Izbirno, če veste, kdaj se dogodek začne'; + + @override + String get broadcastCurrentGameUrl => 'URL trenutno igrane igre'; + + @override + String get broadcastDownloadAllRounds => 'Prenesite vse kroge'; + + @override + String get broadcastResetRound => 'Ponastavi ta krog'; + + @override + String get broadcastDeleteRound => 'Izbriši ta krog'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dokončno izbrišite krog in njegove igre.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbriši vse igre tega kroga. Vir bo moral biti aktiven, da jih lahko znova ustvarite.'; + + @override + String get broadcastEditRoundStudy => 'Uredi krog študije'; + + @override + String get broadcastDeleteTournament => 'Zbrišite ta turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre.'; + + @override + String get broadcastShowScores => 'Prikaži rezultate igralcev na podlagi rezultatov igre'; + + @override + String get broadcastReplacePlayerTags => 'Izbirno: zamenjajte imena igralcev, ratinge in nazive'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Uradna lestvica'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'Vse ekipe'; + + @override + String get broadcastTournamentFormat => 'Oblika turnirja'; + + @override + String get broadcastTournamentLocation => 'Lokacija turnirja'; + + @override + String get broadcastTopPlayers => 'Najboljši igralci'; + + @override + String get broadcastTimezone => 'Časovni pas'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating kategorija'; + + @override + String get broadcastOptionalDetails => 'Neobvezne podrobnosti'; + + @override + String get broadcastPastBroadcasts => 'Pretekle oddaje'; + + @override + String get broadcastAllBroadcastsByMonth => 'Oglejte si vse oddaje po mesecih'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count oddaj', + few: '$count oddaje', + two: '$count oddaji', + one: '$count oddaja', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izzivi:$param1'; @@ -643,6 +902,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šahovska ura'; @@ -784,6 +1046,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvok obvestila zvonca'; + @override + String get preferencesBlindfold => 'Šah z zavezanimi očmi'; + @override String get puzzlePuzzles => 'Šahovski problemi'; @@ -1434,10 +1699,10 @@ class AppLocalizationsSl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Nasprotnik ima omejene poteze in vsaka poslabša njegovo pozicijo.'; @override - String get puzzleThemeHealthyMix => 'Zdrava mešanica'; + String get puzzleThemeMix => 'Zdrava mešanica'; @override - String get puzzleThemeHealthyMixDescription => 'Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.'; + String get puzzleThemeMixDescription => 'Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.'; @override String get puzzleThemePlayerGames => 'Igralske igre'; @@ -1811,9 +2076,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get byCPL => 'Za stotinko kmeta'; - @override - String get openStudy => 'Odpri študij'; - @override String get enable => 'Omogoči'; @@ -1821,7 +2083,7 @@ class AppLocalizationsSl extends AppLocalizations { String get bestMoveArrow => 'Puščica najboljše poteze'; @override - String get showVariationArrows => 'Show variation arrows'; + String get showVariationArrows => 'Prikaži puščice z variacijami'; @override String get evaluationGauge => 'Kazalnik ocene'; @@ -1841,9 +2103,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get removesTheDepthLimit => 'Odstrani omejitev globine in ohrani računalnik topel'; - @override - String get engineManager => 'Vodja motorja'; - @override String get blunder => 'Spodrsljaj'; @@ -1922,7 +2181,7 @@ class AppLocalizationsSl extends AppLocalizations { String get friends => 'Prijatelji'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'drugi igralci'; @override String get discussions => 'Pogovori'; @@ -2107,6 +2366,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get gamesPlayed => 'Odigranih iger'; + @override + String get ok => 'V redu'; + @override String get cancel => 'Prekliči'; @@ -2481,9 +2743,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Sledi vam'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je začel slediti $param2'; @@ -2675,7 +2934,7 @@ class AppLocalizationsSl extends AppLocalizations { String get editProfile => 'Uredi profil'; @override - String get realName => 'Real name'; + String get realName => 'Pravo ime'; @override String get setFlair => 'Določite svoj okus'; @@ -2756,10 +3015,10 @@ class AppLocalizationsSl extends AppLocalizations { String get yes => 'Da'; @override - String get website => 'Website'; + String get website => 'Spletna stran'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobilna aplikacija'; @override String get help => 'Pomoč:'; @@ -2816,7 +3075,13 @@ class AppLocalizationsSl extends AppLocalizations { String get other => 'Drugo'; @override - String get reportDescriptionHelp => 'Prilepite povezave do igre (ali iger) in pojasnite kaj je narobe z obnašanjem uporabnika. Ne napišite samo \"uporabnik goljufa\" temveč pojasnite zakaj mislite tako. Prijava bo obdelana hitreje če bo napisana v angleščini.'; + String get reportCheatBoostHelp => 'Prilepite povezavo do igre (ali iger) in pojasnite, kaj je narobe z nasprotnikovim načinom igranja. Ne napišite le, da \"nasprotnik goljufa\", ampak pojasnite, kako ste prišli do te ugotovitve.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Navedite vsaj eno povezavo do igre s primerom goljufanja.'; @@ -4121,6 +4386,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get nothingToSeeHere => 'Tukaj trenutno ni ničesar za videti.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5123,731 @@ class AppLocalizationsSl extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess voditelji prenosa'; + @override + String get studyPrivate => 'Zasebno'; + + @override + String get studyMyStudies => 'Moje študije'; + + @override + String get studyStudiesIContributeTo => 'Študije h katerim prispevam'; + + @override + String get studyMyPublicStudies => 'Moje javne študije'; + + @override + String get studyMyPrivateStudies => 'Moje zasebne študije'; + + @override + String get studyMyFavoriteStudies => 'Moje najljubše študije'; + + @override + String get studyWhatAreStudies => 'Kaj so študije?'; + + @override + String get studyAllStudies => 'Vse študije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Študije, ki jih je ustvaril $param'; + } + + @override + String get studyNoneYet => 'Še nič.'; + + @override + String get studyHot => 'Vroče'; + + @override + String get studyDateAddedNewest => 'Dodano (novejše)'; + + @override + String get studyDateAddedOldest => 'Dodano (starejše)'; + + @override + String get studyRecentlyUpdated => 'Nazadnje objavljeno'; + + @override + String get studyMostPopular => 'Najbolj popularno'; + + @override + String get studyAlphabetical => 'Po abecednem redu'; + + @override + String get studyAddNewChapter => 'Dodaj poglavje'; + + @override + String get studyAddMembers => 'Dodaj člane'; + + @override + String get studyInviteToTheStudy => 'Povabi na študijo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosimo, povabite samo tiste ljudi, ki jih poznate in ki bi se želeli pridružiti tej študiji.'; + + @override + String get studySearchByUsername => 'Iskanje po uporabniškem imenu'; + + @override + String get studySpectator => 'Opazovalec'; + + @override + String get studyContributor => 'Sodelovalec'; + + @override + String get studyKick => 'Odstrani'; + + @override + String get studyLeaveTheStudy => 'Zapusti študijo'; + + @override + String get studyYouAreNowAContributor => 'Ste nov sodelovalec'; + + @override + String get studyYouAreNowASpectator => 'Sedaj ste opazovalec'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Všečkaj'; + + @override + String get studyUnlike => 'Ni mi všeč'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentiraj to pozicijo'; + + @override + String get studyCommentThisMove => 'Komentiraj to potezo'; + + @override + String get studyAnnotateWithGlyphs => 'Označi s simbolom'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'To poglavje je prekratko, da bi se analiziralo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo sodelovalci v študiji lahko zahtevajo računalniško analizo.'; + + @override + String get studyGetAFullComputerAnalysis => 'Pridobi na računalniškem strežniku izvedeno računalniško analizo glavne varjante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Poskrbite, da bo poglavje zaključeno. Analizo lahko zahtevate samo enkrat.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Vsi sinhronizirani člani so v isti poziciji'; + + @override + String get studyShareChanges => 'Deli spremembe z gledalci in jih shrani na strežnik'; + + @override + String get studyPlaying => 'V teku'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Prva stran'; + + @override + String get studyPrevious => 'Prejšnja stran'; + + @override + String get studyNext => 'Naslednja stran'; + + @override + String get studyLast => 'Zadnja stran'; + @override String get studyShareAndExport => 'Deli in Izvozi podatke'; + @override + String get studyCloneStudy => 'Kloniraj'; + + @override + String get studyStudyPgn => 'PGN študije'; + + @override + String get studyDownloadAllGames => 'Prenesi vse igre'; + + @override + String get studyChapterPgn => 'PGN poglavja'; + + @override + String get studyCopyChapterPgn => 'Kopiraj PGN'; + + @override + String get studyDownloadGame => 'Prenesi igro'; + + @override + String get studyStudyUrl => 'URL študije'; + + @override + String get studyCurrentChapterUrl => 'URL trenutnega poglavja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'To lahko prilepite na forum, da vstavite'; + + @override + String get studyStartAtInitialPosition => 'Začni v začetni poziciji'; + + @override + String studyStartAtX(String param) { + return 'Začni z $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vstavite v vašo spletno stran ali blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Preberite več o vstavljanju'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vdelati je mogoče le javni študij!'; + + @override + String get studyOpen => 'Odpri'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam ponuja $param2'; + } + + @override + String get studyStudyNotFound => 'Študije nismo našli'; + + @override + String get studyEditChapter => 'Uredi poglavje'; + + @override + String get studyNewChapter => 'Novo poglavje'; + + @override + String studyImportFromChapterX(String param) { + return 'Uvozi iz $param'; + } + + @override + String get studyOrientation => 'Smer'; + + @override + String get studyAnalysisMode => 'Analizni način'; + + @override + String get studyPinnedChapterComment => 'Pripet komentar poglavja'; + + @override + String get studySaveChapter => 'Shrani poglavje'; + + @override + String get studyClearAnnotations => 'Zbriši oznake'; + + @override + String get studyClearVariations => 'Izbriši variante'; + + @override + String get studyDeleteChapter => 'Izbriši poglavje'; + + @override + String get studyDeleteThisChapter => 'Izbriši to poglavje? Poti nazaj ni več!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Izbriši vse komentarje in oblike v tem poglavju?'; + + @override + String get studyRightUnderTheBoard => 'Takoj pod šahovnico'; + + @override + String get studyNoPinnedComment => 'Brez'; + + @override + String get studyNormalAnalysis => 'Običajna analiza'; + + @override + String get studyHideNextMoves => 'Skrij naslednje poteze'; + + @override + String get studyInteractiveLesson => 'Interaktivne lekcije'; + + @override + String studyChapterX(String param) { + return 'Poglavje: $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Začni v začetni poziciji'; + + @override + String get studyEditor => 'Urejevalnik'; + + @override + String get studyStartFromCustomPosition => 'Začni v prilagojeni poziciji'; + + @override + String get studyLoadAGameByUrl => 'Naloži partijo iz URL'; + + @override + String get studyLoadAPositionFromFen => 'Naloži pozicijo iz FEN'; + + @override + String get studyLoadAGameFromPgn => 'Naloži partijo iz PGN'; + + @override + String get studyAutomatic => 'Samodejno'; + + @override + String get studyUrlOfTheGame => 'URL igre'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Naloži partijo iz $param1 ali $param2'; + } + + @override + String get studyCreateChapter => 'Ustvari poglavje'; + + @override + String get studyCreateStudy => 'Ustvarite študijo'; + + @override + String get studyEditStudy => 'Uredite študijo'; + + @override + String get studyVisibility => 'Vidnost'; + + @override + String get studyPublic => 'Javno'; + + @override + String get studyUnlisted => 'Ni na seznamu'; + + @override + String get studyInviteOnly => 'Samo na povabilo'; + + @override + String get studyAllowCloning => 'Dovoli kloniranje'; + + @override + String get studyNobody => 'Nihče'; + + @override + String get studyOnlyMe => 'Samo jaz'; + + @override + String get studyContributors => 'Prispevali so'; + + @override + String get studyMembers => 'Člani'; + + @override + String get studyEveryone => 'Kdorkoli'; + + @override + String get studyEnableSync => 'Omogoči sinhronizacijo'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: vse obdrži v isti poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: naj uporabniki prosto raziskujejo'; + + @override + String get studyPinnedStudyComment => 'Označen komentar študije'; + @override String get studyStart => 'Začni'; + + @override + String get studySave => 'Shrani'; + + @override + String get studyClearChat => 'Počisti klepet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Brisanje zgodovine klepeta? Poti nazaj več ni!'; + + @override + String get studyDeleteStudy => 'Izbriši študijo'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Želite izbrisati celotno študijo? Ni poti nazaj! Za potrditev vnesite ime študije: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kje želite to študirati?'; + + @override + String get studyGoodMove => 'Dobra poteza'; + + @override + String get studyMistake => 'Napakica'; + + @override + String get studyBrilliantMove => 'Briljantna poteza'; + + @override + String get studyBlunder => 'Napaka'; + + @override + String get studyInterestingMove => 'Zanimiva poteza'; + + @override + String get studyDubiousMove => 'Dvomljiva poteza'; + + @override + String get studyOnlyMove => 'Edina poteza'; + + @override + String get studyZugzwang => 'Nujnica'; + + @override + String get studyEqualPosition => 'Enaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Beli je nekoliko boljši'; + + @override + String get studyBlackIsSlightlyBetter => 'Črni je nekoliko boljši'; + + @override + String get studyWhiteIsBetter => 'Beli je boljši'; + + @override + String get studyBlackIsBetter => 'Črni je boljši'; + + @override + String get studyWhiteIsWinning => 'Beli zmaguje'; + + @override + String get studyBlackIsWinning => 'Črni zmaguje'; + + @override + String get studyNovelty => 'Novost'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protinapad'; + + @override + String get studyTimeTrouble => 'Časovna stiska'; + + @override + String get studyWithCompensation => 'S kompenzacijo'; + + @override + String get studyWithTheIdea => 'Z idejo'; + + @override + String get studyNextChapter => 'Naslednje poglavje'; + + @override + String get studyPrevChapter => 'Prejšnje poglavje'; + + @override + String get studyStudyActions => 'Študijske akcije'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Priljubljene teme'; + + @override + String get studyManageTopics => 'Upravljaj teme'; + + @override + String get studyBack => 'Nazaj'; + + @override + String get studyPlayAgain => 'Igrajte ponovno'; + + @override + String get studyWhatWouldYouPlay => 'Kaj bi igrali v tem položaju?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitke! Končali ste to lekcijo.'; + + @override + String studyPerPage(String param) { + return '$param na stran'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count poglavij', + few: '$count Poglavja', + two: '$count Poglavji', + one: '$count Poglavje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partij', + few: '$count Partije', + two: '$count Partiji', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članov', + few: '$count Člani', + two: '$count Člana', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prilepite PGN besedilo, z največ $count partijami', + few: 'Prilepite PGN besedilo, z največ $count partijami', + two: 'Prilepite PGN besedilo, z največ $count partijama', + one: 'Prilepite PGN besedilo, z največ $count partijo', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'pravkar'; + + @override + String get timeagoRightNow => 'ta trenutek'; + + @override + String get timeagoCompleted => 'končano'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count sekund', + few: 'čez $count sekunde', + two: 'čez $count sekundi', + one: 'čez $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count minut', + few: 'čez $count minute', + two: 'čez $count minuti', + one: 'čez $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count ur', + few: 'čez $count ure', + two: 'čez $count uri', + one: 'čez $count uro', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count dni', + few: 'čez $count dnevih', + two: 'čez $count dneva', + one: 'čez $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count tednov', + few: 'čez $count tedne', + two: 'čez $count tedna', + one: 'čez $count teden', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count mesecev', + few: 'čez $count mesece', + two: 'čez $count meseca', + one: 'čez $count mesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count let', + few: 'čez $count leta', + two: 'čez $count leti', + one: 'čez $count leto', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count minutami', + few: 'Pred $count minutami', + two: 'Pred $count minutama', + one: 'Pred $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count urami', + few: 'Pred $count urami', + two: 'Pred $count urama', + one: 'Pred $count uro', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count dnevi', + few: 'Pred $count dnevi', + two: 'Pred $count dnevoma', + one: 'Pred $count dnevom', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count tedni', + few: 'Pred $count tedni', + two: 'Pred $count tednoma', + one: 'Pred $count tednom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count meseci', + few: 'Pred $count meseci', + two: 'Pred $count mesecema', + one: 'Pred $count mesecem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count leti', + few: 'Pred $count leti', + two: 'Pred $count letoma', + one: 'Pred $count letom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'še $count minut', + few: 'še $count minute', + two: 'še $count minuti', + one: 'še $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'še $count ur', + few: 'še $count ure', + two: 'še $count uri', + one: 'še $count ura', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index ee6e39f9de..0be1d08cf9 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsSq extends AppLocalizations { AppLocalizationsSq([String locale = 'sq']) : super(locale); @override - String get mobileHomeTab => 'Kreu'; + String get mobileAllGames => 'Krejt lojërat'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Jeni i sigurt?'; @override - String get mobileToolsTab => 'Mjete'; + String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; @override - String get mobileWatchTab => 'Shiheni'; + String get mobileClearButton => 'Spastroje'; @override - String get mobileSettingsTab => 'Rregullime'; + String get mobileCorrespondenceClearSavedMove => 'Spastroje lëvizjen e ruajtur'; @override - String get mobileMustBeLoggedIn => 'Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.'; + String get mobileCustomGameJoinAGame => 'Merrni pjesë në një lojë'; @override - String get mobileSystemColors => 'Ngjyra sistemi'; + String get mobileFeedbackButton => 'Përshtypje'; @override - String get mobileFeedbackButton => 'Përshtypje'; + String mobileGreeting(String param) { + return 'Tungjatjeta, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Tungjatjeta'; @override - String get mobileSettingsHapticFeedback => 'Dridhje gjatë lëvizjesh'; + String get mobileHideVariation => 'Fshihe variantin'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Kreu'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.'; + String get mobileLiveStreamers => 'Transmetues drejtpërsëdrejti'; @override - String get mobileNotFollowingAnyUser => 'S’ndiqni ndonjë përdorues.'; + String get mobileMustBeLoggedIn => 'Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.'; @override - String get mobileAllGames => 'Krejt lojërat'; + String get mobileNoSearchResults => 'S’ka përfundime'; @override - String get mobileRecentSearches => 'Kërkime së fundi'; + String get mobileNotFollowingAnyUser => 'S’ndiqni ndonjë përdorues.'; @override - String get mobileClearButton => 'Spastroje'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsSq extends AppLocalizations { } @override - String get mobileNoSearchResults => 'S’ka përfundime'; + String get mobilePrefMagnifyDraggedPiece => 'Zmadho gurin e tërhequr'; @override - String get mobileAreYouSure => 'Jeni i sigurt?'; + String get mobilePuzzleStormConfirmEndRun => 'Doni të përfundohen ku raund?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat'; @override String get mobilePuzzleStormNothingToShow => 'S’ka gjë për shfaqje. Luani ndonjë raund Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Ndajeni këtë ushtrim me të tjerët'; + String get mobilePuzzleStormSubtitle => 'Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.'; @override - String get mobileShareGameURL => 'Ndani URL loje me të tjerë'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Ndani PGN me të tjerë'; + String get mobilePuzzleThemesSubtitle => 'Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.'; @override - String get mobileSharePositionAsFEN => 'Tregojuni të tjerëve pozicionin si FEN'; + String get mobilePuzzlesTab => 'Ushtrime'; @override - String get mobileShowVariations => 'Shfaq variante'; + String get mobileRecentSearches => 'Kërkime së fundi'; @override - String get mobileHideVariation => 'Fshihe variantin'; + String get mobileSettingsHapticFeedback => 'Dridhje gjatë lëvizjesh'; @override - String get mobileShowComments => 'Shfaq komente'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Doni të përfundohen ku raund?'; + String get mobileSettingsImmersiveModeSubtitle => 'Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat'; + String get mobileSettingsTab => 'Rregullime'; @override - String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; + String get mobileShareGamePGN => 'Ndani PGN me të tjerë'; @override - String get mobileCancelDrawOffer => 'Anulojeni ofertën për barazim'; + String get mobileShareGameURL => 'Ndani URL loje me të tjerë'; @override - String get mobileWaitingForOpponentToJoin => 'Po pritet që të vijë kundërshtari…'; + String get mobileSharePositionAsFEN => 'Tregojuni të tjerëve pozicionin si FEN'; @override - String get mobileBlindfoldMode => 'Me sytë lidhur'; + String get mobileSharePuzzle => 'Ndajeni këtë ushtrim me të tjerët'; @override - String get mobileLiveStreamers => 'Transmetues drejtpërsëdrejti'; + String get mobileShowComments => 'Shfaq komente'; @override - String get mobileCustomGameJoinAGame => 'Merrni pjesë në një lojë'; + String get mobileShowResult => 'Shfaq përfundimin'; @override - String get mobileCorrespondenceClearSavedMove => 'Spastroje lëvizjen e ruajtur'; + String get mobileShowVariations => 'Shfaq variante'; @override String get mobileSomethingWentWrong => 'Diç shkoi ters.'; @override - String get mobileShowResult => 'Shfaq përfundimin'; - - @override - String get mobilePuzzleThemesSubtitle => 'Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.'; + String get mobileSystemColors => 'Ngjyra sistemi'; @override - String get mobilePuzzleStormSubtitle => 'Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.'; + String get mobileTheme => 'Temë'; @override - String mobileGreeting(String param) { - return 'Tungjatjeta, $param'; - } + String get mobileToolsTab => 'Mjete'; @override - String get mobileGreetingWithoutName => 'Tungjatjeta'; + String get mobileWaitingForOpponentToJoin => 'Po pritet që të vijë kundërshtari…'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Shiheni'; @override String get activityActivity => 'Aktiviteti'; @@ -246,6 +243,17 @@ class AppLocalizationsSq extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,16 +356,263 @@ class AppLocalizationsSq extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmetime'; + @override + String get broadcastMyBroadcasts => 'Transmetimet e mia'; + @override String get broadcastLiveBroadcasts => 'Transmetime të drejtpërdrejta turnesh'; + @override + String get broadcastBroadcastCalendar => 'Kalendar transmetimesh'; + + @override + String get broadcastNewBroadcast => 'Transmetim i ri i drejtpërdrejtë'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmetime me pajtim'; + + @override + String get broadcastAboutBroadcasts => 'Rreth transmetimeve'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Si të përdoren Transmetimet Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi.'; + + @override + String get broadcastAddRound => 'Shtoni një raund'; + + @override + String get broadcastOngoing => 'Në zhvillim'; + + @override + String get broadcastUpcoming => 'I ardhshëm'; + + @override + String get broadcastCompleted => 'I mbaruar'; + + @override + String get broadcastCompletedHelp => 'Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim.'; + + @override + String get broadcastRoundName => 'Emër raundi'; + + @override + String get broadcastRoundNumber => 'Numër raundi'; + + @override + String get broadcastTournamentName => 'Emër turneu'; + + @override + String get broadcastTournamentDescription => 'Përshkrim i shkurtër i turneut'; + + @override + String get broadcastFullDescription => 'Përshkrim i plotë i turneut'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Përshkrim i gjatë opsional i turneut. $param1 është e disponueshme. Gjatësia duhet të jetë më pak se $param2 shenja.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL Burimi PGN-je'; + + @override + String get broadcastSourceUrlHelp => 'URL-ja që do të kontrollojë Lichess-i për të marrë përditësime PGN-sh. Duhet të jetë e përdorshme publikisht që nga Interneti.'; + + @override + String get broadcastSourceGameIds => 'Deri në 64 ID lojërash Lichess, ndarë me hapësira.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Datë fillimi në zonën kohore vendore të turneut: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opsionale, nëse e dini kur fillon veprimtaria'; + + @override + String get broadcastCurrentGameUrl => 'URL e lojës së tanishme'; + + @override + String get broadcastDownloadAllRounds => 'Shkarko krejt raundet'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Fshije këtë raund'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Fshije përfundimisht raundin dhe lojërat e tij.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Fshi krejt lojërat e këtij raundi. Burimi do të duhet të jetë aktiv, që të mund të rikrijohen ato.'; + + @override + String get broadcastEditRoundStudy => 'Përpunoni analizë raundi'; + + @override + String get broadcastDeleteTournament => 'Fshije këtë turne'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Fshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të.'; + + @override + String get broadcastShowScores => 'Shfaq pikë lojtatësh bazuar në përfundime lojërash'; + + @override + String get broadcastReplacePlayerTags => 'Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj'; + + @override + String get broadcastFideFederations => 'Federata FIDE'; + + @override + String get broadcastTop10Rating => '10 vlerësimet kryesuese'; + + @override + String get broadcastFidePlayers => 'Lojtarë FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'S’u gjet lojtar FIDE'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federim'; + + @override + String get broadcastAgeThisYear => 'Moshë këtë vit'; + + @override + String get broadcastUnrated => 'Pa pikë'; + + @override + String get broadcastRecentTournaments => 'Turne së fundi'; + + @override + String get broadcastOpenLichess => 'Hape në Lichess'; + + @override + String get broadcastTeams => 'Ekipe'; + + @override + String get broadcastBoards => 'Fusha'; + + @override + String get broadcastOverview => 'Përmbledhje'; + + @override + String get broadcastSubscribeTitle => 'Pajtohuni, që të noftoheni se kur fillon çdo raund. Mund të aktivizoni/çaktivizoni zilen, ose njoftimet “push” për transmetime, që nga parapëlqimet për llogarinë tuaj.'; + + @override + String get broadcastUploadImage => 'Ngarkoni figurë turneu'; + + @override + String get broadcastNoBoardsYet => 'Ende pa fusha. Këto do të shfaqen sapo të ngrkohen lojërat.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Fushat mund të ngarkohen me një burim, ose përmes $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Fillon pas $param'; + } + + @override + String get broadcastStartVerySoon => 'Transmetimi do të fillojë shumë shpejt.'; + + @override + String get broadcastNotYetStarted => 'Transmetimi s’ka filluar ende.'; + + @override + String get broadcastOfficialWebsite => 'Sajti zyrtar'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'Më tepër mundësi te $param'; + } + + @override + String get broadcastWebmastersPage => 'faqe webmaster-ësh'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Një burim publik,, PGN, i atypëratyshëm për këtë raund. Ofrojmë gjithashtu edhe një $param, për njëkohësim më të shpejtë dhe më efikas.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Trupëzojeni këtë transmetim në sajtin tuaj'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Trupëzojeni $param në sajtin tuaj'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Lojëra në këtë turne'; + + @override + String get broadcastScore => 'Përfundim'; + + @override + String get broadcastAllTeams => 'Krejt ekipet'; + + @override + String get broadcastTournamentFormat => 'Format turneu'; + + @override + String get broadcastTournamentLocation => 'Vendndodhje Turney'; + + @override + String get broadcastTopPlayers => 'Lojtarët kryesues'; + + @override + String get broadcastTimezone => 'Zonë kohore'; + + @override + String get broadcastFideRatingCategory => 'Kategori vlerësimi FIDE'; + + @override + String get broadcastOptionalDetails => 'Hollësi opsionale'; + + @override + String get broadcastPastBroadcasts => 'Transmetime të kaluara'; + + @override + String get broadcastAllBroadcastsByMonth => 'Shihni krejt transmetimet sipas muajsh'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmetime', + one: '$count transmetim', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { - return 'Challenges: $param1'; + return 'Sfida: $param1'; } @override - String get challengeChallengeToPlay => 'Sfidoni në një lojë'; + String get challengeChallengeToPlay => 'Sfidoni me një lojë'; @override String get challengeChallengeDeclined => 'Sfida u refuzua'; @@ -609,6 +864,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ora e shahut'; @@ -727,7 +985,7 @@ class AppLocalizationsSq extends AppLocalizations { String get preferencesNotifyGameEvent => 'Përditësime loje me korrespondencë'; @override - String get preferencesNotifyChallenge => 'Challenges'; + String get preferencesNotifyChallenge => 'Sfida'; @override String get preferencesNotifyTournamentSoon => 'Turne që fillon së shpejti'; @@ -750,6 +1008,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Tingull zileje njoftimesh'; + @override + String get preferencesBlindfold => 'Me sytë lidhur'; + @override String get puzzlePuzzles => 'Ushtrime'; @@ -1390,10 +1651,10 @@ class AppLocalizationsSq extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Kundërshtari është i kufizuar në lëvizjet që mund të bëjë dhe krejt lëvizjet përkeqësojnë pozicionin e tij.'; @override - String get puzzleThemeHealthyMix => 'Përzierje e ushtrimeve'; + String get puzzleThemeMix => 'Ushtrime të përzierë'; @override - String get puzzleThemeHealthyMixDescription => 'Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.'; + String get puzzleThemeMixDescription => 'Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.'; @override String get puzzleThemePlayerGames => 'Lojëra të lojëtarit'; @@ -1767,9 +2028,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get byCPL => 'nga CPL'; - @override - String get openStudy => 'Studim i hapur'; - @override String get enable => 'Aktivizoje'; @@ -1797,9 +2055,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get removesTheDepthLimit => 'Heq kufirin e thellësisë dhe e mban të ngrohtë kompjuterin tuaj'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Gafë'; @@ -2063,6 +2318,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get gamesPlayed => 'Lojëra të luajtura'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anuloje'; @@ -2437,9 +2695,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get unblock => 'Zhbllokoje'; - @override - String get followsYou => 'Ju ndjek juve'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 nisi të ndjekë $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsSq extends AppLocalizations { String get other => 'Tjetër'; @override - String get reportDescriptionHelp => 'Ngjitni lidhjen për te loja(ra) dhe shpjegoni çfarë nuk shkon me sjelljen e këtij përdoruesi. Mos shkruani thjesht “mashtrojnë”, por na tregoni si mbërritët në këtë përfundim. Raportimi juaj do të përpunohet më shpejt, nëse shkruhet në anglisht.'; + String get reportCheatBoostHelp => 'Ngjitni lidhjen për te loja(rat) dhe shpjegoni se ç’nuk shkon me sjelljen e këtij përdoruesi. Mos thoni thjesht “bën me hile”, por na tregoni se si arritët në këtë konkluzion.'; + + @override + String get reportUsernameHelp => 'Shpjegoni pse ky emër përdoruesi është fyes. Mos thoni thjesht “është fyes/i papërshtatshëm”, por na tregoni se si arritët në këtë konkluzion, veçanërisht nëse fyerja është e hollë, jo në anglisht, është në një slang, ose është një referencë historike/kulturore.'; + + @override + String get reportProcessedFasterInEnglish => 'Raportimi juaj do të shqyrtohet më shpejt, nëse është shkruar në anglisht.'; @override String get error_provideOneCheatedGameLink => 'Ju lutemi, jepni të paktën një lidhje te një lojë me hile.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get nothingToSeeHere => 'S’ka ç’shihet këtu tani.'; + @override + String get stats => 'Statistika'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4987,693 @@ class AppLocalizationsSq extends AppLocalizations { @override String get streamerLichessStreamers => 'Transmetues Lichess-i'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mësimet e mia'; + + @override + String get studyStudiesIContributeTo => 'Mësimet në të cilat kam kontribuar'; + + @override + String get studyMyPublicStudies => 'Mësimet e mia publike'; + + @override + String get studyMyPrivateStudies => 'Mësimet e mia private'; + + @override + String get studyMyFavoriteStudies => 'Mësimet e mia të parapëlqyera'; + + @override + String get studyWhatAreStudies => 'Ç’janë mësimet?'; + + @override + String get studyAllStudies => 'Krejt mësimet'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Mësime të krijuara nga $param'; + } + + @override + String get studyNoneYet => 'Ende asnjë.'; + + @override + String get studyHot => 'Më aktualet'; + + @override + String get studyDateAddedNewest => 'Data e krijimit (nga më e reja)'; + + @override + String get studyDateAddedOldest => 'Data e krijimit (nga më e vjetra)'; + + @override + String get studyRecentlyUpdated => 'E përditësuar së fundmi'; + + @override + String get studyMostPopular => 'Më populloret'; + + @override + String get studyAlphabetical => 'Alfabetik'; + + @override + String get studyAddNewChapter => 'Shto një kapitull të ri'; + + @override + String get studyAddMembers => 'Shto anëtarë'; + + @override + String get studyInviteToTheStudy => 'Ftoni në mësim'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ju lutemi, ftoni vetëm njerëzit që i njihni dhe që duan vërtet të marrin pjesë në këtë mësim.'; + + @override + String get studySearchByUsername => 'Kërkoni sipas emrit të përdoruesit'; + + @override + String get studySpectator => 'Shikues'; + + @override + String get studyContributor => 'Kontribues'; + + @override + String get studyKick => 'Përjashtoje'; + + @override + String get studyLeaveTheStudy => 'Braktisni mësimin'; + + @override + String get studyYouAreNowAContributor => 'Tani jeni një kontribues'; + + @override + String get studyYouAreNowASpectator => 'Tani jeni shikues'; + + @override + String get studyPgnTags => 'Etiketa PGN'; + + @override + String get studyLike => 'Pëlqejeni'; + + @override + String get studyUnlike => 'Shpëlqejeni'; + + @override + String get studyNewTag => 'Etiketë e re'; + + @override + String get studyCommentThisPosition => 'Komentoni këtë pozicion'; + + @override + String get studyCommentThisMove => 'Komentoni këtë lëvizje'; + + @override + String get studyAnnotateWithGlyphs => 'Shenjoni me karaktere'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Ky kapitull është shumë i shkurtë për t’u analizuar.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Analizë kompjuterike mund të kërkohet vetëm nga kontribuesit e këtij mësimi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Merrni nga shërbyesi një analizë të plotë kompjuterike të variantit kryesor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sigurohuni që kapitulli të jetë i plotë. Mund të kërkoni analizë vetëm një herë.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Krejt anëtarët SYNC mbesin në të njëjtin pozicion'; + + @override + String get studyShareChanges => 'Ndani ndryshimet me shikuesit dhe ruajini ato në shërbyes'; + + @override + String get studyPlaying => 'Po luhet'; + + @override + String get studyShowEvalBar => 'Shtylla vlerësimi'; + + @override + String get studyFirst => 'E para'; + + @override + String get studyPrevious => 'E mëparshmja'; + + @override + String get studyNext => 'Pasuesja'; + + @override + String get studyLast => 'E fundit'; + @override String get studyShareAndExport => 'Ndajeni me të tjerë & eksportoni'; + @override + String get studyCloneStudy => 'Klonoje'; + + @override + String get studyStudyPgn => 'Studioni PGN'; + + @override + String get studyDownloadAllGames => 'Shkarkoji krejt lojërat'; + + @override + String get studyChapterPgn => 'PGN e kapitullit'; + + @override + String get studyCopyChapterPgn => 'Kopjo PGN'; + + @override + String get studyDownloadGame => 'Shkarko lojën'; + + @override + String get studyStudyUrl => 'URL Mësimi'; + + @override + String get studyCurrentChapterUrl => 'URL e Kapitullit Aktual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Këtë mund ta ngjitni te forumi ose blogu juaj Lichess, për ta trupëzuar'; + + @override + String get studyStartAtInitialPosition => 'Fillo në pozicionin fillestar'; + + @override + String studyStartAtX(String param) { + return 'Fillo tek $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Trupëzojeni te sajti juaj'; + + @override + String get studyReadMoreAboutEmbedding => 'Lexoni më tepër rreth trupëzimit'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Mund të trupëzoni vetëm mësime publike!'; + + @override + String get studyOpen => 'Hap'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, sjellë për ju nga $param2'; + } + + @override + String get studyStudyNotFound => 'Mësimi s’u gjet'; + + @override + String get studyEditChapter => 'Përpunoni kapitullin'; + + @override + String get studyNewChapter => 'Kapitull i ri'; + + @override + String studyImportFromChapterX(String param) { + return 'Importo prej $param'; + } + + @override + String get studyOrientation => 'Drejtimi'; + + @override + String get studyAnalysisMode => 'Mënyra Analizim'; + + @override + String get studyPinnedChapterComment => 'Koment kapitulli i fiksuar'; + + @override + String get studySaveChapter => 'Ruaje kapitullin'; + + @override + String get studyClearAnnotations => 'Spastro shënimet'; + + @override + String get studyClearVariations => 'Spastroji variantet'; + + @override + String get studyDeleteChapter => 'Fshije kapitullin'; + + @override + String get studyDeleteThisChapter => 'Të fshihet ky kapitull? S’ka kthim mbrapa!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Të spastrohen krejt komentet, glifet dhe format e vizatuara në këtë kapitull?'; + + @override + String get studyRightUnderTheBoard => 'Mu nën fushë'; + + @override + String get studyNoPinnedComment => 'Asnjë'; + + @override + String get studyNormalAnalysis => 'Analizë normale'; + + @override + String get studyHideNextMoves => 'Fshih lëvizjen e radhës'; + + @override + String get studyInteractiveLesson => 'Mësim me ndërveprim'; + + @override + String studyChapterX(String param) { + return 'Kapitulli $param'; + } + + @override + String get studyEmpty => 'E zbrazët'; + + @override + String get studyStartFromInitialPosition => 'Fillo nga pozicioni fillestar'; + + @override + String get studyEditor => 'Përpunues'; + + @override + String get studyStartFromCustomPosition => 'Fillo nga pozicion vetjak'; + + @override + String get studyLoadAGameByUrl => 'Ngarko lojëra nga URL'; + + @override + String get studyLoadAPositionFromFen => 'Ngarko pozicionin nga FEN'; + + @override + String get studyLoadAGameFromPgn => 'Ngarko lojëra nga PGN'; + + @override + String get studyAutomatic => 'Automatik'; + + @override + String get studyUrlOfTheGame => 'URL e lojërave, një për rresht'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ngarko lojëra nga $param1 ose $param2'; + } + + @override + String get studyCreateChapter => 'Krijo kapitull'; + + @override + String get studyCreateStudy => 'Krijoni mësim'; + + @override + String get studyEditStudy => 'Përpunoni mësimin'; + + @override + String get studyVisibility => 'Dukshmëri'; + + @override + String get studyPublic => 'Publike'; + + @override + String get studyUnlisted => 'Jo në listë'; + + @override + String get studyInviteOnly => 'Vetëm me ftesa'; + + @override + String get studyAllowCloning => 'Lejo klonimin'; + + @override + String get studyNobody => 'Askush'; + + @override + String get studyOnlyMe => 'Vetëm unë'; + + @override + String get studyContributors => 'Kontribues'; + + @override + String get studyMembers => 'Anëtarë'; + + @override + String get studyEveryone => 'Cilido'; + + @override + String get studyEnableSync => 'Lejo njëkohësim'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Po: mbaje këdo në të njëjtin pozicion'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Jo: lejoji njerëzit të shfletojnë lirisht'; + + @override + String get studyPinnedStudyComment => 'Koment studimi i fiksuar'; + @override String get studyStart => 'Fillo'; + + @override + String get studySave => 'Ruaje'; + + @override + String get studyClearChat => 'Spastroje bisedën'; + + @override + String get studyDeleteTheStudyChatHistory => 'Të fshihet historiku i fjalosjeve të mësimit? S’ka kthim mbrapa!'; + + @override + String get studyDeleteStudy => 'Fshije mësimin'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Të fshihet krejt mësimi? S’ka kthim mbrapa! Për ta ripohuar, shtypni emrin e mësimit: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Ku doni ta studioni atë?'; + + @override + String get studyGoodMove => 'Lëvizje e mirë'; + + @override + String get studyMistake => 'Gabim'; + + @override + String get studyBrilliantMove => 'Lëvizje e shkëlqyer'; + + @override + String get studyBlunder => 'Gafë'; + + @override + String get studyInterestingMove => 'Lëvizje me interes'; + + @override + String get studyDubiousMove => 'Lëvizje e dyshimtë'; + + @override + String get studyOnlyMove => 'Lëvizja e vetme'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'është baras me'; + + @override + String get studyUnclearPosition => 'Shenjë gishti e paqartë'; + + @override + String get studyWhiteIsSlightlyBetter => 'I bardhi është pakëz më mirë'; + + @override + String get studyBlackIsSlightlyBetter => 'I ziu është pakëz më mirë'; + + @override + String get studyWhiteIsBetter => 'I bardhi është më mirë'; + + @override + String get studyBlackIsBetter => 'I ziu është më mirë'; + + @override + String get studyWhiteIsWinning => 'I bardhi po fiton'; + + @override + String get studyBlackIsWinning => 'I ziu po fiton'; + + @override + String get studyNovelty => 'Risi'; + + @override + String get studyDevelopment => 'Zhvillim'; + + @override + String get studyInitiative => 'Nismë'; + + @override + String get studyAttack => 'Sulm'; + + @override + String get studyCounterplay => 'Kundërsulm'; + + @override + String get studyTimeTrouble => 'Probleme me këtë instalim?'; + + @override + String get studyWithCompensation => 'Me kompesim'; + + @override + String get studyWithTheIdea => 'Me idenë'; + + @override + String get studyNextChapter => 'Kapitulli pasues'; + + @override + String get studyPrevChapter => 'Kapitulli i mëparshëm'; + + @override + String get studyStudyActions => 'Studioni veprimet'; + + @override + String get studyTopics => 'Tema'; + + @override + String get studyMyTopics => 'Temat e mia'; + + @override + String get studyPopularTopics => 'Tema popullore'; + + @override + String get studyManageTopics => 'Administroni tema'; + + @override + String get studyBack => 'Mbrapsht'; + + @override + String get studyPlayAgain => 'Riluaje'; + + @override + String get studyWhatWouldYouPlay => 'Ç’lëvizje do të bënit në këtë pozicion?'; + + @override + String get studyYouCompletedThisLesson => 'Përgëzime! E mbaruat këtë mësim.'; + + @override + String studyPerPage(String param) { + return '$param për faqe'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapituj', + one: '$count Kapitull', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Lojëra', + one: '$count Lojë', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Anëtarë', + one: '$count Anëtar', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hidhni këtu tekstin e PGN-s tuaj, deri në $count lojëra', + one: 'Hidhni këtu tekstin e PGN-s tuaj, deri në $count lojë', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'tani'; + + @override + String get timeagoRightNow => 'pikërisht tani'; + + @override + String get timeagoCompleted => 'mbaroi'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count sekondave', + one: 'në $count sekondë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count minutave', + one: 'në $count minutë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'në $count orë', + one: 'në $count orë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count ditëve', + one: 'në $count ditë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count javë', + one: 'në $count javë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count muajve', + one: 'në $count muaj', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count viteve', + one: 'në $count vit', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count minutave', + one: '$count minutë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count orëve', + one: '$count orë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count ditëve', + one: '$count ditë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count jave', + one: '$count javë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count muajve', + one: '$count muaj më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count viteve', + one: '$count vit më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Edhe $count minuta', + one: 'Edhe $count minutë', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Edhe $count orë', + one: 'Edhe $count orë', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 8fdff22f73..99baf28f50 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsSr extends AppLocalizations { AppLocalizationsSr([String locale = 'sr']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Clear'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Home'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsSr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowResult => 'Show result'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Show variations'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Активност'; @@ -254,6 +251,17 @@ class AppLocalizationsSr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -364,9 +372,256 @@ class AppLocalizationsSr extends AppLocalizations { @override String get broadcastBroadcasts => 'Емитовања'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Уживо емитовање турнира'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Нова ужива емитовања'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Текућа'; + + @override + String get broadcastUpcoming => 'Предстојећа'; + + @override + String get broadcastCompleted => 'Завршена'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Број рунде'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Цео опис догађаја'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -625,6 +880,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховски сат'; @@ -766,6 +1024,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Проблеми'; @@ -1405,10 +1666,10 @@ class AppLocalizationsSr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Противник има ограничен избор потеза и сваким потезом погоршава своју позицију.'; @override - String get puzzleThemeHealthyMix => 'Здрава мешавина'; + String get puzzleThemeMix => 'Здрава мешавина'; @override - String get puzzleThemeHealthyMixDescription => 'Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.'; + String get puzzleThemeMixDescription => 'Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.'; @override String get puzzleThemePlayerGames => 'Играчеве партије'; @@ -1782,9 +2043,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get byCPL => 'По рачунару'; - @override - String get openStudy => 'Отвори проуку'; - @override String get enable => 'Укључи'; @@ -1812,9 +2070,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get removesTheDepthLimit => 'Уклања ограничење дубине и греје рачунар'; - @override - String get engineManager => 'Менаџер машине'; - @override String get blunder => 'Груба грешка'; @@ -2078,6 +2333,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get gamesPlayed => 'Број одиграних партија'; + @override + String get ok => 'OK'; + @override String get cancel => 'Откажи'; @@ -2452,9 +2710,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get unblock => 'Одблокирај'; - @override - String get followsYou => 'Прате тебе'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 је почео/ла пратити $param2'; @@ -2787,7 +3042,13 @@ class AppLocalizationsSr extends AppLocalizations { String get other => 'Остало'; @override - String get reportDescriptionHelp => 'Залијепите везу до игре и објасните шта није у реду са понашањем корисника. Немојте само рећи \"варао\", али реците како сте дошли до тог закључка. Ваша пријава ће бити обрађена брже ако је напишете на енглеском језику.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Наведите барем једну везу игре у којој је играч варао.'; @@ -4092,6 +4353,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4777,9 +5041,698 @@ class AppLocalizationsSr extends AppLocalizations { @override String get streamerLichessStreamers => 'Личес стримери'; + @override + String get studyPrivate => 'Приватна'; + + @override + String get studyMyStudies => 'Моје студије'; + + @override + String get studyStudiesIContributeTo => 'Студије којима доприносим'; + + @override + String get studyMyPublicStudies => 'Моје јавне студије'; + + @override + String get studyMyPrivateStudies => 'Моје приватне студије'; + + @override + String get studyMyFavoriteStudies => 'Моје омиљене студије'; + + @override + String get studyWhatAreStudies => 'Шта су студије?'; + + @override + String get studyAllStudies => 'Све студије'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Студије које је $param направио/ла'; + } + + @override + String get studyNoneYet => 'Ниједна за сад.'; + + @override + String get studyHot => 'У тренду'; + + @override + String get studyDateAddedNewest => 'Датум додавања (најновије)'; + + @override + String get studyDateAddedOldest => 'Датум додавања (најстарије)'; + + @override + String get studyRecentlyUpdated => 'Недавно ажуриране'; + + @override + String get studyMostPopular => 'Најпопуларније'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Додајте ново поглавље'; + + @override + String get studyAddMembers => 'Додај чланове'; + + @override + String get studyInviteToTheStudy => 'Позовите у студију'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Молимо вас да само позивате људе које познајете и који активно желе да се придруже овој студији.'; + + @override + String get studySearchByUsername => 'Претражујте по корисничком имену'; + + @override + String get studySpectator => 'Посматрач'; + + @override + String get studyContributor => 'Cарадник'; + + @override + String get studyKick => 'Избаци'; + + @override + String get studyLeaveTheStudy => 'Напусти студију'; + + @override + String get studyYouAreNowAContributor => 'Сада сте сарадник'; + + @override + String get studyYouAreNowASpectator => 'Сада сте посматрач'; + + @override + String get studyPgnTags => 'PGN ознаке'; + + @override + String get studyLike => 'Свиђа ми се'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Нова ознака'; + + @override + String get studyCommentThisPosition => 'Прокоментаришите ову позицију'; + + @override + String get studyCommentThisMove => 'Прокоментаришите овај потез'; + + @override + String get studyAnnotateWithGlyphs => 'Прибележите глифовима'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Поглавље је прекратко за анализу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Само сарадници у студији могу захтевати рачунарску анализу.'; + + @override + String get studyGetAFullComputerAnalysis => 'Добијте потпуну рачунарску анализу главне варијације од стране сервера.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Побрините се да је поглавље завршено. Само једном можете захтевати анализу.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Сви SYNC чланови остају на истој позицији'; + + @override + String get studyShareChanges => 'Делите измене са посматрачима и сачувајте их на сервер'; + + @override + String get studyPlaying => 'У току'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Прва'; + + @override + String get studyPrevious => 'Претходна'; + + @override + String get studyNext => 'Следећа'; + + @override + String get studyLast => 'Последња'; + @override String get studyShareAndExport => 'Подели и извези'; + @override + String get studyCloneStudy => 'Клонирај'; + + @override + String get studyStudyPgn => 'PGN студије'; + + @override + String get studyDownloadAllGames => 'Преузми све партије'; + + @override + String get studyChapterPgn => 'PGN поглавља'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Преузми партију'; + + @override + String get studyStudyUrl => 'Линк студије'; + + @override + String get studyCurrentChapterUrl => 'Линк тренутног поглавља'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ово можете налепити у форум да уградите'; + + @override + String get studyStartAtInitialPosition => 'Започни на иницијалној позицији'; + + @override + String studyStartAtX(String param) { + return 'Започни на $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Угради у свој сајт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Прочитај више о уграђивању'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Само јавне студије могу бити уграђене!'; + + @override + String get studyOpen => 'Отворите'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 Вам доноси $param1'; + } + + @override + String get studyStudyNotFound => 'Студија није пронађена'; + + @override + String get studyEditChapter => 'Измени поглавље'; + + @override + String get studyNewChapter => 'Ново поглавље'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Оријентација'; + + @override + String get studyAnalysisMode => 'Врста анализе'; + + @override + String get studyPinnedChapterComment => 'Закачен коментар поглавља'; + + @override + String get studySaveChapter => 'Сачувај поглавље'; + + @override + String get studyClearAnnotations => 'Избриши анотације'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Избриши поглавље'; + + @override + String get studyDeleteThisChapter => 'Избриши ово поглавље? Нема повратка назад!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Избриши све коментаре, глифове и нацртане облике у овом поглављу?'; + + @override + String get studyRightUnderTheBoard => 'Одмах испод табле'; + + @override + String get studyNoPinnedComment => 'Ниједан'; + + @override + String get studyNormalAnalysis => 'Нормална анализа'; + + @override + String get studyHideNextMoves => 'Сакриј следеће потезе'; + + @override + String get studyInteractiveLesson => 'Интерактивна лекција'; + + @override + String studyChapterX(String param) { + return 'Поглавље $param'; + } + + @override + String get studyEmpty => 'Празно'; + + @override + String get studyStartFromInitialPosition => 'Започните од иницијалне позиције'; + + @override + String get studyEditor => 'Уређивач'; + + @override + String get studyStartFromCustomPosition => 'Започните од жељене позиције'; + + @override + String get studyLoadAGameByUrl => 'Учитајте партије преко линкова'; + + @override + String get studyLoadAPositionFromFen => 'Учитајте позицију из FEN-а'; + + @override + String get studyLoadAGameFromPgn => 'Учитајте партију из PGN-а'; + + @override + String get studyAutomatic => 'Аутоматски'; + + @override + String get studyUrlOfTheGame => 'Линкови партија, једна по реду'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Учитајте партије са $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Направи поглавље'; + + @override + String get studyCreateStudy => 'Направи студију'; + + @override + String get studyEditStudy => 'Измени студију'; + + @override + String get studyVisibility => 'Видљивост'; + + @override + String get studyPublic => 'Јавно'; + + @override + String get studyUnlisted => 'Неприказано'; + + @override + String get studyInviteOnly => 'Само по позиву'; + + @override + String get studyAllowCloning => 'Дозволите клонирање'; + + @override + String get studyNobody => 'Нико'; + + @override + String get studyOnlyMe => 'Само ја'; + + @override + String get studyContributors => 'Сарадници'; + + @override + String get studyMembers => 'Чланови'; + + @override + String get studyEveryone => 'Сви'; + + @override + String get studyEnableSync => 'Омогући синхронизацију'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: задржи све на истој позицији'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: дозволи људима да слободно прегледају'; + + @override + String get studyPinnedStudyComment => 'Закачен коментар студије'; + @override String get studyStart => 'Започни'; + + @override + String get studySave => 'Сачувај'; + + @override + String get studyClearChat => 'Очисти ћаскање'; + + @override + String get studyDeleteTheStudyChatHistory => 'Избриши историју ћаскања студије? Нема повратка назад!'; + + @override + String get studyDeleteStudy => 'Избриши студију'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Где желите то проучити?'; + + @override + String get studyGoodMove => 'Добар потез'; + + @override + String get studyMistake => 'Грешка'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Груба грешка'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Поглављa', + few: '$count Поглављa', + one: '$count Поглавље', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Партија', + few: '$count Партијe', + one: '$count Партија', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Чланова', + few: '$count Чланa', + one: '$count Члан', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Налепите свој PGN текст овде, до $count партија', + few: 'Налепите свој PGN текст овде, до $count партије', + one: 'Налепите свој PGN текст овде, до $count партије', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'управо сада'; + + @override + String get timeagoRightNow => 'управо сад'; + + @override + String get timeagoCompleted => 'завршено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count дана', + few: 'за $count сати', + one: 'за $count секунди', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index 592ce18f9a..7a7dd5cbfa 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsSv extends AppLocalizations { AppLocalizationsSv([String locale = 'sv']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Alla spel'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Är du säker?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileWatchTab => 'Watch'; + String get mobileClearButton => 'Rensa'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCustomGameJoinAGame => 'Gå med i spel'; @override - String get mobileSystemColors => 'System colors'; + String get mobileFeedbackButton => 'Feedback'; @override - String get mobileFeedbackButton => 'Feedback'; + String mobileGreeting(String param) { + return 'Hej $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Hej'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileHideVariation => 'Dölj variationer'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Hem'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileAllGames => 'All games'; + String get mobileNoSearchResults => 'Inga resultat'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNotFollowingAnyUser => 'Du följer inte någon användare.'; @override - String get mobileClearButton => 'Clear'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Spelare med \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; - - @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Problem'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Senaste sökningar'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileShareGamePGN => 'Dela PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Dela parti-URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Dela position som FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Dela detta schackproblem'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Visa kommentarer'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Visa resultat'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Visa variationer'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Något gick fel.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Systemets färger'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Verktyg'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Titta'; @override String get activityActivity => 'Aktivitet'; @@ -246,6 +243,17 @@ class AppLocalizationsSv extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsSv extends AppLocalizations { @override String get broadcastBroadcasts => 'Sändningar'; + @override + String get broadcastMyBroadcasts => 'Mina sändningar'; + @override String get broadcastLiveBroadcasts => 'Direktsända turneringar'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Ny direktsändning'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Om sändningar'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hur man använder Lichess-Sändningar.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nya rundan kommer att ha samma medlemmar och bidragsgivare som den föregående.'; + + @override + String get broadcastAddRound => 'Lägg till en omgång'; + + @override + String get broadcastOngoing => 'Pågående'; + + @override + String get broadcastUpcoming => 'Kommande'; + + @override + String get broadcastCompleted => 'Slutförda'; + + @override + String get broadcastCompletedHelp => 'Lichess upptäcker slutförandet av rundor baserat på källspelen. Använd detta alternativ om det inte finns någon källa.'; + + @override + String get broadcastRoundName => 'Omgångens namn'; + + @override + String get broadcastRoundNumber => 'Omgångens nummer'; + + @override + String get broadcastTournamentName => 'Turneringens namn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivning av turneringen'; + + @override + String get broadcastFullDescription => 'Fullständig beskrivning'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfri längre beskrivning av sändningen. $param1 är tillgänglig. Längden måste vara mindre än $param2 tecken.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL som Lichess kan använda för att få PGN-uppdateringar. Den måste vara publikt tillgänglig från Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfritt, om du vet när händelsen startar'; + + @override + String get broadcastCurrentGameUrl => 'Länk till aktuellt parti (URL)'; + + @override + String get broadcastDownloadAllRounds => 'Ladda ner alla omgångar'; + + @override + String get broadcastResetRound => 'Återställ den här omgången'; + + @override + String get broadcastDeleteRound => 'Ta bort den här omgången'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Ta bort denna runda och dess partier definitivt.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Radera alla partier i denna runda. Källan kommer behöva vara aktiv för att återskapa dem.'; + + @override + String get broadcastEditRoundStudy => 'Redigera studie för ronden'; + + @override + String get broadcastDeleteTournament => 'Radera turnering'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitivt radera turnering.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sändningar', + one: '$count sändning', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utmaningar: $param1'; @@ -609,6 +864,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get preferencesInGameOnly => 'Endast i parti'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schack-klocka'; @@ -750,6 +1008,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klock-notisljud'; + @override + String get preferencesBlindfold => 'I blindo'; + @override String get puzzlePuzzles => 'Problem'; @@ -1390,10 +1651,10 @@ class AppLocalizationsSv extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Motspelaren har begränsat antal möjliga drag, och alla möjliga drag förvärrar motspelarens position.'; @override - String get puzzleThemeHealthyMix => 'Blandad kompott'; + String get puzzleThemeMix => 'Blandad kompott'; @override - String get puzzleThemeHealthyMixDescription => 'Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.'; + String get puzzleThemeMixDescription => 'Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.'; @override String get puzzleThemePlayerGames => 'Spelarspel'; @@ -1767,9 +2028,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Öppna studie'; - @override String get enable => 'Aktivera'; @@ -1797,9 +2055,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get removesTheDepthLimit => 'Tar bort sökdjupsbegränsningen och håller datorn varm'; - @override - String get engineManager => 'Hantera analysmotor'; - @override String get blunder => 'Blunder'; @@ -2063,6 +2318,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get gamesPlayed => 'Partier spelade'; + @override + String get ok => 'OK'; + @override String get cancel => 'Avbryt'; @@ -2437,9 +2695,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get unblock => 'Avblockera'; - @override - String get followsYou => 'Följer dig'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 började följa $param2'; @@ -2772,7 +3027,13 @@ class AppLocalizationsSv extends AppLocalizations { String get other => 'Annat'; @override - String get reportDescriptionHelp => 'Klistra in länken till partiet och förklara vad som är fel med den här användarens beteende. Säg inte bara \"de fuskar\", utan förklara hur du dragit denna slutsats. Din rapport kommer att behandlas fortare om den är skriven på engelska.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Ange minst en länk till ett spel där användaren fuskade.'; @@ -4077,6 +4338,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4724,8 +4988,692 @@ class AppLocalizationsSv extends AppLocalizations { String get streamerLichessStreamers => 'Videokanaler från Lichess'; @override - String get studyShareAndExport => 'Dela & exportera'; + String get studyPrivate => 'Privat'; @override - String get studyStart => 'Starta'; + String get studyMyStudies => 'Mina studier'; + + @override + String get studyStudiesIContributeTo => 'Studier som jag bidrar till'; + + @override + String get studyMyPublicStudies => 'Mina offentliga studier'; + + @override + String get studyMyPrivateStudies => 'Mina privata studier'; + + @override + String get studyMyFavoriteStudies => 'Mina favoritstudier'; + + @override + String get studyWhatAreStudies => 'Vad är studier?'; + + @override + String get studyAllStudies => 'Alla studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier skapade av $param'; + } + + @override + String get studyNoneYet => 'Inga ännu.'; + + @override + String get studyHot => 'Populära'; + + @override + String get studyDateAddedNewest => 'Datum tillagd (nyaste)'; + + @override + String get studyDateAddedOldest => 'Datum tillagd (nyaste)'; + + @override + String get studyRecentlyUpdated => 'Nyligen uppdaterade'; + + @override + String get studyMostPopular => 'Mest populära'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Lägg till ett nytt kapitel'; + + @override + String get studyAddMembers => 'Lägg till medlemmar'; + + @override + String get studyInviteToTheStudy => 'Bjud in till studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Viktigt: bjud bara in människor du känner och som aktivt vill gå med i studien.'; + + @override + String get studySearchByUsername => 'Sök efter användarnamn'; + + @override + String get studySpectator => 'Åskådare'; + + @override + String get studyContributor => 'Bidragsgivare'; + + @override + String get studyKick => 'Sparka'; + + @override + String get studyLeaveTheStudy => 'Lämna studien'; + + @override + String get studyYouAreNowAContributor => 'Du är nu bidragsgivare'; + + @override + String get studyYouAreNowASpectator => 'Du är nu en åskådare'; + + @override + String get studyPgnTags => 'PGN taggar'; + + @override + String get studyLike => 'Gilla'; + + @override + String get studyUnlike => 'Sluta gilla'; + + @override + String get studyNewTag => 'Ny tag'; + + @override + String get studyCommentThisPosition => 'Kommentera denna position'; + + @override + String get studyCommentThisMove => 'Kommentera detta drag'; + + @override + String get studyAnnotateWithGlyphs => 'Kommentera med glyfer'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitlet är för kort för att analyseras.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Endast studiens bidragsgivare kan begära en datoranalys.'; + + @override + String get studyGetAFullComputerAnalysis => 'Hämta en fullständig serveranalys av huvudlinjen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Försäkra dig om att kapitlet är färdigt. Du kan bara begära analysen en gång.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alla SYNC-medlemmar är kvar på samma position'; + + @override + String get studyShareChanges => 'Dela ändringar med åskådare och spara dem på servern'; + + @override + String get studyPlaying => 'Spelar'; + + @override + String get studyShowEvalBar => 'Värderingsfält'; + + @override + String get studyFirst => 'Första'; + + @override + String get studyPrevious => 'Föregående'; + + @override + String get studyNext => 'Nästa'; + + @override + String get studyLast => 'Sista'; + + @override + String get studyShareAndExport => 'Dela & exportera'; + + @override + String get studyCloneStudy => 'Klona'; + + @override + String get studyStudyPgn => 'Studiens PGN'; + + @override + String get studyDownloadAllGames => 'Ladda ner alla partier'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'Kopiera PGN'; + + @override + String get studyDownloadGame => 'Ladda ner parti'; + + @override + String get studyStudyUrl => 'Studiens URL'; + + @override + String get studyCurrentChapterUrl => 'Aktuell kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan klistra in detta i forumet för att infoga'; + + @override + String get studyStartAtInitialPosition => 'Start vid ursprunglig position'; + + @override + String studyStartAtX(String param) { + return 'Börja på $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Infoga på din hemsida eller blogg'; + + @override + String get studyReadMoreAboutEmbedding => 'Läs mer om att infoga'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Endast offentliga studier kan läggas till!'; + + @override + String get studyOpen => 'Öppna'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 gjord av $param2'; + } + + @override + String get studyStudyNotFound => 'Studien kan inte hittas'; + + @override + String get studyEditChapter => 'Redigera kapitel'; + + @override + String get studyNewChapter => 'Nytt kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importera från $param'; + } + + @override + String get studyOrientation => 'Orientering'; + + @override + String get studyAnalysisMode => 'Analysläge'; + + @override + String get studyPinnedChapterComment => 'Fastnålad kommentar till kapitlet'; + + @override + String get studySaveChapter => 'Spara kapitlet'; + + @override + String get studyClearAnnotations => 'Rensa kommentarer'; + + @override + String get studyClearVariations => 'Rensa variationer'; + + @override + String get studyDeleteChapter => 'Ta bort kapitel'; + + @override + String get studyDeleteThisChapter => 'Ta bort detta kapitel. Det går inte att ångra!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Rensa alla kommentarer, symboler och former i detta kapitel?'; + + @override + String get studyRightUnderTheBoard => 'Direkt under brädet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analys'; + + @override + String get studyHideNextMoves => 'Dölj nästa drag'; + + @override + String get studyInteractiveLesson => 'Interaktiv lektion'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Starta från ursprunglig position'; + + @override + String get studyEditor => 'Redigeringsverktyg'; + + @override + String get studyStartFromCustomPosition => 'Starta från anpassad position'; + + @override + String get studyLoadAGameByUrl => 'Importera ett spel med URL'; + + @override + String get studyLoadAPositionFromFen => 'Importera en position med FEN-kod'; + + @override + String get studyLoadAGameFromPgn => 'Importera ett spel med PGN-kod'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL till partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Importera ett parti från $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Skapa kapitel'; + + @override + String get studyCreateStudy => 'Skapa en studie'; + + @override + String get studyEditStudy => 'Redigera studie'; + + @override + String get studyVisibility => 'Synlighet'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ej listad'; + + @override + String get studyInviteOnly => 'Endast inbjudna'; + + @override + String get studyAllowCloning => 'Tillåt kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Bara mig'; + + @override + String get studyContributors => 'Medhjälpare'; + + @override + String get studyMembers => 'Medlemmar'; + + @override + String get studyEveryone => 'Alla'; + + @override + String get studyEnableSync => 'Aktivera synkronisering'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: håll alla på samma position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nej: låt alla bläddra fritt'; + + @override + String get studyPinnedStudyComment => 'Ständigt synlig studiekommentar'; + + @override + String get studyStart => 'Starta'; + + @override + String get studySave => 'Spara'; + + @override + String get studyClearChat => 'Rensa Chatten'; + + @override + String get studyDeleteTheStudyChatHistory => 'Radera studiens chatthistorik? Detta går inte att ångra!'; + + @override + String get studyDeleteStudy => 'Ta bort studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Radera hela studien? Detta går inte att ångra! Skriv namnet på studien för att bekräfta: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Var vill du studera detta?'; + + @override + String get studyGoodMove => 'Bra drag'; + + @override + String get studyMistake => 'Misstag'; + + @override + String get studyBrilliantMove => 'Lysande drag'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Intressant drag'; + + @override + String get studyDubiousMove => 'Tvivelaktigt drag'; + + @override + String get studyOnlyMove => 'Enda draget'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Likvärdig position'; + + @override + String get studyUnclearPosition => 'Oklar position'; + + @override + String get studyWhiteIsSlightlyBetter => 'Vit är något bättre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart är något bättre'; + + @override + String get studyWhiteIsBetter => 'Vit är bättre'; + + @override + String get studyBlackIsBetter => 'Svart är bättre'; + + @override + String get studyWhiteIsWinning => 'Vit vinner'; + + @override + String get studyBlackIsWinning => 'Svart vinner'; + + @override + String get studyNovelty => 'Ny variant'; + + @override + String get studyDevelopment => 'Utveckling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Motspel'; + + @override + String get studyTimeTrouble => 'Tidsproblem'; + + @override + String get studyWithCompensation => 'Med kompensation'; + + @override + String get studyWithTheIdea => 'Med idén'; + + @override + String get studyNextChapter => 'Nästa kapitel'; + + @override + String get studyPrevChapter => 'Föregående kapitel'; + + @override + String get studyStudyActions => 'Studie-alternativ'; + + @override + String get studyTopics => 'Ämnen'; + + @override + String get studyMyTopics => 'Mina ämnen'; + + @override + String get studyPopularTopics => 'Populära ämnen'; + + @override + String get studyManageTopics => 'Hantera ämnen'; + + @override + String get studyBack => 'Tillbaka'; + + @override + String get studyPlayAgain => 'Spela igen'; + + @override + String get studyWhatWouldYouPlay => 'Vad skulle du spela i denna position?'; + + @override + String get studyYouCompletedThisLesson => 'Grattis! Du har slutfört denna lektionen.'; + + @override + String studyPerPage(String param) { + return '$param per page'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count partier', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Medlemmar', + one: '$count Medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Klistra in din PGN-kod här, upp till $count partier', + one: 'Klistra in din PGN-kod här, upp till $count parti', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'just nu'; + + @override + String get timeagoRightNow => 'just nu'; + + @override + String get timeagoCompleted => 'slutfört'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minuter', + one: 'om $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timmar', + one: 'om $count timme', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dagar', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count veckor', + one: 'om $count vecka', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count månader', + one: 'om $count månad', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuter sedan', + one: '$count minut sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timmar sedan', + one: '$count timme sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagar sedan', + one: '$count dag sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count veckor sedan', + one: '$count vecka sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count månader sedan', + one: '$count månad sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år sedan', + one: '$count år sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuter återstår', + one: '$count minut återstår', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timmar återstår', + one: '$count timme återstår', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 24e4ffa09d..b01b058a91 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsTr extends AppLocalizations { AppLocalizationsTr([String locale = 'tr']) : super(locale); @override - String get mobileHomeTab => 'Ana sayfa'; + String get mobileAllGames => 'Tüm oyunlar'; @override - String get mobilePuzzlesTab => 'Bulmacalar'; + String get mobileAreYouSure => 'Emin misiniz?'; @override - String get mobileToolsTab => 'Araçlar'; + String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; @override - String get mobileWatchTab => 'İzle'; + String get mobileClearButton => 'Temizle'; @override - String get mobileSettingsTab => 'Ayarlar'; + String get mobileCorrespondenceClearSavedMove => 'Kayıtlı hamleyi sil'; @override - String get mobileMustBeLoggedIn => 'Bu sayfayı görüntülemek için giriş yapmalısınız.'; + String get mobileCustomGameJoinAGame => 'Bir oyuna katıl'; @override - String get mobileSystemColors => 'Sistem renkleri'; + String get mobileFeedbackButton => 'Geri bildirimde bulun'; @override - String get mobileFeedbackButton => 'Geri bildirimde bulun'; + String mobileGreeting(String param) { + return 'Merhaba, $param'; + } @override - String get mobileOkButton => 'Tamam'; + String get mobileGreetingWithoutName => 'Merhaba'; @override - String get mobileSettingsHapticFeedback => 'Titreşimli geri bildirim'; + String get mobileHideVariation => 'Varyasyonu gizle'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHomeTab => 'Ana sayfa'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileLiveStreamers => 'Canlı yayıncılar'; @override - String get mobileNotFollowingAnyUser => 'Hiçbir kullanıcıyı takip etmiyorsunuz.'; + String get mobileMustBeLoggedIn => 'Bu sayfayı görüntülemek için giriş yapmalısınız.'; @override - String get mobileAllGames => 'Tüm oyunlar'; + String get mobileNoSearchResults => 'Sonuç bulunamadı'; @override - String get mobileRecentSearches => 'Son aramalar'; + String get mobileNotFollowingAnyUser => 'Hiçbir kullanıcıyı takip etmiyorsunuz.'; @override - String get mobileClearButton => 'Temizle'; + String get mobileOkButton => 'Tamam'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return '\"$param\" ile başlayan oyuncularla'; } @override - String get mobileNoSearchResults => 'Sonuç bulunamadı'; + String get mobilePrefMagnifyDraggedPiece => 'Sürüklenen parçayı büyüt'; @override - String get mobileAreYouSure => 'Emin misiniz?'; + String get mobilePuzzleStormConfirmEndRun => 'Bu oyunu bitirmek istiyor musun?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Gösterilecek bir şey yok, lütfen filtreleri değiştirin'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.'; @override - String get mobileSharePuzzle => 'Bulmacayı paylaş'; + String get mobilePuzzleStormSubtitle => '3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.'; @override - String get mobileShareGameURL => 'Oyun linkini paylaş'; + String get mobilePuzzleStreakAbortWarning => 'Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.'; @override - String get mobileShareGamePGN => 'PGN\'yi paylaş'; + String get mobilePuzzleThemesSubtitle => 'En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.'; @override - String get mobileSharePositionAsFEN => 'Konumu FEN olarak paylaş'; + String get mobilePuzzlesTab => 'Bulmacalar'; @override - String get mobileShowVariations => 'Varyasyonları göster'; + String get mobileRecentSearches => 'Son aramalar'; @override - String get mobileHideVariation => 'Varyasyonu gizle'; + String get mobileSettingsHapticFeedback => 'Titreşimli geri bildirim'; @override - String get mobileShowComments => 'Yorumları göster'; + String get mobileSettingsImmersiveMode => 'Sürükleyici mod'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Ayarlar'; @override - String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; + String get mobileShareGamePGN => 'PGN\'yi paylaş'; @override - String get mobileCancelDrawOffer => 'Berabere teklifini iptal et'; + String get mobileShareGameURL => 'Oyun linkini paylaş'; @override - String get mobileWaitingForOpponentToJoin => 'Rakip bekleniyor...'; + String get mobileSharePositionAsFEN => 'Konumu FEN olarak paylaş'; @override - String get mobileBlindfoldMode => 'Körleme modu'; + String get mobileSharePuzzle => 'Bulmacayı paylaş'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileShowComments => 'Yorumları göster'; @override - String get mobileCustomGameJoinAGame => 'Bir oyuna katıl'; + String get mobileShowResult => 'Sonucu göster'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowVariations => 'Varyasyonları göster'; @override String get mobileSomethingWentWrong => 'Birşeyler ters gitti.'; @override - String get mobileShowResult => 'Show result'; - - @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Sistem renkleri'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Araçlar'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Rakip bekleniyor...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'İzle'; @override String get activityActivity => 'Son Etkinlikler'; @@ -246,6 +243,17 @@ class AppLocalizationsTr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 yazışmalı oyunu tamamladı', + one: '$count $param2 yazışmalı oyunu tamamladı', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,256 @@ class AppLocalizationsTr extends AppLocalizations { @override String get broadcastBroadcasts => 'Canlı Turnuvalar'; + @override + String get broadcastMyBroadcasts => 'Canlı Turnuvalarım'; + @override String get broadcastLiveBroadcasts => 'Canlı Turnuva Yayınları'; + @override + String get broadcastBroadcastCalendar => 'Turnuva takvimi'; + + @override + String get broadcastNewBroadcast => 'Canlı Turnuva Ekle'; + + @override + String get broadcastSubscribedBroadcasts => 'Abone olduğunuz yayınlar'; + + @override + String get broadcastAboutBroadcasts => 'Canlı Turnuvalar hakkında'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Lichess Canlı Turnuvaları nasıl kullanılır.'; + + @override + String get broadcastTheNewRoundHelp => 'Yeni tur, önceki turdaki üyeler ve katkıda bulunanlarla aynı olacak.'; + + @override + String get broadcastAddRound => 'Bir tur ekle'; + + @override + String get broadcastOngoing => 'Devam eden turnuvalar'; + + @override + String get broadcastUpcoming => 'Yaklaşan turnuvalar'; + + @override + String get broadcastCompleted => 'Tamamlanan turnuvalar'; + + @override + String get broadcastCompletedHelp => 'Lichess, tur tamamlanmasını kaynak oyunlara dayanarak algılar. Kaynak yoksa bu anahtarı kullanın.'; + + @override + String get broadcastRoundName => 'Tur ismi'; + + @override + String get broadcastRoundNumber => 'Tur sayısı'; + + @override + String get broadcastTournamentName => 'Turnuva ismi'; + + @override + String get broadcastTournamentDescription => 'Turnuvanın kısa tanımı'; + + @override + String get broadcastFullDescription => 'Etkinliğin detaylıca açıklaması'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Etkinliğin isteğe bağlı detaylı açıklaması. $param1 seçeneği mevcuttur. Metnin uzunluğu azami $param2 karakter olmalıdır.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Kaynak URL\'si'; + + @override + String get broadcastSourceUrlHelp => 'Lichess, sağladığınız URL yardımıyla PGN\'yi güncelleyecektir. İnternet üzerinden herkese açık bir URL yazmalısınız.'; + + @override + String get broadcastSourceGameIds => 'Boşluklarla ayrılmış 64 adede kadar Lichess oyun ID\'si.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Turnuva yerel saati ile başlama zamanı: $param'; + } + + @override + String get broadcastStartDateHelp => 'İsteğe bağlı, etkinliğin ne zaman başladığını biliyorsanız ekleyebilirsiniz.'; + + @override + String get broadcastCurrentGameUrl => 'Şu anki oyunun linki'; + + @override + String get broadcastDownloadAllRounds => 'Bütün maçları indir'; + + @override + String get broadcastResetRound => 'Bu turu sıfırla'; + + @override + String get broadcastDeleteRound => 'Bu turu sil'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Turu ve oyunlarını tamamen sil.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Bu turdaki tüm oyunları sil. Tekrardan yapılabilmesi için kaynağın aktif olması gerekir.'; + + @override + String get broadcastEditRoundStudy => 'Tur çalışmasını düzenle'; + + @override + String get broadcastDeleteTournament => 'Bu turnuvayı sil'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Bütün turnuvayı, turlarını ve oyunlarını kalıcı olarak sil.'; + + @override + String get broadcastShowScores => 'Oyuncuların puanlarını oyun sonuçlarına göre göster'; + + @override + String get broadcastReplacePlayerTags => 'İsteğe bağlı: Oyuncu adlarını, derecelendirmelerini ve unvanlarını değiştirin'; + + @override + String get broadcastFideFederations => 'FIDE federasyonları'; + + @override + String get broadcastTop10Rating => 'İlk 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE oyuncuları'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE oyuncusu bulunamadı'; + + @override + String get broadcastFideProfile => 'FIDE profili'; + + @override + String get broadcastFederation => 'Federasyon'; + + @override + String get broadcastAgeThisYear => 'Bu yılki yaşı'; + + @override + String get broadcastUnrated => 'Derecelendirilmemiş'; + + @override + String get broadcastRecentTournaments => 'Son Turnuvalar'; + + @override + String get broadcastOpenLichess => 'Lichess\'te aç'; + + @override + String get broadcastTeams => 'Takımlar'; + + @override + String get broadcastBoards => 'Tahtalar'; + + @override + String get broadcastOverview => 'Genel Bakış'; + + @override + String get broadcastSubscribeTitle => 'Tur başladığında bildirim almak için abone olun. Hesap tercihlerinizden anlık ya da çan bildirimi tercihinizi hesap tercihlerinizden belirleyebilirsiniz.'; + + @override + String get broadcastUploadImage => 'Turnuva görseli yükleyin'; + + @override + String get broadcastNoBoardsYet => 'Henüz tahta bulunmamaktadır. Oyunlar yüklendikçe tahtalar ortaya çıkacaktır.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Tahtalar bir kaynaktan ya da ${param}ndan yüklenebilir'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param\'ten sonra başlar'; + } + + @override + String get broadcastStartVerySoon => 'Yayın az sonra başlayacak.'; + + @override + String get broadcastNotYetStarted => 'Yayın henüz başlamadı.'; + + @override + String get broadcastOfficialWebsite => 'Resmî site'; + + @override + String get broadcastStandings => 'Sıralamalar'; + + @override + String get broadcastOfficialStandings => 'Resmi Sıralamalar'; + + @override + String broadcastIframeHelp(String param) { + return '${param}nda daha fazla seçenek'; + } + + @override + String get broadcastWebmastersPage => 'ağ yöneticileri sayfası'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Bu turun açık, gerçek zamanlı PGN kaynağı. Daha hızlı ve verimli senkronizasyon için $param\'ımız da bulunmaktadır.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'İnternet sitenizde bu yayını gömülü paylaşın'; + + @override + String broadcastEmbedThisRound(String param) { + return '${param}u İnternet sitenizde gömülü paylaşın'; + } + + @override + String get broadcastRatingDiff => 'Puan farkı'; + + @override + String get broadcastGamesThisTournament => 'Bu turnuvadaki maçlar'; + + @override + String get broadcastScore => 'Skor'; + + @override + String get broadcastAllTeams => 'Tüm takımlar'; + + @override + String get broadcastTournamentFormat => 'Turnuva biçimi'; + + @override + String get broadcastTournamentLocation => 'Turnuva Konumu'; + + @override + String get broadcastTopPlayers => 'En iyi oyuncular'; + + @override + String get broadcastTimezone => 'Zaman dilimi'; + + @override + String get broadcastFideRatingCategory => 'FIDE derecelendirme kategorisi'; + + @override + String get broadcastOptionalDetails => 'İsteğe bağlı ayrıntılar'; + + @override + String get broadcastPastBroadcasts => 'Geçmiş yayınlar'; + + @override + String get broadcastAllBroadcastsByMonth => 'Tüm yayınları aylara göre görüntüleyin'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count canlı turnuva', + one: '$count canlı turnuva', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '$param1 karşılaşmaları'; @@ -609,6 +864,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get preferencesInGameOnly => 'Sadece oyun sırasında'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Satranç saati'; @@ -750,6 +1008,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Çan bildirimi sesi'; + @override + String get preferencesBlindfold => 'Körleme modu'; + @override String get puzzlePuzzles => 'Bulmacalar'; @@ -1390,10 +1651,10 @@ class AppLocalizationsTr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Rakibin iyi bir hamlesinin kalmadığı, yapabileceği bütün hamlelerin kendisine zarar vereceği taktikler.'; @override - String get puzzleThemeHealthyMix => 'Bir ondan bir bundan'; + String get puzzleThemeMix => 'Bir ondan bir bundan'; @override - String get puzzleThemeHealthyMixDescription => 'Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.'; + String get puzzleThemeMixDescription => 'Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.'; @override String get puzzleThemePlayerGames => 'Bireysel oyunlar'; @@ -1639,7 +1900,7 @@ class AppLocalizationsTr extends AppLocalizations { String get collapseVariations => 'Varyasyonları daralt'; @override - String get expandVariations => 'Varyasyonları genişler'; + String get expandVariations => 'Varyasyonları genişlet'; @override String get forceVariation => 'Varyant olarak göster'; @@ -1767,9 +2028,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get byCPL => 'CPL ile'; - @override - String get openStudy => 'Çalışma oluştur'; - @override String get enable => 'Etkinleştir'; @@ -1797,9 +2055,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get removesTheDepthLimit => 'Derinlik sınırını kaldırır ve bilgisayarınızı sıcacık tutar'; - @override - String get engineManager => 'Motor yöneticisi'; - @override String get blunder => 'Vahim hata'; @@ -1878,7 +2133,7 @@ class AppLocalizationsTr extends AppLocalizations { String get friends => 'Arkadaşlar'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'diğer oyuncular'; @override String get discussions => 'Sohbetler'; @@ -2063,6 +2318,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get gamesPlayed => 'Oynanmış oyunlar'; + @override + String get ok => 'Tamam'; + @override String get cancel => 'İptal et'; @@ -2437,9 +2695,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get unblock => 'Engeli kaldır'; - @override - String get followsYou => 'Sizi takip ediyor'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1, $param2 isimli oyuncuyu takip etmeye başladı'; @@ -2712,10 +2967,10 @@ class AppLocalizationsTr extends AppLocalizations { String get yes => 'Evet'; @override - String get website => 'Website'; + String get website => 'Web sitesi'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Yardım:'; @@ -2772,7 +3027,13 @@ class AppLocalizationsTr extends AppLocalizations { String get other => 'Diğer'; @override - String get reportDescriptionHelp => 'Raporlamak istediğiniz oyunun linkini yapıştırın ve sorununuzu açıklayın. Lütfen sadece \"hile yapıyor\" gibisinden açıklama yazmayın, hile olduğunu nasıl anladığınızı açıklayın. Rapor edeceğiniz kişiyi ya da oyunu \"İngilizce\" açıklarsanız, daha hızlı sonuca ulaşırsınız.'; + String get reportCheatBoostHelp => 'Oyunun (oyunların) bağlantısını yapıştırın ve kullanıcının hangi davranışı yanlış açıklayın. Sadece \"bu hile\" demeyin, bize bu sonuca nasıl vardığınızı söyleyin.'; + + @override + String get reportUsernameHelp => 'Bu kullanıcı adı hakkında neyin saldırganca olduğunu açıklayın. Sadece \"bu saldırganca/uygunsuz\" demeyin, bu sonuca nasıl vardığınızı söyleyin bize, özellikle de hakaret gizlenmiş, ingilizce olmayan, argo, veya tarihsel/kültürel referansalar varsa.'; + + @override + String get reportProcessedFasterInEnglish => 'Eğer İngilizce yazarsanız raporunuz daha hızlı işleme alınır.'; @override String get error_provideOneCheatedGameLink => 'Lütfen hileli gördüğünüz en az 1 adet oyun linki verin.'; @@ -2875,7 +3136,7 @@ class AppLocalizationsTr extends AppLocalizations { String get outsideTheBoard => 'Karelerin dışında'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Tahtadaki tüm karelerde'; @override String get onSlowGames => 'Yavaş oyunlarda'; @@ -4077,6 +4338,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get nothingToSeeHere => 'Şu anda görülebilecek bir şey yok.'; + @override + String get stats => 'İstatistikler'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4724,8 +4988,692 @@ class AppLocalizationsTr extends AppLocalizations { String get streamerLichessStreamers => 'Lichess yayıncıları'; @override - String get studyShareAndExport => 'Paylaş ve dışa aktar'; + String get studyPrivate => 'Gizli'; @override - String get studyStart => 'Başlat'; + String get studyMyStudies => 'Çalışmalarım'; + + @override + String get studyStudiesIContributeTo => 'Katkıda bulunduğum çalışmalar'; + + @override + String get studyMyPublicStudies => 'Herkese açık çalışmalarım'; + + @override + String get studyMyPrivateStudies => 'Gizli çalışmalarım'; + + @override + String get studyMyFavoriteStudies => 'En sevdiğim çalışmalar'; + + @override + String get studyWhatAreStudies => 'Çalışmalar nedir?'; + + @override + String get studyAllStudies => 'Bütün çalışmalar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Çalışmalar $param tarafından oluşturulmuştur'; + } + + @override + String get studyNoneYet => 'Henüz yok.'; + + @override + String get studyHot => 'Popüler'; + + @override + String get studyDateAddedNewest => 'Eklenme tarihi (en yeni)'; + + @override + String get studyDateAddedOldest => 'Eklenme tarihi (en eski)'; + + @override + String get studyRecentlyUpdated => 'Yeni güncellenmiş'; + + @override + String get studyMostPopular => 'En popüler'; + + @override + String get studyAlphabetical => 'Alfabetik'; + + @override + String get studyAddNewChapter => 'Yeni bir bölüm ekle'; + + @override + String get studyAddMembers => 'Üye ekle'; + + @override + String get studyInviteToTheStudy => 'Bu çalışmaya davet edin'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Lütfen sadece tanıdığınız ve katkı sağlayacağını düşündüğünüz kişileri davet ediniz.'; + + @override + String get studySearchByUsername => 'Kullanıcı adına göre ara'; + + @override + String get studySpectator => 'İzleyici'; + + @override + String get studyContributor => 'Katılımcı'; + + @override + String get studyKick => 'Çıkar'; + + @override + String get studyLeaveTheStudy => 'Çalışmadan ayrıl'; + + @override + String get studyYouAreNowAContributor => 'Artık bir katılımcısınız'; + + @override + String get studyYouAreNowASpectator => 'Artık bir izleyicisiniz'; + + @override + String get studyPgnTags => 'PGN etiketleri'; + + @override + String get studyLike => 'Beğen'; + + @override + String get studyUnlike => 'Beğenmekten Vazgeç'; + + @override + String get studyNewTag => 'Yeni etiket'; + + @override + String get studyCommentThisPosition => 'Bu pozisyon için yorum yap'; + + @override + String get studyCommentThisMove => 'Bu hamle için yorum yap'; + + @override + String get studyAnnotateWithGlyphs => 'Glifler ile açıkla'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Bu bölüm analiz için çok kısa.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Sadece katılımcılar bilgisayar analizi isteyebilir.'; + + @override + String get studyGetAFullComputerAnalysis => 'Bu varyant için ayrıntılı bir sunucu analizi yapın.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bu bölümü tamamladığınızdan emin olun. Sadece bir kez bilgisayar analizi talep edebilirsiniz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Senkronize edilen bütün üyeler aynı pozisyonda kalır'; + + @override + String get studyShareChanges => 'Değişiklikleri izleyiciler ile paylaşın ve sunucuya kaydedin'; + + @override + String get studyPlaying => 'Oynanıyor'; + + @override + String get studyShowEvalBar => 'Değerlendirme çubuğu'; + + @override + String get studyFirst => 'İlk'; + + @override + String get studyPrevious => 'Önceki'; + + @override + String get studyNext => 'Sonraki'; + + @override + String get studyLast => 'Son'; + + @override + String get studyShareAndExport => 'Paylaş ve dışa aktar'; + + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Çalışma PGN\'si'; + + @override + String get studyDownloadAllGames => 'Bütün oyunları indir'; + + @override + String get studyChapterPgn => 'Bölüm PGN\'si'; + + @override + String get studyCopyChapterPgn => 'PGN \'yi kopyala'; + + @override + String get studyDownloadGame => 'Oyunu indir'; + + @override + String get studyStudyUrl => 'Çalışma Adresi'; + + @override + String get studyCurrentChapterUrl => 'Mevcut Bölümün Adresi'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Forumda gömülü olarak paylaşmak için yukarıdaki bağlantıyı kullanabilirsiniz'; + + @override + String get studyStartAtInitialPosition => 'İlk pozisyondan başlasın'; + + @override + String studyStartAtX(String param) { + return '$param pozisyonundan başlasın'; + } + + @override + String get studyEmbedInYourWebsite => 'İnternet sitenizde ya da blogunuzda gömülü olarak paylaşın'; + + @override + String get studyReadMoreAboutEmbedding => 'Gömülü paylaşma hakkında'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Yalnızca herkese açık çalışmalar gömülü paylaşılabilir!'; + + @override + String get studyOpen => 'Aç'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 sana $param1 getirdi'; + } + + @override + String get studyStudyNotFound => 'Böyle bir çalışma bulunamadı'; + + @override + String get studyEditChapter => 'Bölümü düzenle'; + + @override + String get studyNewChapter => 'Yeni bölüm'; + + @override + String studyImportFromChapterX(String param) { + return '$param çalışmasından içe aktar'; + } + + @override + String get studyOrientation => 'Tahta yönü'; + + @override + String get studyAnalysisMode => 'Analiz modu'; + + @override + String get studyPinnedChapterComment => 'Bölüm üzerine yorumlar'; + + @override + String get studySaveChapter => 'Bölümü kaydet'; + + @override + String get studyClearAnnotations => 'Açıklamaları sil'; + + @override + String get studyClearVariations => 'Varyasyonları sil'; + + @override + String get studyDeleteChapter => 'Bölümü sil'; + + @override + String get studyDeleteThisChapter => 'Bu bölüm silinsin mi? Bunun geri dönüşü yok!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Bu bölümdeki bütün yorumlar ve işaretler temizlensin mi?'; + + @override + String get studyRightUnderTheBoard => 'Tahtanın hemen altında görünsün'; + + @override + String get studyNoPinnedComment => 'Yok'; + + @override + String get studyNormalAnalysis => 'Normal analiz'; + + @override + String get studyHideNextMoves => 'Sonraki hamleleri gizle'; + + @override + String get studyInteractiveLesson => 'Etkileşimli ders'; + + @override + String studyChapterX(String param) { + return 'Bölüm $param'; + } + + @override + String get studyEmpty => 'Boş'; + + @override + String get studyStartFromInitialPosition => 'İlk pozisyondan başlasın'; + + @override + String get studyEditor => 'Editör'; + + @override + String get studyStartFromCustomPosition => 'Özel bir pozisyondan başlasın'; + + @override + String get studyLoadAGameByUrl => 'URL ile oyun yükle'; + + @override + String get studyLoadAPositionFromFen => 'FEN kullanarak pozisyon yükle'; + + @override + String get studyLoadAGameFromPgn => 'PGN ile oyun yükle'; + + @override + String get studyAutomatic => 'Otomatik'; + + @override + String get studyUrlOfTheGame => 'Oyunun bağlantısı'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 veya $param2 kullanarak oyun yükle'; + } + + @override + String get studyCreateChapter => 'Bölüm oluştur'; + + @override + String get studyCreateStudy => 'Çalışma oluştur'; + + @override + String get studyEditStudy => 'Çalışmayı düzenle'; + + @override + String get studyVisibility => 'Görünürlük'; + + @override + String get studyPublic => 'Herkese açık'; + + @override + String get studyUnlisted => 'Liste dışı'; + + @override + String get studyInviteOnly => 'Sadece davet edilenler'; + + @override + String get studyAllowCloning => 'Klonlamaya izni olanlar'; + + @override + String get studyNobody => 'Hiç kimse'; + + @override + String get studyOnlyMe => 'Sadece ben'; + + @override + String get studyContributors => 'Katkıda Bulunanlar'; + + @override + String get studyMembers => 'Üyeler'; + + @override + String get studyEveryone => 'Herkes'; + + @override + String get studyEnableSync => 'Senkronizasyonu etkinleştir'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Evet: herkes aynı pozisyonda kalsın'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Hayır: herkes dilediğince gezinebilsin'; + + @override + String get studyPinnedStudyComment => 'Çalışma üzerine yorumlar'; + + @override + String get studyStart => 'Başlat'; + + @override + String get studySave => 'Kaydet'; + + @override + String get studyClearChat => 'Sohbeti temizle'; + + @override + String get studyDeleteTheStudyChatHistory => 'Çalışmanın sohbet geçmişi silinsin mi? Bunun geri dönüşü yok!'; + + @override + String get studyDeleteStudy => 'Çalışmayı sil'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Tüm çalışma silinsin mi? Bunun geri dönüşü yok! İşlemi onaylamak için çalışmanın adını yazın: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Bu çalışmayı nerede sürdürmek istersiniz?'; + + @override + String get studyGoodMove => 'İyi hamle'; + + @override + String get studyMistake => 'Hata'; + + @override + String get studyBrilliantMove => 'Muhteşem hamle'; + + @override + String get studyBlunder => 'Vahim hata'; + + @override + String get studyInterestingMove => 'İlginç hamle'; + + @override + String get studyDubiousMove => 'Şüpheli hamle'; + + @override + String get studyOnlyMove => 'Tek hamle'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Eşit pozisyon'; + + @override + String get studyUnclearPosition => 'Belirsiz pozisyon'; + + @override + String get studyWhiteIsSlightlyBetter => 'Beyaz biraz önde'; + + @override + String get studyBlackIsSlightlyBetter => 'Siyah biraz önde'; + + @override + String get studyWhiteIsBetter => 'Beyaz daha üstün'; + + @override + String get studyBlackIsBetter => 'Siyah daha üstün'; + + @override + String get studyWhiteIsWinning => 'Beyaz kazanıyor'; + + @override + String get studyBlackIsWinning => 'Siyah kazanıyor'; + + @override + String get studyNovelty => 'Farklı bir açılış'; + + @override + String get studyDevelopment => 'Gelişim hamlesi'; + + @override + String get studyInitiative => 'Girişim'; + + @override + String get studyAttack => 'Saldırı'; + + @override + String get studyCounterplay => 'Karşı saldırı'; + + @override + String get studyTimeTrouble => 'Vakit sıkıntısı'; + + @override + String get studyWithCompensation => 'Pozisyon avantajı ile'; + + @override + String get studyWithTheIdea => 'Plan doğrultusunda'; + + @override + String get studyNextChapter => 'Sonraki bölüm'; + + @override + String get studyPrevChapter => 'Önceki bölüm'; + + @override + String get studyStudyActions => 'Çalışma seçenekleri'; + + @override + String get studyTopics => 'Konular'; + + @override + String get studyMyTopics => 'Konularım'; + + @override + String get studyPopularTopics => 'Popüler konular'; + + @override + String get studyManageTopics => 'Konuları yönet'; + + @override + String get studyBack => 'Baştan başlat'; + + @override + String get studyPlayAgain => 'Tekrar oyna'; + + @override + String get studyWhatWouldYouPlay => 'Burada hangi hamleyi yapardınız?'; + + @override + String get studyYouCompletedThisLesson => 'Tebrikler! Bu dersi tamamlandınız.'; + + @override + String studyPerPage(String param) { + return 'Sayfa başına $param'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Bölüm', + one: '$count Bölüm', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Oyun', + one: '$count oyun', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Üye', + one: '$count Üye', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN metninizi buraya yapıştırın, en fazla $count oyuna kadar', + one: 'PGN metninizi buraya yapıştırın, en fazla $count oyuna kadar', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'şu anda'; + + @override + String get timeagoRightNow => 'hemen şimdi'; + + @override + String get timeagoCompleted => 'tamamlanmış'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saniyede', + one: '$count saniyede', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakikada', + one: '$count dakikada', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saatte', + one: '$count saatte', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count günde', + one: '$count günde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count haftada', + one: '$count haftada', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ayda', + one: '$count ayda', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count yılda', + one: '$count yılda', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakika önce', + one: '$count dakika önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saat önce', + one: '$count saat önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count gün önce', + one: '$count gün önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hafta önce', + one: '$count hafta önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ay önce', + one: '$count ay önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count yıl önce', + one: '$count yıl önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakika kaldı', + one: '$count dakika kaldı', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saat kaldı', + one: '$count saat kaldı', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index fb35d69ab9..fa9eb61c0a 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsUk extends AppLocalizations { AppLocalizationsUk([String locale = 'uk']) : super(locale); @override - String get mobileHomeTab => 'Головна'; + String get mobileAllGames => 'Усі ігри'; @override - String get mobilePuzzlesTab => 'Задачі'; + String get mobileAreYouSure => 'Ви впевнені?'; @override - String get mobileToolsTab => 'Інструм.'; + String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; @override - String get mobileWatchTab => 'Дивитися'; + String get mobileClearButton => 'Очистити'; @override - String get mobileSettingsTab => 'Налашт.'; + String get mobileCorrespondenceClearSavedMove => 'Очистити збережений хід'; @override - String get mobileMustBeLoggedIn => 'Ви повинні ввійти, аби переглянути цю сторінку.'; + String get mobileCustomGameJoinAGame => 'Приєднатися до гри'; @override - String get mobileSystemColors => 'Системні кольори'; + String get mobileFeedbackButton => 'Відгук'; @override - String get mobileFeedbackButton => 'Відгук'; + String mobileGreeting(String param) { + return 'Привіт, $param'; + } @override - String get mobileOkButton => 'Гаразд'; + String get mobileGreetingWithoutName => 'Привіт'; @override - String get mobileSettingsHapticFeedback => 'Вібрація при ході'; + String get mobileHideVariation => 'Сховати варіанти'; @override - String get mobileSettingsImmersiveMode => 'Повноекранний режим'; + String get mobileHomeTab => 'Головна'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.'; + String get mobileLiveStreamers => 'Стримери в прямому етері'; @override - String get mobileNotFollowingAnyUser => 'Ви ні на кого не підписані.'; + String get mobileMustBeLoggedIn => 'Ви повинні ввійти, аби переглянути цю сторінку.'; @override - String get mobileAllGames => 'Усі ігри'; + String get mobileNoSearchResults => 'Немає результатів '; @override - String get mobileRecentSearches => 'Недавні пошуки'; + String get mobileNotFollowingAnyUser => 'Ви ні на кого не підписані.'; @override - String get mobileClearButton => 'Очистити'; + String get mobileOkButton => 'Гаразд'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Немає результатів '; + String get mobilePrefMagnifyDraggedPiece => 'Збільшувати розмір фігури при перетягуванні'; @override - String get mobileAreYouSure => 'Ви впевнені?'; + String get mobilePuzzleStormConfirmEndRun => 'Ви хочете закінчити цю серію?'; @override - String get mobilePuzzleStreakAbortWarning => 'Ви втратите поточну серію, і ваш рахунок буде збережено.'; + String get mobilePuzzleStormFilterNothingToShow => 'Нічого не знайдено, будь ласка, змініть фільтри'; @override String get mobilePuzzleStormNothingToShow => 'Нічого показати. Зіграйте в гру Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Поділитися задачею'; + String get mobilePuzzleStormSubtitle => 'Розв\'яжіть якомога більше задач за 3 хвилини.'; @override - String get mobileShareGameURL => 'Поділитися посиланням на гру'; + String get mobilePuzzleStreakAbortWarning => 'Ви втратите поточну серію, і ваш рахунок буде збережено.'; @override - String get mobileShareGamePGN => 'Поділитися PGN'; + String get mobilePuzzleThemesSubtitle => 'Розв\'язуйте задачі з улюбленими дебютами або обирайте тему.'; @override - String get mobileSharePositionAsFEN => 'Поділитися FEN'; + String get mobilePuzzlesTab => 'Задачі'; @override - String get mobileShowVariations => 'Показати варіанти'; + String get mobileRecentSearches => 'Недавні пошуки'; @override - String get mobileHideVariation => 'Сховати варіанти'; + String get mobileSettingsHapticFeedback => 'Вібрація при ході'; @override - String get mobileShowComments => 'Показати коментарі'; + String get mobileSettingsImmersiveMode => 'Повноекранний режим'; @override - String get mobilePuzzleStormConfirmEndRun => 'Ви хочете закінчити цю серію?'; + String get mobileSettingsImmersiveModeSubtitle => 'Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Нічого не знайдено, будь ласка, змініть фільтри'; + String get mobileSettingsTab => 'Налашт.'; @override - String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; + String get mobileShareGamePGN => 'Поділитися PGN'; @override - String get mobileCancelDrawOffer => 'Скасувати пропозицію нічиєї'; + String get mobileShareGameURL => 'Поділитися посиланням на гру'; @override - String get mobileWaitingForOpponentToJoin => 'Очікування на суперника...'; + String get mobileSharePositionAsFEN => 'Поділитися FEN'; @override - String get mobileBlindfoldMode => 'Наосліп'; + String get mobileSharePuzzle => 'Поділитися задачею'; @override - String get mobileLiveStreamers => 'Стримери в прямому етері'; + String get mobileShowComments => 'Показати коментарі'; @override - String get mobileCustomGameJoinAGame => 'Приєднатися до гри'; + String get mobileShowResult => 'Показати результат'; @override - String get mobileCorrespondenceClearSavedMove => 'Очистити збережений хід'; + String get mobileShowVariations => 'Показати варіанти'; @override String get mobileSomethingWentWrong => 'Щось пішло не так.'; @override - String get mobileShowResult => 'Показати результат'; - - @override - String get mobilePuzzleThemesSubtitle => 'Розв\'язуйте задачі з улюбленими дебютами або обирайте тему.'; + String get mobileSystemColors => 'Системні кольори'; @override - String get mobilePuzzleStormSubtitle => 'Розв\'яжіть якомога більше задач за 3 хвилини.'; + String get mobileTheme => 'Тема'; @override - String mobileGreeting(String param) { - return 'Привіт, $param'; - } + String get mobileToolsTab => 'Інструм.'; @override - String get mobileGreetingWithoutName => 'Привіт'; + String get mobileWaitingForOpponentToJoin => 'Очікування на суперника...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Дивитися'; @override String get activityActivity => 'Активність'; @@ -262,6 +259,19 @@ class AppLocalizationsUk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Зіграно $count $param2 заочних ігор', + many: 'Зіграно $count $param2 заочних ігор', + few: 'Зіграно $count $param2 заочні гри', + one: 'Зіграно $count $param2 заочну гру', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,258 @@ class AppLocalizationsUk extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляції'; + @override + String get broadcastMyBroadcasts => 'Мої трансляції'; + @override String get broadcastLiveBroadcasts => 'Онлайн трансляції турнірів'; + @override + String get broadcastBroadcastCalendar => 'Календар трансляцій'; + + @override + String get broadcastNewBroadcast => 'Нова трансляція'; + + @override + String get broadcastSubscribedBroadcasts => 'Обрані трансляції'; + + @override + String get broadcastAboutBroadcasts => 'Про трансляцію'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Як користуватися Lichess трансляціями.'; + + @override + String get broadcastTheNewRoundHelp => 'У новому раунді будуть ті самі учасники та редактори, що й у попередньому.'; + + @override + String get broadcastAddRound => 'Додати раунд'; + + @override + String get broadcastOngoing => 'Поточні'; + + @override + String get broadcastUpcoming => 'Майбутні'; + + @override + String get broadcastCompleted => 'Завершені'; + + @override + String get broadcastCompletedHelp => 'Lichess виявляє завершення раунду на основі ігор. Використовуйте цей перемикач якщо немає джерела.'; + + @override + String get broadcastRoundName => 'Назва раунду'; + + @override + String get broadcastRoundNumber => 'Номер раунду'; + + @override + String get broadcastTournamentName => 'Назва турніру'; + + @override + String get broadcastTournamentDescription => 'Короткий опис турніру'; + + @override + String get broadcastFullDescription => 'Повний опис події'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Необов\'язковий довгий опис трансляції. Наявна розмітка $param1. Довжина має бути менша ніж $param2 символів.'; + } + + @override + String get broadcastSourceSingleUrl => 'Адреса джерела PGN'; + + @override + String get broadcastSourceUrlHelp => 'Посилання, яке Lichess перевірятиме, щоб отримати оновлення PGN. Воно має бути загальнодоступним в Інтернеті.'; + + @override + String get broadcastSourceGameIds => 'До 64 ігрових ID Lichess, відокремлені пробілами.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'За бажанням, якщо ви знаєте, коли починається подія'; + + @override + String get broadcastCurrentGameUrl => 'Посилання на поточну гру'; + + @override + String get broadcastDownloadAllRounds => 'Завантажити всі тури'; + + @override + String get broadcastResetRound => 'Скинути цей раунд'; + + @override + String get broadcastDeleteRound => 'Видалити цей раунд'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Видалити всі ігри цього раунду.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Видалити всі ігри цього раунду. Джерело має бути активним для того, щоб повторно відтворити його.'; + + @override + String get broadcastEditRoundStudy => 'Редагувати дослідження раунду'; + + @override + String get broadcastDeleteTournament => 'Видалити турнір'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Остаточно видалити весь турнір, всі його раунди та всі його ігри.'; + + @override + String get broadcastShowScores => 'Показувати результати гравців за результатами гри'; + + @override + String get broadcastReplacePlayerTags => 'За бажанням: замінити імена, рейтинги та титули гравців'; + + @override + String get broadcastFideFederations => 'Федерації FIDE'; + + @override + String get broadcastTop10Rating => 'Топ 10 рейтингу'; + + @override + String get broadcastFidePlayers => 'Гравці FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Гравця FIDE не знайдено'; + + @override + String get broadcastFideProfile => 'Профіль FIDE'; + + @override + String get broadcastFederation => 'Федерація'; + + @override + String get broadcastAgeThisYear => 'Вік цього року'; + + @override + String get broadcastUnrated => 'Без рейтингу'; + + @override + String get broadcastRecentTournaments => 'Нещодавні турніри'; + + @override + String get broadcastOpenLichess => 'Відкрити в Lichess'; + + @override + String get broadcastTeams => 'Команди'; + + @override + String get broadcastBoards => 'Дошки'; + + @override + String get broadcastOverview => 'Огляд'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Завантажити зображення турніру'; + + @override + String get broadcastNoBoardsYet => 'Ще немає дощок. Вони з\'являться, коли ігри будуть завантажені.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'Трансляція розпочнеться дуже скоро.'; + + @override + String get broadcastNotYetStarted => 'Трансляція ще не розпочалася.'; + + @override + String get broadcastOfficialWebsite => 'Офіційний вебсайт'; + + @override + String get broadcastStandings => 'Турнірна таблиця'; + + @override + String get broadcastOfficialStandings => 'Офіційна турнірна таблиця'; + + @override + String broadcastIframeHelp(String param) { + return 'Більше опцій на $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Вбудувати цю трансляцію на своєму сайті'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Вбудувати $param на своєму сайті'; + } + + @override + String get broadcastRatingDiff => 'Різниця у рейтингу'; + + @override + String get broadcastGamesThisTournament => 'Ігри в цьому турнірі'; + + @override + String get broadcastScore => 'Очки'; + + @override + String get broadcastAllTeams => 'Усі команди'; + + @override + String get broadcastTournamentFormat => 'Формат турніру'; + + @override + String get broadcastTournamentLocation => 'Місце турніру'; + + @override + String get broadcastTopPlayers => 'Найкращі гравці'; + + @override + String get broadcastTimezone => 'Часовий пояс'; + + @override + String get broadcastFideRatingCategory => 'Категорія рейтингу FIDE'; + + @override + String get broadcastOptionalDetails => 'Додаткові деталі'; + + @override + String get broadcastPastBroadcasts => 'Минулі трансляції'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count трансляцій', + many: '$count трансляцій', + few: '$count трансляції', + one: '$count трансляція', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Виклики: $param1'; @@ -643,6 +902,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get preferencesInGameOnly => 'Лише під час гри'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховий годинник'; @@ -710,7 +972,7 @@ class AppLocalizationsUk extends AppLocalizations { String get preferencesInCorrespondenceGames => 'У заочних партіях'; @override - String get preferencesCorrespondenceAndUnlimited => 'За листуванням та необмежені'; + String get preferencesCorrespondenceAndUnlimited => 'Заочні та необмежені'; @override String get preferencesConfirmResignationAndDrawOffers => 'Підтверджувати повернення ходу та пропозиції нічий'; @@ -784,6 +1046,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Звук сповіщення'; + @override + String get preferencesBlindfold => 'Наосліп'; + @override String get puzzlePuzzles => 'Задачі'; @@ -1434,10 +1699,10 @@ class AppLocalizationsUk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Суперник обмежений в своїх ходах, а кожен хід погіршує його позицію.'; @override - String get puzzleThemeHealthyMix => 'Здорова суміш'; + String get puzzleThemeMix => 'Усього потрохи'; @override - String get puzzleThemeHealthyMixDescription => 'Всього потроху. Ви не знаєте, чого очікувати, тому готуйтесь до всього! Як у справжніх партіях.'; + String get puzzleThemeMixDescription => 'Усього потрохи. Ви не знатимете, чого очікувати, тому готуйтеся до всього! Як у справжніх партіях.'; @override String get puzzleThemePlayerGames => 'Ігри гравця'; @@ -1811,9 +2076,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get byCPL => 'Цікаве'; - @override - String get openStudy => 'Почати дослідження'; - @override String get enable => 'Увімкнути'; @@ -1841,9 +2103,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get removesTheDepthLimit => 'Знімає обмеження на глибину аналізу - ваш комп’ютер стане теплішим'; - @override - String get engineManager => 'Менеджер рушія'; - @override String get blunder => 'Груба помилка'; @@ -2107,6 +2366,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get gamesPlayed => 'Ігор зіграно'; + @override + String get ok => 'Гаразд'; + @override String get cancel => 'Скасувати'; @@ -2481,9 +2743,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get unblock => 'Розблокувати'; - @override - String get followsYou => 'Спостерігає за вами'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 починає спостерігати за $param2'; @@ -2816,7 +3075,13 @@ class AppLocalizationsUk extends AppLocalizations { String get other => 'Інше'; @override - String get reportDescriptionHelp => 'Вставте посилання на гру (ігри) та поясніть, що не так із поведінкою цього користувача. Не пишіть просто \"він шахраює\", а розкажіть, як ви дійшли до такого висновку. Вашу скаргу розглянуть швидше, якщо ви напишете її англійською.'; + String get reportCheatBoostHelp => 'Вставте посилання на гру або ігри та поясніть, що не так з поведінкою цього користувача. Не кажіть «він нечесно грав», а поясніть, як ви прийшли до такого висновку.'; + + @override + String get reportUsernameHelp => 'Поясніть, що саме в цьому імені користувача є образливе. Не кажіть «воно образливе/неприйнятне», а поясніть, чому ви так вважаєте, особливо коли образа заплутана, не англійською, на сленгу, чи є посиланням на щось історичне/культурне.'; + + @override + String get reportProcessedFasterInEnglish => 'Ваша скарга оброблятиметься швидше, якщо буде написана англійською.'; @override String get error_provideOneCheatedGameLink => 'Будь ласка, додайте посилання на хоча б одну нечесну гру.'; @@ -4121,6 +4386,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get nothingToSeeHere => 'Поки тут нічого немає.'; + @override + String get stats => 'Статистика'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5123,731 @@ class AppLocalizationsUk extends AppLocalizations { @override String get streamerLichessStreamers => 'Стримери Lichess'; + @override + String get studyPrivate => 'Приватне'; + + @override + String get studyMyStudies => 'Мої дослідження'; + + @override + String get studyStudiesIContributeTo => 'Дослідження, яким я сприяю'; + + @override + String get studyMyPublicStudies => 'Мої публічні дослідження'; + + @override + String get studyMyPrivateStudies => 'Мої приватні дослідження'; + + @override + String get studyMyFavoriteStudies => 'Мої улюблені дослідження'; + + @override + String get studyWhatAreStudies => 'Що таке дослідження?'; + + @override + String get studyAllStudies => 'Усі дослідження'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Дослідження, створені $param'; + } + + @override + String get studyNoneYet => 'Ще немає.'; + + @override + String get studyHot => 'Активні'; + + @override + String get studyDateAddedNewest => 'Дата додавання (старіші)'; + + @override + String get studyDateAddedOldest => 'Дата додавання (старіші)'; + + @override + String get studyRecentlyUpdated => 'Нещодавно оновлені'; + + @override + String get studyMostPopular => 'Найпопулярніші'; + + @override + String get studyAlphabetical => 'За алфавітом'; + + @override + String get studyAddNewChapter => 'Додати новий розділ'; + + @override + String get studyAddMembers => 'Додати учасників'; + + @override + String get studyInviteToTheStudy => 'Запросити до дослідження'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Будь ласка запрошуйте лише людей, яких ви знаєте, і які хочуть активно долучитися до цього дослідження.'; + + @override + String get studySearchByUsername => 'Пошук за іменем користувача'; + + @override + String get studySpectator => 'Глядач'; + + @override + String get studyContributor => 'Співавтор'; + + @override + String get studyKick => 'Вигнати'; + + @override + String get studyLeaveTheStudy => 'Покинути дослідження'; + + @override + String get studyYouAreNowAContributor => 'Тепер ви співавтор'; + + @override + String get studyYouAreNowASpectator => 'Тепер ви глядач'; + + @override + String get studyPgnTags => 'Теги PGN'; + + @override + String get studyLike => 'Подобається'; + + @override + String get studyUnlike => 'Не подобається'; + + @override + String get studyNewTag => 'Новий тег'; + + @override + String get studyCommentThisPosition => 'Коментувати цю позицію'; + + @override + String get studyCommentThisMove => 'Коментувати цей хід'; + + @override + String get studyAnnotateWithGlyphs => 'Додати символьну анотацію'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Розділ занадто короткий для аналізу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Лише співавтори дослідження можуть дати запит на комп\'ютерний аналіз.'; + + @override + String get studyGetAFullComputerAnalysis => 'Отримати повний серверний комп\'ютерний аналіз головної лінії.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Переконайтесь, що розділ завершено. Ви можете дати запит на аналіз лише один раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Усі синхронізовані учасники залишаються на тій же позиції'; + + @override + String get studyShareChanges => 'Поділитися змінами з глядачами та зберегти їх на сервері'; + + @override + String get studyPlaying => 'Активні'; + + @override + String get studyShowEvalBar => 'Шкала оцінки'; + + @override + String get studyFirst => 'Перша'; + + @override + String get studyPrevious => 'Попередня'; + + @override + String get studyNext => 'Наступна'; + + @override + String get studyLast => 'Остання'; + @override String get studyShareAndExport => 'Надсилання та експорт'; + @override + String get studyCloneStudy => 'Клонувати'; + + @override + String get studyStudyPgn => 'PGN дослідження'; + + @override + String get studyDownloadAllGames => 'Завантажити всі партії'; + + @override + String get studyChapterPgn => 'PGN розділу'; + + @override + String get studyCopyChapterPgn => 'Скопіювати PGN'; + + @override + String get studyDownloadGame => 'Завантажити гру'; + + @override + String get studyStudyUrl => 'Посилання на дослідження'; + + @override + String get studyCurrentChapterUrl => 'Посилання на цей розділ'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ви можете вставити цей код на форумі для вбудування'; + + @override + String get studyStartAtInitialPosition => 'Старт з початкової позиції'; + + @override + String studyStartAtX(String param) { + return 'Почати з $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вбудувати на своєму сайті'; + + @override + String get studyReadMoreAboutEmbedding => 'Докладніше про вбудовування'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Лише публічні дослідження можна вбудовувати!'; + + @override + String get studyOpen => 'Відкрити'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 надано вам $param2'; + } + + @override + String get studyStudyNotFound => 'Дослідження не знайдено'; + + @override + String get studyEditChapter => 'Редагувати розділ'; + + @override + String get studyNewChapter => 'Новий розділ'; + + @override + String studyImportFromChapterX(String param) { + return 'Імпортувати з $param'; + } + + @override + String get studyOrientation => 'Орієнтація'; + + @override + String get studyAnalysisMode => 'Режим аналізу'; + + @override + String get studyPinnedChapterComment => 'Закріплений коментар розділу'; + + @override + String get studySaveChapter => 'Зберегти розділ'; + + @override + String get studyClearAnnotations => 'Очистити анотацію'; + + @override + String get studyClearVariations => 'Очистити анотацію'; + + @override + String get studyDeleteChapter => 'Видалити розділ'; + + @override + String get studyDeleteThisChapter => 'Видалити цей розділ? Відновити буде неможливо!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Очистити всі коментарі та позначки з цього розділу?'; + + @override + String get studyRightUnderTheBoard => 'Відразу під шахівницею'; + + @override + String get studyNoPinnedComment => 'Немає'; + + @override + String get studyNormalAnalysis => 'Звичайний аналіз'; + + @override + String get studyHideNextMoves => 'Приховати наступні ходи'; + + @override + String get studyInteractiveLesson => 'Інтерактивний урок'; + + @override + String studyChapterX(String param) { + return 'Розділ $param'; + } + + @override + String get studyEmpty => 'Порожній'; + + @override + String get studyStartFromInitialPosition => 'Старт з початкової позиції'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Почати з обраної позиції'; + + @override + String get studyLoadAGameByUrl => 'Завантажте гру за посиланням'; + + @override + String get studyLoadAPositionFromFen => 'Завантажити позицію з FEN'; + + @override + String get studyLoadAGameFromPgn => 'Завантажити гру з PGN'; + + @override + String get studyAutomatic => 'Автоматично'; + + @override + String get studyUrlOfTheGame => 'Посилання на гру'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Завантажити гру з $param1 або $param2'; + } + + @override + String get studyCreateChapter => 'Створити розділ'; + + @override + String get studyCreateStudy => 'Створити дослідження'; + + @override + String get studyEditStudy => 'Редагування дослідження'; + + @override + String get studyVisibility => 'Видимість'; + + @override + String get studyPublic => 'Публічне'; + + @override + String get studyUnlisted => 'Поза списком'; + + @override + String get studyInviteOnly => 'Лише за запрошенням'; + + @override + String get studyAllowCloning => 'Дозволити копіювання'; + + @override + String get studyNobody => 'Ніхто'; + + @override + String get studyOnlyMe => 'Лише я'; + + @override + String get studyContributors => 'Співавтори'; + + @override + String get studyMembers => 'Учасники'; + + @override + String get studyEveryone => 'Всі'; + + @override + String get studyEnableSync => 'Увімкнути синхронізацію'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Так: однакова позиція для всіх'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ні: дозволити вільний перегляд'; + + @override + String get studyPinnedStudyComment => 'Закріплений коментар дослідження'; + @override String get studyStart => 'Почати'; + + @override + String get studySave => 'Зберегти'; + + @override + String get studyClearChat => 'Очистити чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Видалити історію чату дослідження? Відновити буде неможливо!'; + + @override + String get studyDeleteStudy => 'Видалити дослідження'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ви дійсно бажаєте видалити все дослідження? Назад дороги немає! Введіть назву дослідження для підтвердження: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Де ви хочете це дослідити?'; + + @override + String get studyGoodMove => 'Хороший хід'; + + @override + String get studyMistake => 'Помилка'; + + @override + String get studyBrilliantMove => 'Блискучий хід'; + + @override + String get studyBlunder => 'Груба помилка'; + + @override + String get studyInterestingMove => 'Цікавий хід'; + + @override + String get studyDubiousMove => 'Сумнівний хід'; + + @override + String get studyOnlyMove => 'Єдиний хід'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Рівна позиція'; + + @override + String get studyUnclearPosition => 'Незрозуміла позиція'; + + @override + String get studyWhiteIsSlightlyBetter => 'Позиція білих трохи краще'; + + @override + String get studyBlackIsSlightlyBetter => 'Позиція чорних трохи краще'; + + @override + String get studyWhiteIsBetter => 'Позиція білих краще'; + + @override + String get studyBlackIsBetter => 'Позиція чорних краще'; + + @override + String get studyWhiteIsWinning => 'Білі перемагають'; + + @override + String get studyBlackIsWinning => 'Чорні перемагають'; + + @override + String get studyNovelty => 'Новинка'; + + @override + String get studyDevelopment => 'Розвиток'; + + @override + String get studyInitiative => 'Ініціатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контргра'; + + @override + String get studyTimeTrouble => 'Цейтнот'; + + @override + String get studyWithCompensation => 'З компенсацією'; + + @override + String get studyWithTheIdea => 'З ідеєю'; + + @override + String get studyNextChapter => 'Наступний розділ'; + + @override + String get studyPrevChapter => 'Попередній розділ'; + + @override + String get studyStudyActions => 'Команди дослідження'; + + @override + String get studyTopics => 'Теми'; + + @override + String get studyMyTopics => 'Мої теми'; + + @override + String get studyPopularTopics => 'Популярні теми'; + + @override + String get studyManageTopics => 'Управління темами'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Грати знову'; + + @override + String get studyWhatWouldYouPlay => 'Що б ви грали в цій позиції?'; + + @override + String get studyYouCompletedThisLesson => 'Вітаємо! Ви завершили цей урок.'; + + @override + String studyPerPage(String param) { + return '$param на сторінку'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count розділи', + many: '$count розділів', + few: '$count розділи', + one: '$count розділ', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Партій', + many: '$count Партій', + few: '$count Партії', + one: '$count Партія', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count учасників', + many: '$count учасників', + few: '$count учасники', + one: '$count учасник', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Вставте ваш PGN текст тут, до $count ігор', + many: 'Вставте ваш PGN текст тут, до $count ігор', + few: 'Вставте ваш PGN текст тут, до $count ігор', + one: 'Вставте ваш PGN текст тут, до $count гри', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'щойно'; + + @override + String get timeagoRightNow => 'зараз'; + + @override + String get timeagoCompleted => 'завершено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count секунди', + many: 'за $count секунд', + few: 'за $count секунди', + one: 'за $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count хвилини', + many: 'за $count хвилин', + few: 'за $count хвилини', + one: 'за $count хвилину', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count години', + many: 'за $count годин', + few: 'за $count години', + one: 'за $count годину', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count дня', + many: 'за $count днів', + few: 'за $count дні', + one: 'за $count день', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count тижня', + many: 'за $count тижнів', + few: 'за $count тижні', + one: 'за $count тиждень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count місяця', + many: 'за $count місяців', + few: 'за $count місяці', + one: 'за $count місяць', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count року', + many: 'за $count років', + few: 'за $count роки', + one: 'за $count рік', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count хвилини тому', + many: '$count хвилин тому', + few: '$count хвилини тому', + one: '$count хвилину тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count години тому', + many: '$count годин тому', + few: '$count години тому', + one: '$count годину тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дня тому', + many: '$count днів тому', + few: '$count дні тому', + one: '$count день тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count тижня тому', + many: '$count тижнів тому', + few: '$count тижні тому', + one: '$count тиждень тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count місяця тому', + many: '$count місяців тому', + few: '$count місяці тому', + one: '$count місяць тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count року тому', + many: '$count років тому', + few: '$count роки тому', + one: '$count рік тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'залишилося $count хвилини', + many: 'залишилося $count хвилин', + few: 'залишилося $count хвилини', + one: 'залишилася $count хвилина', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'залишилося $count години', + many: 'залишилося $count годин', + few: 'залишилося $count години', + one: 'залишилася $count година', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index 700900240c..d3117af9cb 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,52 +9,54 @@ class AppLocalizationsVi extends AppLocalizations { AppLocalizationsVi([String locale = 'vi']) : super(locale); @override - String get mobileHomeTab => 'Trang chủ'; + String get mobileAllGames => 'Tất cả ván đấu'; @override - String get mobilePuzzlesTab => 'Câu đố'; + String get mobileAreYouSure => 'Bạn chắc chứ?'; @override - String get mobileToolsTab => 'Công cụ'; + String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; @override - String get mobileWatchTab => 'Xem'; + String get mobileClearButton => 'Xóa'; @override - String get mobileSettingsTab => 'Cài đặt'; + String get mobileCorrespondenceClearSavedMove => 'Xóa nước cờ đã lưu'; @override - String get mobileMustBeLoggedIn => 'Bạn phải đăng nhập để xem trang này.'; + String get mobileCustomGameJoinAGame => 'Tham gia một ván cờ'; @override - String get mobileSystemColors => 'Màu hệ thống'; + String get mobileFeedbackButton => 'Phản hồi'; @override - String get mobileFeedbackButton => 'Phản hồi'; + String mobileGreeting(String param) { + return 'Xin chào, $param'; + } @override - String get mobileOkButton => 'OK'; + String get mobileGreetingWithoutName => 'Xin chào'; @override - String get mobileSettingsHapticFeedback => 'Rung phản hồi'; + String get mobileHideVariation => 'Ẩn các biến'; @override - String get mobileSettingsImmersiveMode => 'Chế độ toàn màn hình'; + String get mobileHomeTab => 'Trang chủ'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.'; + String get mobileLiveStreamers => 'Các Streamer phát trực tiếp'; @override - String get mobileNotFollowingAnyUser => 'Bạn chưa theo dõi người dùng nào.'; + String get mobileMustBeLoggedIn => 'Bạn phải đăng nhập để xem trang này.'; @override - String get mobileAllGames => 'Tất cả ván đấu'; + String get mobileNoSearchResults => 'Không có kết quả'; @override - String get mobileRecentSearches => 'Tìm kiếm gần đây'; + String get mobileNotFollowingAnyUser => 'Bạn chưa theo dõi người dùng nào.'; @override - String get mobileClearButton => 'Xóa'; + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,87 +64,82 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Không có kết quả'; + String get mobilePrefMagnifyDraggedPiece => 'Phóng to quân cờ được kéo'; @override - String get mobileAreYouSure => 'Bạn chắc chứ?'; + String get mobilePuzzleStormConfirmEndRun => 'Bạn có muốn kết thúc lượt chạy này không?'; @override - String get mobilePuzzleStreakAbortWarning => 'Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.'; + String get mobilePuzzleStormFilterNothingToShow => 'Không có gì để hiển thị, vui lòng thay đổi bộ lọc'; @override String get mobilePuzzleStormNothingToShow => 'Không có gì để xem. Chơi một vài ván Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Chia sẻ câu đố này'; + String get mobilePuzzleStormSubtitle => 'Giải càng nhiều câu đố càng tốt trong 3 phút.'; @override - String get mobileShareGameURL => 'Chia sẻ URL ván cờ'; + String get mobilePuzzleStreakAbortWarning => 'Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.'; @override - String get mobileShareGamePGN => 'Chia sẻ tập tin PGN'; + String get mobilePuzzleThemesSubtitle => 'Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.'; @override - String get mobileSharePositionAsFEN => 'Chia sẻ thế cờ dạng FEN'; + String get mobilePuzzlesTab => 'Câu đố'; @override - String get mobileShowVariations => 'Hiện các biến'; + String get mobileRecentSearches => 'Tìm kiếm gần đây'; @override - String get mobileHideVariation => 'Ẩn các biến'; + String get mobileSettingsHapticFeedback => 'Rung phản hồi'; @override - String get mobileShowComments => 'Hiển thị bình luận'; + String get mobileSettingsImmersiveMode => 'Chế độ toàn màn hình'; @override - String get mobilePuzzleStormConfirmEndRun => 'Bạn có muốn kết thúc lượt chạy này không?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Không có gì để hiển thị, vui lòng thay đổi bộ lọc'; + String get mobileSettingsTab => 'Cài đặt'; @override - String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; + String get mobileShareGamePGN => 'Chia sẻ tập tin PGN'; @override - String get mobileCancelDrawOffer => 'Hủy đề nghị hòa'; + String get mobileShareGameURL => 'Chia sẻ URL ván cờ'; @override - String get mobileWaitingForOpponentToJoin => 'Đang chờ đối thủ tham gia...'; + String get mobileSharePositionAsFEN => 'Chia sẻ thế cờ dạng FEN'; @override - String get mobileBlindfoldMode => 'Bịt mắt'; + String get mobileSharePuzzle => 'Chia sẻ câu đố này'; @override - String get mobileLiveStreamers => 'Các Streamer phát trực tiếp'; + String get mobileShowComments => 'Hiển thị bình luận'; @override - String get mobileCustomGameJoinAGame => 'Tham gia một ván cờ'; + String get mobileShowResult => 'Xem kết quả'; @override - String get mobileCorrespondenceClearSavedMove => 'Xóa nước cờ đã lưu'; + String get mobileShowVariations => 'Hiện các biến'; @override String get mobileSomethingWentWrong => 'Đã xảy ra lỗi.'; @override - String get mobileShowResult => 'Xem kết quả'; - - @override - String get mobilePuzzleThemesSubtitle => 'Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.'; + String get mobileSystemColors => 'Màu hệ thống'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Giao diện'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Công cụ'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Đang chờ đối thủ tham gia...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Xem'; @override String get activityActivity => 'Hoạt động'; @@ -156,7 +153,7 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String get activitySignedUp => 'Đã ghi danh ở lichess.org'; + String get activitySignedUp => 'Đã ghi danh tại lichess.org'; @override String activitySupportedNbMonths(int count, String param2) { @@ -238,12 +235,22 @@ class AppLocalizationsVi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Đã hoàn thành $count ván cờ qua thư $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Đã theo dõi $count người chơi', + other: 'Đã theo dõi $count kỳ thủ', ); return '$_temp0'; } @@ -331,9 +338,255 @@ class AppLocalizationsVi extends AppLocalizations { @override String get broadcastBroadcasts => 'Các phát sóng'; + @override + String get broadcastMyBroadcasts => 'Các phát sóng của tôi'; + @override String get broadcastLiveBroadcasts => 'Các giải đấu phát sóng trực tiếp'; + @override + String get broadcastBroadcastCalendar => 'Lịch phát sóng'; + + @override + String get broadcastNewBroadcast => 'Phát sóng trực tiếp mới'; + + @override + String get broadcastSubscribedBroadcasts => 'Các phát sóng đã đăng ký theo dõi'; + + @override + String get broadcastAboutBroadcasts => 'Giới thiệu về phát sóng'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Cách sử dụng Phát sóng của Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước.'; + + @override + String get broadcastAddRound => 'Thêm vòng'; + + @override + String get broadcastOngoing => 'Đang diễn ra'; + + @override + String get broadcastUpcoming => 'Sắp diễn ra'; + + @override + String get broadcastCompleted => 'Đã hoàn thành'; + + @override + String get broadcastCompletedHelp => 'Lichess phát hiện việc hoàn thành vòng đấu dựa trên các ván đấu nguồn. Sử dụng nút chuyển đổi này nếu không có nguồn.'; + + @override + String get broadcastRoundName => 'Tên vòng'; + + @override + String get broadcastRoundNumber => 'Vòng đấu số'; + + @override + String get broadcastTournamentName => 'Tên giải đấu'; + + @override + String get broadcastTournamentDescription => 'Mô tả ngắn giải đấu'; + + @override + String get broadcastFullDescription => 'Mô tả đầy đủ giải đấu'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Tùy chọn mô tả dài về giải đấu. Có thể sử dụng $param1. Độ dài phải nhỏ hơn $param2 ký tự.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL Nguồn PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL mà Lichess sẽ khảo sát để nhận cập nhật PGN. Nó phải được truy cập công khai từ Internet.'; + + @override + String get broadcastSourceGameIds => 'Tối đa 64 ID ván cờ trên Lichess, phân tách bằng dấu cách.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Thời gian bắt đầu của giải theo múi giờ địa phương: $param'; + } + + @override + String get broadcastStartDateHelp => 'Tùy chọn, nếu bạn biết khi nào sự kiện bắt đầu'; + + @override + String get broadcastCurrentGameUrl => 'URL ván đấu hiện tại'; + + @override + String get broadcastDownloadAllRounds => 'Tải về tất cả ván đấu'; + + @override + String get broadcastResetRound => 'Đặt lại vòng này'; + + @override + String get broadcastDeleteRound => 'Xóa vòng này'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dứt khoát xóa tất cả vòng đấu và các ván đấu trong đó.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Xóa toàn bộ ván cờ trong vòng này. Để tạo lại chúng bạn cần thêm lại nguồn.'; + + @override + String get broadcastEditRoundStudy => 'Chỉnh sửa vòng nghiên cứu'; + + @override + String get broadcastDeleteTournament => 'Xóa giải đấu này'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Xóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó.'; + + @override + String get broadcastShowScores => 'Hiển thị điểm số của người chơi dựa trên kết quả ván đấu'; + + @override + String get broadcastReplacePlayerTags => 'Tùy chọn: biệt danh, hệ số Elo và danh hiệu'; + + @override + String get broadcastFideFederations => 'Các liên đoàn FIDE'; + + @override + String get broadcastTop10Rating => 'Hệ số Elo top 10'; + + @override + String get broadcastFidePlayers => 'Các kỳ thủ FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Không tìm thấy kỳ thủ FIDE'; + + @override + String get broadcastFideProfile => 'Hồ sơ FIDE'; + + @override + String get broadcastFederation => 'Liên đoàn'; + + @override + String get broadcastAgeThisYear => 'Tuổi năm nay'; + + @override + String get broadcastUnrated => 'Chưa xếp hạng'; + + @override + String get broadcastRecentTournaments => 'Các giải đấu tham gia gần đây'; + + @override + String get broadcastOpenLichess => 'Mở trên Lichess'; + + @override + String get broadcastTeams => 'Các đội'; + + @override + String get broadcastBoards => 'Các bàn đấu'; + + @override + String get broadcastOverview => 'Tổng quan'; + + @override + String get broadcastSubscribeTitle => 'Đăng ký để được thông báo khi mỗi vòng bắt đầu. Bạn có thể chuyển đổi chuông hoặc thông báo đẩy cho các chương trình phát sóng trong tùy chọn tài khoản của mình.'; + + @override + String get broadcastUploadImage => 'Tải hình ảnh giải đấu lên'; + + @override + String get broadcastNoBoardsYet => 'Chưa có bàn nào. Chúng sẽ xuất hiện khi ván đấu được tải lên.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Bàn đấu có thể được tải bằng nguồn hoặc thông qua $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Bắt đầu sau $param'; + } + + @override + String get broadcastStartVerySoon => 'Chương trình phát sóng sẽ sớm bắt đầu.'; + + @override + String get broadcastNotYetStarted => 'Chương trình phát sóng vẫn chưa bắt đầu.'; + + @override + String get broadcastOfficialWebsite => 'Website chính thức'; + + @override + String get broadcastStandings => 'Bảng xếp hạng'; + + @override + String get broadcastOfficialStandings => 'Bảng xếp hạng Chính thức'; + + @override + String broadcastIframeHelp(String param) { + return 'Thêm tùy chọn trên $param'; + } + + @override + String get broadcastWebmastersPage => 'trang nhà phát triển web'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Nguồn PGN công khai, thời gian thực cho vòng này. Chúng tôi cũng cung cấp $param để đồng bộ hóa nhanh hơn và hiệu quả hơn.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Nhúng chương trình phát sóng này vào trang web của bạn'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Nhúng $param vào trang web của bạn'; + } + + @override + String get broadcastRatingDiff => 'Độ thay đổi hệ số'; + + @override + String get broadcastGamesThisTournament => 'Các ván đấu trong giải này'; + + @override + String get broadcastScore => 'Điểm số'; + + @override + String get broadcastAllTeams => 'Tất cả đội'; + + @override + String get broadcastTournamentFormat => 'Điều lệ giải đấu'; + + @override + String get broadcastTournamentLocation => 'Địa điểm tổ chức giải đấu'; + + @override + String get broadcastTopPlayers => 'Những kỳ thủ hàng đầu'; + + @override + String get broadcastTimezone => 'Múi giờ'; + + @override + String get broadcastFideRatingCategory => 'Thể loại xếp hạng FIDE'; + + @override + String get broadcastOptionalDetails => 'Tùy chọn chi tiết'; + + @override + String get broadcastPastBroadcasts => 'Các phát sóng đã qua'; + + @override + String get broadcastAllBroadcastsByMonth => 'Xem tất cả phát sóng theo tháng'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count phát sóng', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Số thách đấu: $param1'; @@ -592,6 +845,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get preferencesInGameOnly => 'Chỉ trong ván cờ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Đồng hồ cờ vua'; @@ -733,6 +989,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Âm thanh chuông báo'; + @override + String get preferencesBlindfold => 'Bịt mắt'; + @override String get puzzlePuzzles => 'Câu đố'; @@ -1203,7 +1462,7 @@ class AppLocalizationsVi extends AppLocalizations { String get puzzleThemeMasterVsMaster => 'Ván đấu giữa 2 kiện tướng'; @override - String get puzzleThemeMasterVsMasterDescription => 'Câu đố từ các ván đấu giữa hai người chơi có danh hiệu.'; + String get puzzleThemeMasterVsMasterDescription => 'Câu đố từ các ván đấu giữa hai kiện tướng.'; @override String get puzzleThemeMate => 'Chiếu hết'; @@ -1368,10 +1627,10 @@ class AppLocalizationsVi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Đối phương bị giới hạn các nước mà họ có thể đi và tất cả các nước đi ấy đều hại họ.'; @override - String get puzzleThemeHealthyMix => 'Phối hợp nhịp nhàng'; + String get puzzleThemeMix => 'Phối hợp nhịp nhàng'; @override - String get puzzleThemeHealthyMixDescription => 'Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!'; + String get puzzleThemeMixDescription => 'Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!'; @override String get puzzleThemePlayerGames => 'Các ván đấu của người chơi'; @@ -1421,7 +1680,7 @@ class AppLocalizationsVi extends AppLocalizations { String get toInviteSomeoneToPlayGiveThisUrl => 'Để mời ai đó chơi, hãy gửi URL này'; @override - String get gameOver => 'Kết thúc ván cờ'; + String get gameOver => 'Ván cờ kết thúc'; @override String get waitingForOpponent => 'Đang chờ đối thủ'; @@ -1543,7 +1802,7 @@ class AppLocalizationsVi extends AppLocalizations { String get whiteResigned => 'Bên trắng chịu thua'; @override - String get blackResigned => 'Đen chịu thua'; + String get blackResigned => 'Bên đen chịu thua'; @override String get whiteLeftTheGame => 'Bên trắng đã rời khỏi ván cờ'; @@ -1561,13 +1820,13 @@ class AppLocalizationsVi extends AppLocalizations { String get requestAComputerAnalysis => 'Yêu cầu máy tính phân tích'; @override - String get computerAnalysis => 'Máy tính phân tích'; + String get computerAnalysis => 'Phân tích từ máy tính'; @override - String get computerAnalysisAvailable => 'Có sẵn máy tính phân tích'; + String get computerAnalysisAvailable => 'Có sẵn phân tích từ máy tính'; @override - String get computerAnalysisDisabled => 'Phân tích máy tính bị vô hiệu hóa'; + String get computerAnalysisDisabled => 'Phân tích từ máy tính bị vô hiệu hóa'; @override String get analysis => 'Bàn cờ phân tích'; @@ -1677,7 +1936,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'Các ván đấu OTB của các kỳ thủ có hệ số Elo FIDE $param1+ từ năm $param2 đến $param3'; + return 'Các ván đấu OTB của các kỳ thủ có hệ số Rating FIDE $param1+ từ năm $param2 đến $param3'; } @override @@ -1745,9 +2004,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get byCPL => 'Theo phần trăm mất tốt (CPL)'; - @override - String get openStudy => 'Mở nghiên cứu'; - @override String get enable => 'Bật'; @@ -1775,9 +2031,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get removesTheDepthLimit => 'Bỏ giới hạn độ sâu và giữ máy tính của bạn mượt hơn'; - @override - String get engineManager => 'Quản lý động cơ'; - @override String get blunder => 'Sai lầm nghiêm trọng'; @@ -1910,7 +2163,7 @@ class AppLocalizationsVi extends AppLocalizations { String get changeUsername => 'Thay đổi tên đăng nhập'; @override - String get changeUsernameNotSame => 'Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"johndoe\" thành \"JohnDoe\".'; + String get changeUsernameNotSame => 'Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"dotrongkhanh04032012\" thành \"DoTrongKhanh04032012\".'; @override String get changeUsernameDescription => 'Thay đổi tên người dùng của bạn. Điều này chỉ có thể thực hiện một lần và bạn chỉ được thay đổi cách viết hoa/viết thường các chữ trong tên người dùng của bạn.'; @@ -2041,6 +2294,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get gamesPlayed => 'Số ván đã chơi'; + @override + String get ok => 'OK'; + @override String get cancel => 'Hủy'; @@ -2361,7 +2617,7 @@ class AppLocalizationsVi extends AppLocalizations { String get thisIsAChessCaptcha => 'Đây là mã CAPTCHA cờ vua.'; @override - String get clickOnTheBoardToMakeYourMove => 'Nhấn vào bàn cờ để di chuyển và chứng minh bạn là con người.'; + String get clickOnTheBoardToMakeYourMove => 'Nhấn vào bàn cờ để di chuyển, và chứng minh bạn là con người.'; @override String get captcha_fail => 'Hãy giải mã captcha cờ vua.'; @@ -2415,9 +2671,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get unblock => 'Bỏ chặn'; - @override - String get followsYou => 'Theo dõi bạn'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 đã bắt đầu theo dõi $param2'; @@ -2750,7 +3003,13 @@ class AppLocalizationsVi extends AppLocalizations { String get other => 'Khác'; @override - String get reportDescriptionHelp => 'Dán đường dẫn đến (các) ván cờ và giải thích về vấn đề của kỳ thủ này. Đừng chỉ nói \"họ gian lận\" mà hãy miêu tả chi tiết nhất có thể. Vấn đề sẽ được giải quyết nhanh hơn nếu bạn viết bằng tiếng Anh.'; + String get reportCheatBoostHelp => 'Dán đường dẫn đến (các) ván cờ và giải thích hành vi sai của kỳ thủ này. Đừng chỉ nói \"họ gian lận\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào.'; + + @override + String get reportUsernameHelp => 'Giải thích những gì về tên người dùng này là xúc phạm. Đừng chỉ nói \"nó gây khó chịu/không phù hợp\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào, đặc biệt nếu sự xúc phạm bị che giấu, không phải bằng tiếng Anh, là tiếng lóng, hoặc là một tài liệu tham khảo lịch sử/văn hóa.'; + + @override + String get reportProcessedFasterInEnglish => 'Báo cáo của bạn sẽ được xử lý nhanh hơn nếu được viết bằng tiếng Anh.'; @override String get error_provideOneCheatedGameLink => 'Hãy cung cấp ít nhất một đường dẫn đến ván cờ bị gian lận.'; @@ -2921,7 +3180,7 @@ class AppLocalizationsVi extends AppLocalizations { String get opponent => 'Đối thủ'; @override - String get learnMenu => 'Học'; + String get learnMenu => 'Học tập'; @override String get studyMenu => 'Nghiên cứu'; @@ -3205,7 +3464,7 @@ class AppLocalizationsVi extends AppLocalizations { String get tournamentHomeTitle => 'Giải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú'; @override - String get tournamentHomeDescription => 'Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.'; + String get tournamentHomeDescription => 'Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ đạn, Cờ chớp, Cờ nhanh, Cờ chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.'; @override String get tournamentNotFound => 'Không tìm thấy giải đấu'; @@ -4055,6 +4314,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get nothingToSeeHere => 'Không có gì để xem ở đây vào lúc này.'; + @override + String get stats => 'Thống kê'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4657,9 +4919,674 @@ class AppLocalizationsVi extends AppLocalizations { @override String get streamerLichessStreamers => 'Các Streamer của Lichess'; + @override + String get studyPrivate => 'Riêng tư'; + + @override + String get studyMyStudies => 'Các nghiên cứu của tôi'; + + @override + String get studyStudiesIContributeTo => 'Các nghiên cứu tôi đóng góp'; + + @override + String get studyMyPublicStudies => 'Nghiên cứu công khai của tôi'; + + @override + String get studyMyPrivateStudies => 'Nghiên cứu riêng tư của tôi'; + + @override + String get studyMyFavoriteStudies => 'Các nghiên cứu yêu thích của tôi'; + + @override + String get studyWhatAreStudies => 'Nghiên cứu là gì?'; + + @override + String get studyAllStudies => 'Tất cả các nghiên cứu'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Các nghiên cứu được tạo bởi $param'; + } + + @override + String get studyNoneYet => 'Chưa có gì cả.'; + + @override + String get studyHot => 'Thịnh hành'; + + @override + String get studyDateAddedNewest => 'Ngày được thêm (mới nhất)'; + + @override + String get studyDateAddedOldest => 'Ngày được thêm (cũ nhất)'; + + @override + String get studyRecentlyUpdated => 'Được cập nhật gần đây'; + + @override + String get studyMostPopular => 'Phổ biến nhất'; + + @override + String get studyAlphabetical => 'Theo thứ tự chữ cái'; + + @override + String get studyAddNewChapter => 'Thêm một chương mới'; + + @override + String get studyAddMembers => 'Thêm thành viên'; + + @override + String get studyInviteToTheStudy => 'Mời vào nghiên cứu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Vui lòng chỉ mời những người bạn biết và những người tích cực muốn tham gia nghiên cứu này.'; + + @override + String get studySearchByUsername => 'Tìm kiếm theo tên người dùng'; + + @override + String get studySpectator => 'Khán giả'; + + @override + String get studyContributor => 'Người đóng góp'; + + @override + String get studyKick => 'Đuổi'; + + @override + String get studyLeaveTheStudy => 'Rời khỏi nghiên cứu'; + + @override + String get studyYouAreNowAContributor => 'Bây giờ bạn là một người đóng góp'; + + @override + String get studyYouAreNowASpectator => 'Bây giờ bạn là một khán giả'; + + @override + String get studyPgnTags => 'Nhãn PGN'; + + @override + String get studyLike => 'Thích'; + + @override + String get studyUnlike => 'Bỏ thích'; + + @override + String get studyNewTag => 'Nhãn mới'; + + @override + String get studyCommentThisPosition => 'Bình luận về thế cờ này'; + + @override + String get studyCommentThisMove => 'Bình luận về nước cờ này'; + + @override + String get studyAnnotateWithGlyphs => 'Chú thích bằng dấu'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Chương này quá ngắn để có thể được phân tích.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Chỉ những người đóng góp nghiên cứu mới có thể yêu cầu máy tính phân tích.'; + + @override + String get studyGetAFullComputerAnalysis => 'Nhận phân tích máy tính phía máy chủ đầy đủ về biến chính.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Hãy chắc chắn chương đã hoàn thành. Bạn chỉ có thể yêu cầu phân tích 1 lần.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Đồng bộ hóa tất cả các thành viên trên cùng một thế cờ'; + + @override + String get studyShareChanges => 'Chia sẻ các thay đổi với khán giả và lưu chúng trên máy chủ'; + + @override + String get studyPlaying => 'Đang chơi'; + + @override + String get studyShowEvalBar => 'Thanh lợi thế'; + + @override + String get studyFirst => 'Trang đầu'; + + @override + String get studyPrevious => 'Trang trước'; + + @override + String get studyNext => 'Trang tiếp theo'; + + @override + String get studyLast => 'Trang cuối'; + @override String get studyShareAndExport => 'Chia sẻ & xuất'; + @override + String get studyCloneStudy => 'Nhân bản'; + + @override + String get studyStudyPgn => 'PGN nghiên cứu'; + + @override + String get studyDownloadAllGames => 'Tải về tất cả ván đấu'; + + @override + String get studyChapterPgn => 'PGN chương'; + + @override + String get studyCopyChapterPgn => 'Sao chép PGN'; + + @override + String get studyDownloadGame => 'Tải về ván cờ'; + + @override + String get studyStudyUrl => 'URL nghiên cứu'; + + @override + String get studyCurrentChapterUrl => 'URL chương hiện tại'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Bạn có thể dán cái này để nhúng vào diễn đàn hoặc blog Lichess cá nhân của bạn'; + + @override + String get studyStartAtInitialPosition => 'Bắt đầu từ thế cờ ban đầu'; + + @override + String studyStartAtX(String param) { + return 'Bắt đầu tại nước $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Nhúng vào trang web của bạn'; + + @override + String get studyReadMoreAboutEmbedding => 'Đọc thêm về việc nhúng'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Chỉ các nghiên cứu công khai mới được nhúng!'; + + @override + String get studyOpen => 'Mở'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 được lấy từ $param2'; + } + + @override + String get studyStudyNotFound => 'Không tìm thấy nghiên cứu nào'; + + @override + String get studyEditChapter => 'Sửa chương'; + + @override + String get studyNewChapter => 'Chương mới'; + + @override + String studyImportFromChapterX(String param) { + return 'Nhập từ chương $param'; + } + + @override + String get studyOrientation => 'Nghiên cứu cho bên'; + + @override + String get studyAnalysisMode => 'Chế độ phân tích'; + + @override + String get studyPinnedChapterComment => 'Đã ghim bình luận chương'; + + @override + String get studySaveChapter => 'Lưu chương'; + + @override + String get studyClearAnnotations => 'Xóa chú thích'; + + @override + String get studyClearVariations => 'Xóa các biến'; + + @override + String get studyDeleteChapter => 'Xóa chương'; + + @override + String get studyDeleteThisChapter => 'Xóa chương này. Sẽ không có cách nào để có thể khôi phục lại!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Xóa tất cả bình luận, dấu chú thích và hình vẽ trong chương này'; + + @override + String get studyRightUnderTheBoard => 'Ngay dưới bàn cờ'; + + @override + String get studyNoPinnedComment => 'Không có'; + + @override + String get studyNormalAnalysis => 'Phân tích thường'; + + @override + String get studyHideNextMoves => 'Ẩn các nước tiếp theo'; + + @override + String get studyInteractiveLesson => 'Bài học tương tác'; + + @override + String studyChapterX(String param) { + return 'Chương $param'; + } + + @override + String get studyEmpty => 'Trống'; + + @override + String get studyStartFromInitialPosition => 'Bắt đầu từ thế cờ ban đầu'; + + @override + String get studyEditor => 'Chỉnh sửa bàn cờ'; + + @override + String get studyStartFromCustomPosition => 'Bắt đầu từ thế cờ tùy chỉnh'; + + @override + String get studyLoadAGameByUrl => 'Tải ván cờ bằng URL'; + + @override + String get studyLoadAPositionFromFen => 'Tải thế cờ từ chuỗi FEN'; + + @override + String get studyLoadAGameFromPgn => 'Tải ván cờ từ PGN'; + + @override + String get studyAutomatic => 'Tự động'; + + @override + String get studyUrlOfTheGame => 'URL của các ván, một URL mỗi dòng'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Tải ván cờ từ $param1 hoặc $param2'; + } + + @override + String get studyCreateChapter => 'Tạo chương'; + + @override + String get studyCreateStudy => 'Tạo nghiên cứu'; + + @override + String get studyEditStudy => 'Chỉnh sửa nghiên cứu'; + + @override + String get studyVisibility => 'Khả năng hiển thị'; + + @override + String get studyPublic => 'Công khai'; + + @override + String get studyUnlisted => 'Không công khai'; + + @override + String get studyInviteOnly => 'Chỉ những người được mời'; + + @override + String get studyAllowCloning => 'Cho phép tạo bản sao'; + + @override + String get studyNobody => 'Không ai cả'; + + @override + String get studyOnlyMe => 'Chỉ mình tôi'; + + @override + String get studyContributors => 'Những người đóng góp'; + + @override + String get studyMembers => 'Thành viên'; + + @override + String get studyEveryone => 'Mọi người'; + + @override + String get studyEnableSync => 'Kích hoạt tính năng đồng bộ hóa'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Có: giữ tất cả mọi người trên 1 thế cờ'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Không: để mọi người tự do xem xét'; + + @override + String get studyPinnedStudyComment => 'Bình luận nghiên cứu được ghim'; + @override String get studyStart => 'Bắt đầu'; + + @override + String get studySave => 'Lưu'; + + @override + String get studyClearChat => 'Xóa trò chuyện'; + + @override + String get studyDeleteTheStudyChatHistory => 'Xóa lịch sử trò chuyện nghiên cứu? Không thể khôi phục lại!'; + + @override + String get studyDeleteStudy => 'Xóa nghiên cứu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Xóa toàn bộ nghiên cứu? Không có cách nào để khôi phục lại! Nhập tên của nghiên cứu để xác nhận: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Bạn muốn nghiên cứu ở đâu?'; + + @override + String get studyGoodMove => 'Nước tốt'; + + @override + String get studyMistake => 'Sai lầm'; + + @override + String get studyBrilliantMove => 'Nước đi thiên tài'; + + @override + String get studyBlunder => 'Sai lầm nghiêm trọng'; + + @override + String get studyInterestingMove => 'Nước đi hay'; + + @override + String get studyDubiousMove => 'Nước đi mơ hồ'; + + @override + String get studyOnlyMove => 'Nước duy nhất'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Thế trận cân bằng'; + + @override + String get studyUnclearPosition => 'Thế cờ không rõ ràng'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bên trắng có một chút lợi thế'; + + @override + String get studyBlackIsSlightlyBetter => 'Bên đen có một chút lợi thế'; + + @override + String get studyWhiteIsBetter => 'Bên trắng lợi thế hơn'; + + @override + String get studyBlackIsBetter => 'Bên đen lợi thế hơn'; + + @override + String get studyWhiteIsWinning => 'Bên trắng đang thắng dần'; + + @override + String get studyBlackIsWinning => 'Bên đen đang thắng dần'; + + @override + String get studyNovelty => 'Mới lạ'; + + @override + String get studyDevelopment => 'Phát triển'; + + @override + String get studyInitiative => 'Chủ động'; + + @override + String get studyAttack => 'Tấn công'; + + @override + String get studyCounterplay => 'Phản công'; + + @override + String get studyTimeTrouble => 'Sắp hết thời gian'; + + @override + String get studyWithCompensation => 'Có bù đắp'; + + @override + String get studyWithTheIdea => 'Với ý tưởng'; + + @override + String get studyNextChapter => 'Chương tiếp theo'; + + @override + String get studyPrevChapter => 'Chương trước'; + + @override + String get studyStudyActions => 'Các thao tác trong nghiên cứu'; + + @override + String get studyTopics => 'Chủ đề'; + + @override + String get studyMyTopics => 'Chủ đề của tôi'; + + @override + String get studyPopularTopics => 'Chủ đề phổ biến'; + + @override + String get studyManageTopics => 'Quản lý chủ đề'; + + @override + String get studyBack => 'Quay Lại'; + + @override + String get studyPlayAgain => 'Chơi lại'; + + @override + String get studyWhatWouldYouPlay => 'Bạn sẽ làm gì ở thế cờ này?'; + + @override + String get studyYouCompletedThisLesson => 'Chúc mừng! Bạn đã hoàn thành bài học này.'; + + @override + String studyPerPage(String param) { + return '$param mỗi trang'; + } + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chương', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ván cờ', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Thành viên', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dán PGN ở đây, tối đa $count ván', + ); + return '$_temp0'; + } + + @override + String get timeagoJustNow => 'vừa mới đây'; + + @override + String get timeagoRightNow => 'ngay bây giờ'; + + @override + String get timeagoCompleted => 'đã hoàn thành'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count giây', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count phút', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count giờ', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count ngày', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count tuần', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count tháng', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count năm', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count phút trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count giờ trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ngày trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuần trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tháng trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count năm trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'còn $count phút', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'còn $count giờ', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 75b678ce81..d38b2f25dd 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint @@ -9,140 +9,137 @@ class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override - String get mobileHomeTab => '主页'; + String get mobileAllGames => '所有对局'; @override - String get mobilePuzzlesTab => '谜题'; + String get mobileAreYouSure => '你确定吗?'; @override - String get mobileToolsTab => '工具'; + String get mobileCancelTakebackOffer => '取消悔棋请求'; @override - String get mobileWatchTab => '观看'; + String get mobileClearButton => '清空'; @override - String get mobileSettingsTab => '设置'; + String get mobileCorrespondenceClearSavedMove => '清除已保存的着法'; @override - String get mobileMustBeLoggedIn => '您必须登录才能浏览此页面。'; + String get mobileCustomGameJoinAGame => '加入一局游戏'; @override - String get mobileSystemColors => '系统颜色'; + String get mobileFeedbackButton => '问题反馈'; @override - String get mobileFeedbackButton => '问题反馈'; + String mobileGreeting(String param) { + return '你好,$param'; + } @override - String get mobileOkButton => '好'; + String get mobileGreetingWithoutName => '你好!'; @override - String get mobileSettingsHapticFeedback => '震动反馈'; + String get mobileHideVariation => '隐藏变着'; @override - String get mobileSettingsImmersiveMode => '沉浸模式'; + String get mobileHomeTab => '主页'; @override - String get mobileSettingsImmersiveModeSubtitle => '播放时隐藏系统UI。 如果您对屏幕边缘的系统导航手势感到困扰,请使用此功能。 适用于游戏和益智风暴屏幕。'; + String get mobileLiveStreamers => '主播'; @override - String get mobileNotFollowingAnyUser => '你没有关注任何用户。'; + String get mobileMustBeLoggedIn => '您必须登录才能浏览此页面。'; @override - String get mobileAllGames => '所有对局'; + String get mobileNoSearchResults => '无结果'; @override - String get mobileRecentSearches => '最近搜索'; + String get mobileNotFollowingAnyUser => '你没有关注任何用户。'; @override - String get mobileClearButton => '清空'; + String get mobileOkButton => '好'; @override String mobilePlayersMatchingSearchTerm(String param) { - return '拥有\"$param\"的玩家'; + return '包含\"$param\"名称的棋手'; } @override - String get mobileNoSearchResults => '无结果'; - - @override - String get mobileAreYouSure => '你确定吗?'; + String get mobilePrefMagnifyDraggedPiece => '放大正在拖动的棋子'; @override - String get mobilePuzzleStreakAbortWarning => '你将失去你目前的连胜,你的分数将被保存。'; + String get mobilePuzzleStormConfirmEndRun => '你想结束这组吗?'; @override - String get mobilePuzzleStormNothingToShow => '没什么好表现的。 玩拼图风暴的一些运行。'; + String get mobilePuzzleStormFilterNothingToShow => '没有结果,请更改筛选条件'; @override - String get mobileSharePuzzle => '分享这个谜题'; + String get mobilePuzzleStormNothingToShow => '没有记录。 请下几组 Puzzle Storm。'; @override - String get mobileShareGameURL => '分享棋局链接'; + String get mobilePuzzleStormSubtitle => '在3分钟内解决尽可能多的谜题。'; @override - String get mobileShareGamePGN => '分享 PGN'; + String get mobilePuzzleStreakAbortWarning => '你将失去你目前的连胜,你的分数将被保存。'; @override - String get mobileSharePositionAsFEN => '保存局面为 FEN'; + String get mobilePuzzleThemesSubtitle => '从你最喜欢的开局解决谜题,或选择一个主题。'; @override - String get mobileShowVariations => '显示变化'; + String get mobilePuzzlesTab => '谜题'; @override - String get mobileHideVariation => '隐藏变异'; + String get mobileRecentSearches => '最近搜索'; @override - String get mobileShowComments => '显示评论'; + String get mobileSettingsHapticFeedback => '震动反馈'; @override - String get mobilePuzzleStormConfirmEndRun => '你想结束这次跑步吗?'; + String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get mobilePuzzleStormFilterNothingToShow => '没有显示,请更改过滤器'; + String get mobileSettingsImmersiveModeSubtitle => '下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。'; @override - String get mobileCancelTakebackOffer => '取消悔棋请求'; + String get mobileSettingsTab => '设置'; @override - String get mobileCancelDrawOffer => '取消和棋请求'; + String get mobileShareGamePGN => '分享 PGN'; @override - String get mobileWaitingForOpponentToJoin => '正在等待对手加入...'; + String get mobileShareGameURL => '分享棋局链接'; @override - String get mobileBlindfoldMode => '盲棋'; + String get mobileSharePositionAsFEN => '保存局面为 FEN'; @override - String get mobileLiveStreamers => '主播'; + String get mobileSharePuzzle => '分享这个谜题'; @override - String get mobileCustomGameJoinAGame => '加入一局游戏'; + String get mobileShowComments => '显示评论'; @override - String get mobileCorrespondenceClearSavedMove => '清除已保存的移动'; + String get mobileShowResult => '显示结果'; @override - String get mobileSomethingWentWrong => '发生一些错误。'; + String get mobileShowVariations => '显示变着'; @override - String get mobileShowResult => '显示结果'; + String get mobileSomethingWentWrong => '出了一些问题。'; @override - String get mobilePuzzleThemesSubtitle => '从你最喜欢的开口玩拼图,或选择一个主题。'; + String get mobileSystemColors => '系统颜色'; @override - String get mobilePuzzleStormSubtitle => '在3分钟内尽可能多地解决谜题'; + String get mobileTheme => '主题'; @override - String mobileGreeting(String param) { - return '你好,$param'; - } + String get mobileToolsTab => '工具'; @override - String get mobileGreetingWithoutName => '你好!'; + String get mobileWaitingForOpponentToJoin => '正在等待对手加入...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => '观看'; @override String get activityActivity => '动态'; @@ -238,6 +235,17 @@ class AppLocalizationsZh extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +339,255 @@ class AppLocalizationsZh extends AppLocalizations { @override String get broadcastBroadcasts => '转播'; + @override + String get broadcastMyBroadcasts => '我的直播'; + @override String get broadcastLiveBroadcasts => '赛事转播'; + @override + String get broadcastBroadcastCalendar => '转播日程表'; + + @override + String get broadcastNewBroadcast => '新建实况转播'; + + @override + String get broadcastSubscribedBroadcasts => '已订阅的转播'; + + @override + String get broadcastAboutBroadcasts => '关于转播'; + + @override + String get broadcastHowToUseLichessBroadcasts => '如何使用Lichess转播'; + + @override + String get broadcastTheNewRoundHelp => '新一轮的成员和贡献者将与前一轮相同。'; + + @override + String get broadcastAddRound => '添加一轮'; + + @override + String get broadcastOngoing => '进行中'; + + @override + String get broadcastUpcoming => '即将举行'; + + @override + String get broadcastCompleted => '已完成'; + + @override + String get broadcastCompletedHelp => 'Lichess基于源游戏检测游戏的完成状态。如果没有源,请使用此选项。'; + + @override + String get broadcastRoundName => '轮次名称'; + + @override + String get broadcastRoundNumber => '轮数'; + + @override + String get broadcastTournamentName => '锦标赛名称'; + + @override + String get broadcastTournamentDescription => '锦标赛简短描述'; + + @override + String get broadcastFullDescription => '赛事详情'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '转播内容的详细描述 (可选)。可以使用 $param1,字数少于 $param2 个。'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN的URL源'; + + @override + String get broadcastSourceUrlHelp => 'Lichess 将从该网址搜查 PGN 的更新。它必须是公开的。'; + + @override + String get broadcastSourceGameIds => '多达64个 Lichess 棋局Id,用空格隔开。'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => '如果你知道比赛开始时间 (可选)'; + + @override + String get broadcastCurrentGameUrl => '当前棋局链接'; + + @override + String get broadcastDownloadAllRounds => '下载所有棋局'; + + @override + String get broadcastResetRound => '重置此轮'; + + @override + String get broadcastDeleteRound => '删除此轮'; + + @override + String get broadcastDefinitivelyDeleteRound => '确定删除该回合及其游戏。'; + + @override + String get broadcastDeleteAllGamesOfThisRound => '删除此回合的所有游戏。源需要激活才能重新创建。'; + + @override + String get broadcastEditRoundStudy => '编辑该轮次的棋局研究'; + + @override + String get broadcastDeleteTournament => '删除该锦标赛'; + + @override + String get broadcastDefinitivelyDeleteTournament => '确定删除整个锦标赛、所有轮次和其中所有比赛。'; + + @override + String get broadcastShowScores => '根据比赛结果显示棋手分数'; + + @override + String get broadcastReplacePlayerTags => '可选项:替换选手的名字、等级分和头衔'; + + @override + String get broadcastFideFederations => 'FIDE 成员国'; + + @override + String get broadcastTop10Rating => '前10名等级分'; + + @override + String get broadcastFidePlayers => 'FIDE 棋手'; + + @override + String get broadcastFidePlayerNotFound => '未找到 FIDE 棋手'; + + @override + String get broadcastFideProfile => 'FIDE个人资料'; + + @override + String get broadcastFederation => '棋联'; + + @override + String get broadcastAgeThisYear => '今年的年龄'; + + @override + String get broadcastUnrated => '未评级'; + + @override + String get broadcastRecentTournaments => '最近的比赛'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => '结束的转播'; + + @override + String get broadcastAllBroadcastsByMonth => '按月查看所有转播'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 直播', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '挑战: $param1'; @@ -425,7 +679,7 @@ class AppLocalizationsZh extends AppLocalizations { String get patronDonate => '捐赠'; @override - String get patronLichessPatron => '赞助 Lichess'; + String get patronLichessPatron => 'Lichess赞助者账号'; @override String perfStatPerfStats(String param) { @@ -536,7 +790,7 @@ class AppLocalizationsZh extends AppLocalizations { String get preferencesPreferences => '偏好设置'; @override - String get preferencesDisplay => '显示'; + String get preferencesDisplay => '界面设置'; @override String get preferencesPrivacy => '隐私设置'; @@ -592,6 +846,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get preferencesInGameOnly => '仅在对局中'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '棋钟'; @@ -733,6 +990,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get preferencesBellNotificationSound => '通知铃声'; + @override + String get preferencesBlindfold => '盲棋'; + @override String get puzzlePuzzles => '谜题'; @@ -1368,10 +1628,10 @@ class AppLocalizationsZh extends AppLocalizations { String get puzzleThemeZugzwangDescription => '对手可选的着法是有限的,并且所有着法都会使其局面更加恶化。'; @override - String get puzzleThemeHealthyMix => '健康搭配'; + String get puzzleThemeMix => '健康搭配'; @override - String get puzzleThemeHealthyMixDescription => '每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。'; + String get puzzleThemeMixDescription => '每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。'; @override String get puzzleThemePlayerGames => '玩家对局'; @@ -1444,7 +1704,7 @@ class AppLocalizationsZh extends AppLocalizations { String get level => '级别'; @override - String get strength => '电脑的难度'; + String get strength => '强度'; @override String get toggleTheChat => '聊天开关'; @@ -1480,10 +1740,10 @@ class AppLocalizationsZh extends AppLocalizations { String get createAGame => '创建对局'; @override - String get whiteIsVictorious => '白方胜'; + String get whiteIsVictorious => '白方胜利'; @override - String get blackIsVictorious => '黑方胜'; + String get blackIsVictorious => '黑方胜利'; @override String get youPlayTheWhitePieces => '你执白棋'; @@ -1492,7 +1752,7 @@ class AppLocalizationsZh extends AppLocalizations { String get youPlayTheBlackPieces => '你执黑棋'; @override - String get itsYourTurn => '轮到你了!'; + String get itsYourTurn => '你的回合!'; @override String get cheatDetected => '检测到作弊'; @@ -1501,10 +1761,10 @@ class AppLocalizationsZh extends AppLocalizations { String get kingInTheCenter => '王占中'; @override - String get threeChecks => '三次将军'; + String get threeChecks => '三次将军胜'; @override - String get raceFinished => '比赛结束'; + String get raceFinished => '竞王结束'; @override String get variantEnding => '变种结束'; @@ -1525,16 +1785,16 @@ class AppLocalizationsZh extends AppLocalizations { String get blackPlays => '黑方走棋'; @override - String get opponentLeftChoices => '您的对手可能已离开棋局。您可以宣布胜利,和棋,或继续等待。'; + String get opponentLeftChoices => '你的对手已离开棋局。你可以宣布胜利、和棋或继续等待。'; @override String get forceResignation => '宣布胜利'; @override - String get forceDraw => '和棋'; + String get forceDraw => '宣布和棋'; @override - String get talkInChat => '聊天请注意文明用语。'; + String get talkInChat => '聊天请注意文明用语!'; @override String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一个访问此网址的人将与你下棋。'; @@ -1599,7 +1859,7 @@ class AppLocalizationsZh extends AppLocalizations { String get showThreat => '显示威胁'; @override - String get inLocalBrowser => '在本地浏览器'; + String get inLocalBrowser => '本地浏览器'; @override String get toggleLocalEvaluation => '切换到本地分析'; @@ -1629,7 +1889,7 @@ class AppLocalizationsZh extends AppLocalizations { String get move => '着法'; @override - String get variantLoss => '变体输了'; + String get variantLoss => '变体输棋'; @override String get variantWin => '变体胜利'; @@ -1647,7 +1907,7 @@ class AppLocalizationsZh extends AppLocalizations { String get close => '关闭'; @override - String get winning => '赢棋'; + String get winning => '胜棋'; @override String get losing => '输棋'; @@ -1656,7 +1916,7 @@ class AppLocalizationsZh extends AppLocalizations { String get drawn => '和棋'; @override - String get unknown => '结局未知'; + String get unknown => '未知'; @override String get database => '数据库'; @@ -1670,24 +1930,24 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get recentGames => '最近对局'; + String get recentGames => '最近棋局'; @override String get topGames => '名局'; @override String masterDbExplanation(String param1, String param2, String param3) { - return '$param2-$param3年国际棋联等级分$param1以上棋手的两百万局棋谱'; + return '$param2-$param3年国际棋联等级分$param1以上棋手的棋谱'; } @override String get dtzWithRounding => '经过四舍五入的DTZ50\'\',是基于到下次吃子或兵动的半步数目。'; @override - String get noGameFound => '没找到符合要求的棋局'; + String get noGameFound => '未找到棋局'; @override - String get maxDepthReached => '已达最大深度!'; + String get maxDepthReached => '已达到最大深度!'; @override String get maybeIncludeMoreGamesFromThePreferencesMenu => '请尝试在“选择”菜单内包括更多棋局。'; @@ -1745,9 +2005,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get byCPL => '按厘兵损失'; - @override - String get openStudy => '进入研讨室'; - @override String get enable => '启用'; @@ -1775,9 +2032,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get removesTheDepthLimit => '取消深度限制(会提升电脑温度)'; - @override - String get engineManager => '引擎管理'; - @override String get blunder => '漏着'; @@ -2041,6 +2295,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get gamesPlayed => '棋局'; + @override + String get ok => 'OK'; + @override String get cancel => '取消'; @@ -2102,7 +2359,7 @@ class AppLocalizationsZh extends AppLocalizations { String get customPosition => '自定义位置'; @override - String get unlimited => '无限时间'; + String get unlimited => '无限制'; @override String get mode => '模式'; @@ -2415,9 +2672,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get unblock => '移出黑名单'; - @override - String get followsYou => '关注了你'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1开始关注$param2'; @@ -2693,7 +2947,7 @@ class AppLocalizationsZh extends AppLocalizations { String get website => '网站'; @override - String get mobile => '流动电话'; + String get mobile => '移动端'; @override String get help => '帮助:'; @@ -2750,7 +3004,13 @@ class AppLocalizationsZh extends AppLocalizations { String get other => '其他'; @override - String get reportDescriptionHelp => '请附上棋局链接解释该用户的行为问题。例如如果你怀疑某用户作弊,请不要只说 “对手作弊”。请解释为什么你认为对手作弊。如果你用英语举报,我们将会更快作出答复。'; + String get reportCheatBoostHelp => '请附上棋局链接解释该用户的行为问题。请不要只说 “对手作弊”,而是解释为什么你认为对手作弊。'; + + @override + String get reportUsernameHelp => '解释这个用户名为何具有冒犯性。不要只说“它具有冒犯性/不恰当”,而是要告诉我们你是如何得出这个结论的,特别是如果侮辱性内容是隐晦的、非英语的、俚语或有历史/文化参考。'; + + @override + String get reportProcessedFasterInEnglish => '如果您使用英语举报,我们将会更快作出答复。'; @override String get error_provideOneCheatedGameLink => '请提供至少一局作弊的棋局的链接。'; @@ -4055,6 +4315,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get nothingToSeeHere => '此刻没有什么可看的。'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4658,4347 +4921,6243 @@ class AppLocalizationsZh extends AppLocalizations { String get streamerLichessStreamers => 'Lichess 主播'; @override - String get studyShareAndExport => '分享并导出'; + String get studyPrivate => '私人'; @override - String get studyStart => '开始'; -} + String get studyMyStudies => '我的研讨'; -/// The translations for Chinese, as used in Taiwan (`zh_TW`). -class AppLocalizationsZhTw extends AppLocalizationsZh { - AppLocalizationsZhTw(): super('zh_TW'); + @override + String get studyStudiesIContributeTo => '我贡献的研讨'; @override - String get mobileHomeTab => '主頁'; + String get studyMyPublicStudies => '我的公开研讨'; @override - String get mobilePuzzlesTab => '謎題'; + String get studyMyPrivateStudies => '我的私有研讨'; @override - String get mobileToolsTab => '工具'; + String get studyMyFavoriteStudies => '我收藏的研讨'; @override - String get mobileWatchTab => '觀看'; + String get studyWhatAreStudies => '什么是研讨?'; @override - String get mobileSettingsTab => '設置'; + String get studyAllStudies => '所有研讨'; @override - String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; + String studyStudiesCreatedByX(String param) { + return '由 $param 创建的研讨'; + } @override - String get mobileSystemColors => '系统颜色'; + String get studyNoneYet => '暂无。'; @override - String get mobileFeedbackButton => '問題反饋'; + String get studyHot => '热门'; @override - String get mobileOkButton => '確認'; + String get studyDateAddedNewest => '添加时间 (最新)'; @override - String get mobileSettingsHapticFeedback => '震動回饋'; + String get studyDateAddedOldest => '添加时间 (最早)'; @override - String get mobileSettingsImmersiveMode => '沉浸模式'; + String get studyRecentlyUpdated => '最近更新'; @override - String get mobileAllGames => '所有遊戲'; + String get studyMostPopular => '最受欢迎'; @override - String get mobileRecentSearches => '最近搜尋'; + String get studyAlphabetical => '按字母顺序'; @override - String get mobileClearButton => '清除'; + String get studyAddNewChapter => '添加一个新章节'; @override - String get mobileNoSearchResults => '無結果'; + String get studyAddMembers => '添加成员'; @override - String get mobileAreYouSure => '您確定嗎?'; + String get studyInviteToTheStudy => '邀请参加研讨'; @override - String get mobileShareGamePGN => '分享 PGN'; + String get studyPleaseOnlyInvitePeopleYouKnow => '请仅邀请你认识的并且积极希望参与这个研讨的成员'; @override - String get mobileCustomGameJoinAGame => '加入遊戲'; + String get studySearchByUsername => '按用户名搜索'; @override - String get activityActivity => '活動'; + String get studySpectator => '旁观者'; @override - String get activityHostedALiveStream => '主持一個現場直播'; + String get studyContributor => '贡献者'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return '在$param2中排名$param1'; - } + String get studyKick => '踢出'; @override - String get activitySignedUp => '在lichess.org中註冊'; + String get studyLeaveTheStudy => '离开研讨'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '以$param2的身分支持lichess.org$count個月', - ); - return '$_temp0'; - } + String get studyYouAreNowAContributor => '你现在是一位贡献者'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param2練習了$count個棋局', - ); - return '$_temp0'; - } + String get studyYouAreNowASpectator => '你现在是一位旁观者'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '解決了$count個戰術題目', - ); - return '$_temp0'; - } + String get studyPgnTags => 'PGN 标签'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '下了$count場$param2類型的棋局', - ); - return '$_temp0'; - } + String get studyLike => '赞'; @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param2發表了$count則訊息', - ); - return '$_temp0'; - } + String get studyUnlike => '取消赞'; @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '下了$count步', - ); - return '$_temp0'; - } + String get studyNewTag => '新建标签'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$count場長時間棋局中', - ); - return '$_temp0'; - } + String get studyCommentThisPosition => '评论当前局面'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '完成了$count場長時間棋局', - ); - return '$_temp0'; - } + String get studyCommentThisMove => '评论这步走法'; @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '開始關注$count個玩家', - ); - return '$_temp0'; - } + String get studyAnnotateWithGlyphs => '用符号标注'; @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '增加了$count個追蹤者', - ); - return '$_temp0'; - } + String get studyTheChapterIsTooShortToBeAnalysed => '本章节太短,无法进行分析。'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '主持了$count場車輪戰', - ); - return '$_temp0'; - } + String get studyOnlyContributorsCanRequestAnalysis => '只有贡献者可以请求服务器分析。'; @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '加入了$count場車輪戰', - ); - return '$_temp0'; - } + String get studyGetAFullComputerAnalysis => '请求服务器完整地分析主线走法。'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '創造了$count個新的研究', - ); - return '$_temp0'; - } + String get studyMakeSureTheChapterIsComplete => '请确保章节已完成。你只能请求分析一次。'; @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '完成了$count場錦標賽', - ); - return '$_temp0'; - } + String get studyAllSyncMembersRemainOnTheSamePosition => 'SYNC 中所有成员处于相同局面'; @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param4錦標賽中下了$param3盤棋局,排名第$count(前$param2%)', - ); - return '$_temp0'; - } + String get studyShareChanges => '与旁观者共享更改并云端保存'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '參與過$count\'場瑞士制錦標賽', - ); - return '$_temp0'; - } + String get studyPlaying => '正在对局'; @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '加入$count團隊', - ); - return '$_temp0'; - } + String get studyShowEvalBar => '评估条'; @override - String get broadcastBroadcasts => '比賽直播'; + String get studyFirst => '首页'; @override - String get broadcastLiveBroadcasts => '錦標賽直播'; + String get studyPrevious => '上一页'; @override - String challengeChallengesX(String param1) { - return '挑戰: $param1'; - } + String get studyNext => '下一页'; @override - String get challengeChallengeToPlay => '邀請對弈'; + String get studyLast => '末页'; @override - String get challengeChallengeDeclined => '對弈邀請已拒絕'; + String get studyShareAndExport => '分享并导出'; @override - String get challengeChallengeAccepted => '對弈邀請已接受'; + String get studyCloneStudy => '复制棋局'; @override - String get challengeChallengeCanceled => '對弈邀請已撤銷'; + String get studyStudyPgn => '研究 PGN'; @override - String get challengeRegisterToSendChallenges => '請登入以向其他人發出對弈邀請'; + String get studyDownloadAllGames => '下载所有棋局'; @override - String challengeYouCannotChallengeX(String param) { - return '你無法向$param發出對弈邀請'; - } + String get studyChapterPgn => '章节PGN'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param沒有接受對弈邀請'; - } + String get studyCopyChapterPgn => '复制PGN'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return '您的$param1積分與$param2相差太多'; - } + String get studyDownloadGame => '下载棋局'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return '由於您的$param積分不夠穩定,無法發出挑戰。'; - } + String get studyStudyUrl => '研究链接'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param只接受好友的對弈邀請'; - } + String get studyCurrentChapterUrl => '当前章节链接'; @override - String get challengeDeclineGeneric => '我目前不接受對弈'; + String get studyYouCanPasteThisInTheForumToEmbed => '你可以将此粘贴到论坛以嵌入章节'; @override - String get challengeDeclineLater => '我現在不接受對弈,請晚點再詢問'; + String get studyStartAtInitialPosition => '从初始局面开始'; @override - String get challengeDeclineTooFast => '這個時間控制對我來說太快了,請用慢一點的遊戲再次挑戰。'; + String studyStartAtX(String param) { + return '从 $param 开始'; + } @override - String get challengeDeclineTooSlow => '這個時間控制對我來說太慢了,請用快一點的遊戲再次挑戰。'; + String get studyEmbedInYourWebsite => '嵌入到你的网站上'; @override - String get challengeDeclineTimeControl => '我不接受這個挑戰的時間控制。'; + String get studyReadMoreAboutEmbedding => '阅读更多关于嵌入的信息'; @override - String get challengeDeclineRated => '請向我發送積分對弈。'; + String get studyOnlyPublicStudiesCanBeEmbedded => '只能嵌入隐私设置为公开的研究!'; @override - String get challengeDeclineCasual => '請向我發送休閒對弈。'; + String get studyOpen => '打开'; @override - String get challengeDeclineStandard => '我不接受變體對弈。'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 由 $param2 提供'; + } @override - String get challengeDeclineVariant => '我現在不想玩這個變體。'; + String get studyStudyNotFound => '找不到研究'; @override - String get challengeDeclineNoBot => '我不接受機器人的對弈。'; + String get studyEditChapter => '编辑章节'; @override - String get challengeDeclineOnlyBot => '我目前只接受機器人的對弈。'; + String get studyNewChapter => '新章节'; @override - String get challengeInviteLichessUser => '或邀請一位 Lichess 用户:'; + String studyImportFromChapterX(String param) { + return '从 $param 导入'; + } @override - String get contactContact => '聯繫我們'; + String get studyOrientation => '视角'; @override - String get contactContactLichess => '聯繫 Lichess'; + String get studyAnalysisMode => '分析模式'; @override - String get patronDonate => '捐款'; + String get studyPinnedChapterComment => '置顶评论'; @override - String get patronLichessPatron => 'Lichess 贊助者'; + String get studySaveChapter => '保存章节'; @override - String perfStatPerfStats(String param) { - return '$param戰績'; - } + String get studyClearAnnotations => '清除注释'; @override - String get perfStatViewTheGames => '查看遊戲紀錄'; + String get studyClearVariations => '清除变着'; @override - String get perfStatProvisional => '臨時'; + String get studyDeleteChapter => '删除章节'; @override - String get perfStatNotEnoughRatedGames => '積分賽場次太少,無法計算準確積分。'; + String get studyDeleteThisChapter => '删除本章节?本操作无法撤销!'; @override - String perfStatProgressOverLastXGames(String param) { - return '最近$param場棋局之積分變化:'; - } + String get studyClearAllCommentsInThisChapter => '清除章节中所有信息?'; @override - String perfStatRatingDeviation(String param) { - return '積分誤差: $param'; - } + String get studyRightUnderTheBoard => '正下方'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return '越低的數值代表積分越穩定。 數值高於$param1時的積分會被判定為浮動積分。\n要被列入排名之中,該數值需低於$param2(標準西洋棋) 或是$param3(西洋棋變體)。'; - } + String get studyNoPinnedComment => '不需要'; @override - String get perfStatTotalGames => '總計棋局'; + String get studyNormalAnalysis => '普通模式'; @override - String get perfStatRatedGames => '積分棋局'; + String get studyHideNextMoves => '隐藏下一步'; @override - String get perfStatTournamentGames => '聯賽棋局'; + String get studyInteractiveLesson => '互动课'; @override - String get perfStatBerserkedGames => '狂暴模式棋局'; + String studyChapterX(String param) { + return '章节 $param'; + } @override - String get perfStatTimeSpentPlaying => '奕棋時間'; + String get studyEmpty => '空白'; @override - String get perfStatAverageOpponent => '對手平均積分'; + String get studyStartFromInitialPosition => '从初始局面开始'; @override - String get perfStatVictories => '勝場'; + String get studyEditor => '编辑器'; @override - String get perfStatDefeats => '敗場'; + String get studyStartFromCustomPosition => '从自定义局面开始'; @override - String get perfStatDisconnections => '斷線場次'; + String get studyLoadAGameByUrl => '通过 URL 加载游戏'; @override - String get perfStatNotEnoughGames => '棋局數不夠多'; + String get studyLoadAPositionFromFen => '从 FEN 加载一个局面'; @override - String perfStatHighestRating(String param) { - return '最高積分:$param'; - } + String get studyLoadAGameFromPgn => '从 PGN 文件加载游戏'; @override - String perfStatLowestRating(String param) { - return '最低積分:$param'; - } + String get studyAutomatic => '自动'; @override - String perfStatFromXToY(String param1, String param2) { - return '從$param1到$param2'; - } + String get studyUrlOfTheGame => '游戏的 URL'; @override - String get perfStatWinningStreak => '連勝場數'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return '从 $param1 或 $param2 加载游戏'; + } @override - String get perfStatLosingStreak => '連敗場數'; + String get studyCreateChapter => '创建章节'; @override - String perfStatLongestStreak(String param) { - return '最長紀錄:$param'; - } + String get studyCreateStudy => '创建课程'; @override - String perfStatCurrentStreak(String param) { - return '目前記錄:$param'; - } + String get studyEditStudy => '编辑课程'; @override - String get perfStatBestRated => '積分賽勝場之最強對手'; + String get studyVisibility => '权限'; @override - String get perfStatGamesInARow => '連續奕棋場數'; + String get studyPublic => '公开'; @override - String get perfStatLessThanOneHour => '兩場間距不到一小時'; + String get studyUnlisted => '未列出'; @override - String get perfStatMaxTimePlaying => '最高奕棋時間'; + String get studyInviteOnly => '仅限邀请'; @override - String get perfStatNow => '現在'; + String get studyAllowCloning => '允许复制'; @override - String get preferencesPreferences => '偏好設置'; + String get studyNobody => '没人'; @override - String get preferencesDisplay => '顯示'; + String get studyOnlyMe => '仅自己'; @override - String get preferencesPrivacy => '隱私'; + String get studyContributors => '贡献者'; @override - String get preferencesNotifications => '通知'; + String get studyMembers => '成员'; @override - String get preferencesPieceAnimation => '棋子動畫'; + String get studyEveryone => '所有人'; @override - String get preferencesMaterialDifference => '子力差距'; + String get studyEnableSync => '允许同步'; @override - String get preferencesBoardHighlights => '棋盤高亮 (最後一步與將軍)'; + String get studyYesKeepEveryoneOnTheSamePosition => '确认:每个人都处于同样的局面'; @override - String get preferencesPieceDestinations => '棋子目的地(有效走法與預先走棋)'; + String get studyNoLetPeopleBrowseFreely => '取消:让玩家自由选择'; @override - String get preferencesBoardCoordinates => '棋盤座標(A-H, 1-8)'; + String get studyPinnedStudyComment => '置顶评论'; @override - String get preferencesMoveListWhilePlaying => '遊戲進行時顯示棋譜'; + String get studyStart => '开始'; @override - String get preferencesPgnPieceNotation => '棋譜記法'; + String get studySave => '保存'; @override - String get preferencesChessPieceSymbol => '棋子符號'; + String get studyClearChat => '清空对话'; @override - String get preferencesPgnLetter => '字母 (K, Q, R, B, N)'; + String get studyDeleteTheStudyChatHistory => '删除课程聊天记录?本操作无法撤销!'; @override - String get preferencesZenMode => '專注模式'; + String get studyDeleteStudy => '删除课程'; @override - String get preferencesShowPlayerRatings => '顯示玩家等級分'; - - @override - String get preferencesExplainShowPlayerRatings => '這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。'; + String studyConfirmDeleteStudy(String param) { + return '确定删除整个研讨?该操作不可恢复,输入研讨名以确认:$param'; + } @override - String get preferencesDisplayBoardResizeHandle => '顯示盤面大小調整區塊'; + String get studyWhereDoYouWantToStudyThat => '你想从哪里开始此项研究?'; @override - String get preferencesOnlyOnInitialPosition => '只在起始局面'; + String get studyGoodMove => '好棋'; @override - String get preferencesInGameOnly => '只在遊戲中'; + String get studyMistake => '错着'; @override - String get preferencesChessClock => '棋鐘'; + String get studyBrilliantMove => '极好'; @override - String get preferencesTenthsOfSeconds => '十分之一秒'; + String get studyBlunder => '漏着'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => '當剩餘時間小於10秒'; + String get studyInterestingMove => '略好'; @override - String get preferencesHorizontalGreenProgressBars => '綠色橫進度條'; + String get studyDubiousMove => '略坏'; @override - String get preferencesSoundWhenTimeGetsCritical => '時間不足時聲音提醒'; + String get studyOnlyMove => '唯一着法'; @override - String get preferencesGiveMoreTime => '給對方更多時間'; + String get studyZugzwang => 'Zugzwang'; @override - String get preferencesGameBehavior => '對局行為'; + String get studyEqualPosition => '均势'; @override - String get preferencesHowDoYouMovePieces => '移動棋子方式?'; + String get studyUnclearPosition => '局势不明'; @override - String get preferencesClickTwoSquares => '點擊棋子及目標位置'; + String get studyWhiteIsSlightlyBetter => '白方略优'; @override - String get preferencesDragPiece => '拖曳棋子'; + String get studyBlackIsSlightlyBetter => '黑方略优'; @override - String get preferencesBothClicksAndDrag => '兩者都行'; + String get studyWhiteIsBetter => '白方占优'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => '預先走棋(在對手的回合走棋)'; + String get studyBlackIsBetter => '黑方占优'; @override - String get preferencesTakebacksWithOpponentApproval => '悔棋(經過對手同意)'; + String get studyWhiteIsWinning => '白方即胜'; @override - String get preferencesInCasualGamesOnly => '僅限非正式遊戲'; + String get studyBlackIsWinning => '黑方即胜'; @override - String get preferencesPromoteToQueenAutomatically => '兵自動升為后'; + String get studyNovelty => '新奇的'; @override - String get preferencesExplainPromoteToQueenAutomatically => '升變的同時按住以暫時取消自動升變'; + String get studyDevelopment => '发展'; @override - String get preferencesWhenPremoving => '預先走棋時'; + String get studyInitiative => '占据主动'; @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '在三次重覆局面時自動要求和局'; + String get studyAttack => '攻击'; @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => '當剩餘時間小於30秒'; + String get studyCounterplay => '反击'; @override - String get preferencesMoveConfirmation => '走棋確認'; + String get studyTimeTrouble => '无暇多虑'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => '可以在遊戲中用棋盤選單中關閉此功能'; + String get studyWithCompensation => '优势补偿'; @override - String get preferencesInCorrespondenceGames => '在長期對局中'; + String get studyWithTheIdea => '教科书式的'; @override - String get preferencesCorrespondenceAndUnlimited => '通信和無限'; + String get studyNextChapter => '下一章节'; @override - String get preferencesConfirmResignationAndDrawOffers => '確認投降或和局請求'; + String get studyPrevChapter => '上一章节'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '入堡方法'; + String get studyStudyActions => '研讨操作'; @override - String get preferencesCastleByMovingTwoSquares => '移動國王兩格'; + String get studyTopics => '主题'; @override - String get preferencesCastleByMovingOntoTheRook => '移動國王到城堡上'; + String get studyMyTopics => '我的主题'; @override - String get preferencesInputMovesWithTheKeyboard => '使用鍵盤輸入著法'; + String get studyPopularTopics => '热门主题'; @override - String get preferencesInputMovesWithVoice => '用語音輸入著法'; + String get studyManageTopics => '管理主题'; @override - String get preferencesSnapArrowsToValidMoves => '將右鍵標示箭頭鎖定到合法棋步'; + String get studyBack => '回到起始'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => '輸棋或和棋後自動發送 \"Good game, well played\"。'; + String get studyPlayAgain => '重玩'; @override - String get preferencesYourPreferencesHaveBeenSaved => '已儲存您的設定。'; + String get studyWhatWouldYouPlay => '你会在这个位置上怎么走?'; @override - String get preferencesScrollOnTheBoardToReplayMoves => '在騎盤上使用滑鼠滾輪以重新顯示過去棋步'; + String get studyYouCompletedThisLesson => '恭喜!你完成了这个课程!'; @override - String get preferencesCorrespondenceEmailNotification => '每日以電郵列出您當前的長期對局'; + String studyPerPage(String param) { + return '$param per page'; + } @override - String get preferencesNotifyStreamStart => '追蹤的直播主開始直播'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '共 $count 章', + ); + return '$_temp0'; + } @override - String get preferencesNotifyInboxMsg => '收件夾有新訊息'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '共 $count 盘棋', + ); + return '$_temp0'; + } @override - String get preferencesNotifyForumMention => '論壇評論中提到您'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 位成员', + ); + return '$_temp0'; + } @override - String get preferencesNotifyInvitedStudy => '研究邀請'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在此粘贴你的 PGN 文本,最多支持 $count 个游戏', + ); + return '$_temp0'; + } @override - String get preferencesNotifyGameEvent => '長期對局更新訊息'; + String get timeagoJustNow => '刚刚'; @override - String get preferencesNotifyChallenge => '挑戰'; + String get timeagoRightNow => '刚刚'; @override - String get preferencesNotifyTournamentSoon => '比賽即將開始'; + String get timeagoCompleted => '已完成'; @override - String get preferencesNotifyTimeAlarm => '長期對局的時間即將耗盡'; + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 秒内', + ); + return '$_temp0'; + } @override - String get preferencesNotifyBell => 'Lichess 內的鈴聲通知'; + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 分钟内', + ); + return '$_temp0'; + } @override - String get preferencesNotifyPush => 'Lichess 外的設備通知'; + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 小时内', + ); + return '$_temp0'; + } @override - String get preferencesNotifyWeb => '瀏覽器通知'; + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 天内', + ); + return '$_temp0'; + } @override - String get preferencesNotifyDevice => '設備通知'; + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 周内', + ); + return '$_temp0'; + } @override - String get preferencesBellNotificationSound => '通知鈴聲'; + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 月内', + ); + return '$_temp0'; + } @override - String get puzzlePuzzles => '謎題'; + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 年内', + ); + return '$_temp0'; + } @override - String get puzzlePuzzleThemes => '謎題主題'; + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分钟前', + ); + return '$_temp0'; + } @override - String get puzzleRecommended => '推薦'; + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 小时前', + ); + return '$_temp0'; + } @override - String get puzzlePhases => '分類'; + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 天前', + ); + return '$_temp0'; + } @override - String get puzzleMotifs => '主題'; + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 周前', + ); + return '$_temp0'; + } @override - String get puzzleAdvanced => '高級'; + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 月前', + ); + return '$_temp0'; + } @override - String get puzzleLengths => '長度'; + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年前', + ); + return '$_temp0'; + } @override - String get puzzleMates => '將軍'; + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '还剩 $count 分钟', + ); + return '$_temp0'; + } @override - String get puzzleGoals => '目標'; + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '还剩 $count 小时', + ); + return '$_temp0'; + } +} - @override - String get puzzleOrigin => '來源'; +/// The translations for Chinese, as used in Taiwan (`zh_TW`). +class AppLocalizationsZhTw extends AppLocalizationsZh { + AppLocalizationsZhTw(): super('zh_TW'); @override - String get puzzleSpecialMoves => '特殊移動'; + String get mobileAllGames => '所有棋局'; @override - String get puzzleDidYouLikeThisPuzzle => '您喜歡這道謎題嗎?'; + String get mobileAreYouSure => '您確定嗎?'; @override - String get puzzleVoteToLoadNextOne => '告訴我們加載下一題!'; + String get mobileCancelTakebackOffer => '取消悔棋請求'; @override - String get puzzleYourPuzzleRatingWillNotChange => '您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。'; + String get mobileClearButton => '清除'; @override - String get puzzleFindTheBestMoveForWhite => '為白方找出最佳移動'; + String get mobileCorrespondenceClearSavedMove => '清除已儲存移動'; @override - String get puzzleFindTheBestMoveForBlack => '為黑方找出最佳移動'; + String get mobileCustomGameJoinAGame => '加入棋局'; @override - String get puzzleToGetPersonalizedPuzzles => '得到個人推薦題目:'; + String get mobileFeedbackButton => '問題反饋'; @override - String puzzlePuzzleId(String param) { - return '謎題 $param'; + String mobileGreeting(String param) { + return '您好, $param'; } @override - String get puzzlePuzzleOfTheDay => '每日一題'; + String get mobileGreetingWithoutName => '您好'; @override - String get puzzleClickToSolve => '點擊解題'; + String get mobileHideVariation => '隱藏變體'; @override - String get puzzleGoodMove => '好棋'; + String get mobileHomeTab => '首頁'; @override - String get puzzleBestMove => '最佳走法!'; + String get mobileLiveStreamers => 'Lichess 實況主'; @override - String get puzzleKeepGoing => '加油!'; + String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; @override - String get puzzlePuzzleSuccess => '成功!'; + String get mobileNoSearchResults => '沒有任何搜尋結果'; @override - String get puzzlePuzzleComplete => '解題完成!'; + String get mobileNotFollowingAnyUser => '您未被任何使用者追蹤。'; @override - String get puzzleNotTheMove => '不是這步!'; + String get mobileOkButton => '確認'; @override - String get puzzleTrySomethingElse => '試試其他的移動'; + String mobilePlayersMatchingSearchTerm(String param) { + return '名稱包含「$param」的玩家'; + } @override - String puzzleRatingX(String param) { - return '評級:$param'; - } + String get mobilePrefMagnifyDraggedPiece => '放大被拖曳的棋子'; @override - String get puzzleHidden => '隱藏'; + String get mobilePuzzleStormConfirmEndRun => '是否中斷於此?'; @override - String puzzleFromGameLink(String param) { - return '來自對局 $param'; - } + String get mobilePuzzleStormFilterNothingToShow => '沒有內容可顯示,請更改篩選條件'; @override - String get puzzleContinueTraining => '繼續訓練'; + String get mobilePuzzleStormNothingToShow => '沒有內容可顯示。您可以進行一些 Puzzle Storm 。'; @override - String get puzzleDifficultyLevel => '困難度'; + String get mobilePuzzleStormSubtitle => '在三分鐘內解開盡可能多的謎題'; @override - String get puzzleNormal => '一般'; + String get mobilePuzzleStreakAbortWarning => '這將失去目前的連勝並且將儲存目前成績。'; @override - String get puzzleEasier => '簡單'; + String get mobilePuzzleThemesSubtitle => '從您喜歡的開局進行謎題,或選擇一個主題。'; @override - String get puzzleEasiest => '超簡單'; + String get mobilePuzzlesTab => '謎題'; @override - String get puzzleHarder => '困難'; + String get mobileRecentSearches => '搜尋紀錄'; @override - String get puzzleHardest => '超困難'; + String get mobileSettingsHapticFeedback => '震動回饋'; @override - String get puzzleExample => '範例'; + String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get puzzleAddAnotherTheme => '加入其他主題'; + String get mobileSettingsImmersiveModeSubtitle => '在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。'; @override - String get puzzleJumpToNextPuzzleImmediately => '立即跳到下一個謎題'; + String get mobileSettingsTab => '設定'; @override - String get puzzlePuzzleDashboard => '謎題能力分析'; + String get mobileShareGamePGN => '分享 PGN'; @override - String get puzzleImprovementAreas => '弱點'; + String get mobileShareGameURL => '分享對局網址'; @override - String get puzzleStrengths => '強項'; + String get mobileSharePositionAsFEN => '以 FEN 分享棋局位置'; @override - String get puzzleHistory => '解題紀錄'; + String get mobileSharePuzzle => '分享這個謎題'; @override - String get puzzleSolved => '解決'; + String get mobileShowComments => '顯示留言'; @override - String get puzzleFailed => '失敗'; + String get mobileShowResult => '顯示結果'; @override - String get puzzleStreakDescription => '累積你的連勝,解著漸漸變難的題目。 沒有時間限制,不要急。走錯一步,將會是遊戲結束!\n不過每一局中你都有跳過一步棋的機會。'; + String get mobileShowVariations => '顯示變體'; @override - String puzzleYourStreakX(String param) { - return '您的連勝場數:$param'; - } + String get mobileSomethingWentWrong => '發生了一些問題。'; @override - String get puzzleStreakSkipExplanation => '跳過這一步來維持您的連勝紀錄!每次遊玩只能使用一次。'; + String get mobileSystemColors => '系統顏色'; @override - String get puzzleContinueTheStreak => '繼續遊玩'; + String get mobileToolsTab => '工具'; @override - String get puzzleNewStreak => '新的連勝紀錄'; + String get mobileWaitingForOpponentToJoin => '正在等待對手加入...'; @override - String get puzzleFromMyGames => '來自我的棋局'; + String get mobileWatchTab => '觀戰'; @override - String get puzzleLookupOfPlayer => '尋找其他棋手的棋局謎題'; + String get activityActivity => '活動'; @override - String puzzleFromXGames(String param) { - return '來自$param棋局的謎題'; - } + String get activityHostedALiveStream => '主持一個現場直播'; @override - String get puzzleSearchPuzzles => '尋找謎題'; + String activityRankedInSwissTournament(String param1, String param2) { + return '在$param2中排名 $param1'; + } @override - String get puzzleFromMyGamesNone => '你在數據庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加添加拼圖的機會!'; + String get activitySignedUp => '在 lichess.org 中註冊'; @override - String puzzleFromXGamesFound(String param1, String param2) { - return '在$param2中找到$param1個謎題'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '以 $param2 身分贊助 lichess.org $count 個月', + ); + return '$_temp0'; } @override - String get puzzlePuzzleDashboardDescription => '訓練、分析、改進'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $param2 練習了 $count 個棋局', + ); + return '$_temp0'; + } @override - String puzzlePercentSolved(String param) { - return '$param 已解決'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '解決了 $count 個戰術題目', + ); + return '$_temp0'; } @override - String get puzzleNoPuzzlesToShow => '沒有什麼可展示的,先去玩一些謎題吧!'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '下了 $count 場$param2類型的棋局', + ); + return '$_temp0'; + } @override - String get puzzleImprovementAreasDescription => '訓練這些類型的謎題來優化你的進步!'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在「$param2」發表了 $count 則訊息', + ); + return '$_temp0'; + } @override - String get puzzleStrengthDescription => '你在這些主題中表現最好'; + String activityPlayedNbMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '下了 $count 步', + ); + return '$_temp0'; + } @override - String puzzlePlayedXTimes(int count) { + String activityInNbCorrespondenceGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '已被嘗試$count次', + other: '在 $count 場通信棋局中', ); return '$_temp0'; } @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { + String activityCompletedNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '低於你的謎題積分$count點', + other: '完成了 $count 場通信棋局', ); return '$_temp0'; } @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { + String activityCompletedNbVariantGames(int count, String param2) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '高於你的謎題積分$count點', + other: '完成了 $count $param2 場通信棋局', ); return '$_temp0'; } @override - String puzzleNbPlayed(int count) { + String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 已遊玩', + other: '開始關注 $count 個玩家', ); return '$_temp0'; } @override - String puzzleNbToReplay(int count) { + String activityGainedNbFollowers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 重玩', + other: '增加了 $count 個追蹤者', ); return '$_temp0'; } @override - String get puzzleThemeAdvancedPawn => '升變兵'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '主持了$count場車輪戰', + ); + return '$_temp0'; + } @override - String get puzzleThemeAdvancedPawnDescription => '你的其中一個兵已經深入了對方的棋位,或許要威脅升變。'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '加入了$count場車輪戰', + ); + return '$_temp0'; + } @override - String get puzzleThemeAdvantage => '擁有優勢'; + String activityCreatedNbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '創造了$count個新的研究', + ); + return '$_temp0'; + } @override - String get puzzleThemeAnastasiaMate => '阿納斯塔西亞殺法'; + String activityCompetedInNbTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '完成了$count場錦標賽', + ); + return '$_temp0'; + } @override - String get puzzleThemeArabianMate => '阿拉伯殺法'; + String activityRankedInTournament(int count, String param2, String param3, String param4) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在$param4錦標賽中下了$param3盤棋局,排名第$count(前$param2%)', + ); + return '$_temp0'; + } @override - String get puzzleThemeArabianMateDescription => '馬和車聯手把對方的王困住在角落的位置'; + String activityCompetedInNbSwissTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '參與過 $count 場瑞士制錦標賽', + ); + return '$_temp0'; + } @override - String get puzzleThemeAttackingF2F7 => '攻擊f2或f7'; + String activityJoinedNbTeams(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '加入 $count 團隊', + ); + return '$_temp0'; + } @override - String get puzzleThemeAttraction => '吸引'; + String get broadcastBroadcasts => '比賽直播'; @override - String get puzzleThemeBackRankMate => '後排將死'; + String get broadcastMyBroadcasts => '我的直播'; @override - String get puzzleThemeBackRankMateDescription => '在對方的王在底線被自身的棋子困住時,將殺對方的王'; + String get broadcastLiveBroadcasts => '錦標賽直播'; @override - String get puzzleThemeBishopEndgame => '象殘局'; + String get broadcastBroadcastCalendar => '直播時程表'; @override - String get puzzleThemeBishopEndgameDescription => '只剩象和兵的殘局'; + String get broadcastNewBroadcast => '新的現場直播'; @override - String get puzzleThemeBodenMate => '波登殺法'; + String get broadcastSubscribedBroadcasts => '已訂閱的直播'; @override - String get puzzleThemeCastling => '易位'; + String get broadcastAboutBroadcasts => '關於直播'; @override - String get puzzleThemeCapturingDefender => '吃子 - 防守者'; + String get broadcastHowToUseLichessBroadcasts => '如何使用 Lichess 比賽直播'; @override - String get puzzleThemeCrushing => '壓倒性優勢'; + String get broadcastTheNewRoundHelp => '新的一局會有跟上一局相同的成員與貢獻者'; @override - String get puzzleThemeCrushingDescription => '察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)'; + String get broadcastAddRound => '新增回合'; @override - String get puzzleThemeDoubleBishopMate => '雙主教將死'; + String get broadcastOngoing => '進行中'; @override - String get puzzleThemeEquality => '均勢'; + String get broadcastUpcoming => '即將舉行'; @override - String get puzzleThemeKingsideAttack => '王翼攻擊'; + String get broadcastCompleted => '已結束'; @override - String get puzzleThemeClearance => '騰挪'; + String get broadcastCompletedHelp => 'Lichess 偵測棋局的結束,但有可能會偵測錯誤。請在這自行設定。'; @override - String get puzzleThemeDefensiveMove => '加強防守'; + String get broadcastRoundName => '回合名稱'; @override - String get puzzleThemeDeflection => '引離'; + String get broadcastRoundNumber => '回合數'; @override - String get puzzleThemeDiscoveredAttack => '閃擊'; + String get broadcastTournamentName => '錦標賽名稱'; @override - String get puzzleThemeDoubleCheck => '雙將'; + String get broadcastTournamentDescription => '簡短比賽說明'; @override - String get puzzleThemeEndgame => '殘局'; + String get broadcastFullDescription => '完整比賽說明'; @override - String get puzzleThemeEndgameDescription => '棋局中最後階段的戰術'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return '直播內容的詳細描述 。可以利用 $param1。字數限於$param2個字。'; + } @override - String get puzzleThemeFork => '捉雙'; + String get broadcastSourceSingleUrl => 'PGN 來源網址'; @override - String get puzzleThemeKnightEndgame => '馬殘局'; + String get broadcastSourceUrlHelp => 'Lichess 將以該網址更新PGN數據,網址必須公開'; @override - String get puzzleThemeKnightEndgameDescription => '只剩馬和兵的殘局'; + String get broadcastSourceGameIds => '最多 64 個以空格分開的 Lichess 棋局序號。'; @override - String get puzzleThemeLong => '長謎題'; + String broadcastStartDateTimeZone(String param) { + return '當地時區的錦標賽起始日期:$param'; + } @override - String get puzzleThemeLongDescription => '三步獲勝'; + String get broadcastStartDateHelp => '可選,如果知道比賽開始時間'; @override - String get puzzleThemeMaster => '大師棋局'; + String get broadcastCurrentGameUrl => '目前棋局連結'; @override - String get puzzleThemeMasterVsMaster => '大師對局'; + String get broadcastDownloadAllRounds => '下載所有棋局'; @override - String get puzzleThemeMate => '將軍'; + String get broadcastResetRound => '重設此回合'; @override - String get puzzleThemeMateIn1 => '一步殺棋'; + String get broadcastDeleteRound => '刪除此回合'; @override - String get puzzleThemeMateIn1Description => '一步將軍'; + String get broadcastDefinitivelyDeleteRound => '刪除這局以及其所有棋局'; @override - String get puzzleThemeMateIn2 => '兩步殺棋'; + String get broadcastDeleteAllGamesOfThisRound => '刪除所有此輪的棋局。直播來源必須是開啟的以成功重新建立棋局。'; @override - String get puzzleThemeMateIn2Description => '走兩步以達到將軍'; + String get broadcastEditRoundStudy => '編輯此輪研究'; @override - String get puzzleThemeMateIn3 => '三步殺棋'; + String get broadcastDeleteTournament => '刪除此錦標賽'; @override - String get puzzleThemeMateIn3Description => '走三步以達到將軍'; + String get broadcastDefinitivelyDeleteTournament => '刪除錦標賽以及所有棋局'; @override - String get puzzleThemeMateIn4 => '四步殺棋'; + String get broadcastShowScores => '根據比賽結果顯示玩家分數'; @override - String get puzzleThemeMateIn4Description => '走四步以達到將軍'; + String get broadcastReplacePlayerTags => '取代玩家名字、評級、以及頭銜(選填)'; @override - String get puzzleThemeMateIn5 => '五步或更高 將軍'; + String get broadcastFideFederations => 'FIDE 國別'; @override - String get puzzleThemeMiddlegame => '中局'; + String get broadcastTop10Rating => '前 10 名平均評級'; @override - String get puzzleThemeMiddlegameDescription => '棋局中第二階段的戰術'; + String get broadcastFidePlayers => 'FIDE 玩家'; @override - String get puzzleThemeOneMove => '一步題'; + String get broadcastFidePlayerNotFound => '找不到 FIDE 玩家'; @override - String get puzzleThemeOneMoveDescription => '只有一步長的題目'; + String get broadcastFideProfile => 'FIDE 序號'; @override - String get puzzleThemeOpening => '開局'; + String get broadcastFederation => '國籍'; @override - String get puzzleThemeOpeningDescription => '棋局中起始階段的戰術'; + String get broadcastAgeThisYear => '年齡'; @override - String get puzzleThemePawnEndgame => '兵殘局'; + String get broadcastUnrated => '未評級'; @override - String get puzzleThemePawnEndgameDescription => '只剩兵的殘局'; + String get broadcastRecentTournaments => '最近錦標賽'; @override - String get puzzleThemePin => '牽制'; + String get broadcastOpenLichess => '在 lichess 中開啟'; @override - String get puzzleThemePromotion => '升變'; + String get broadcastTeams => '團隊'; @override - String get puzzleThemeQueenEndgame => '后殘局'; + String get broadcastBoards => '棋局'; @override - String get puzzleThemeQueenEndgameDescription => '只剩后和兵的殘局'; + String get broadcastOverview => '概覽'; @override - String get puzzleThemeQueenRookEndgame => '后與車'; + String get broadcastSubscribeTitle => '訂閱以在每輪開始時獲得通知。您可以在帳戶設定中切換直播的鈴聲或推播通知。'; @override - String get puzzleThemeQueenRookEndgameDescription => '只剩后、車和兵的殘局'; + String get broadcastUploadImage => '上傳錦標賽圖片'; @override - String get puzzleThemeQueensideAttack => '后翼攻擊'; + String get broadcastNoBoardsYet => '尚無棋局。這些棋局將在對局上傳後顯示。'; @override - String get puzzleThemeQuietMove => '安靜的一着'; + String broadcastBoardsCanBeLoaded(String param) { + return '棋盤能夠以輸入源投放或是利用$param'; + } @override - String get puzzleThemeRookEndgame => '車殘局'; + String broadcastStartsAfter(String param) { + return '於$param開始'; + } @override - String get puzzleThemeRookEndgameDescription => '只剩車和兵的殘局'; + String get broadcastStartVerySoon => '直播即將開始。'; @override - String get puzzleThemeSacrifice => '棄子'; + String get broadcastNotYetStarted => '直播尚未開始。'; @override - String get puzzleThemeShort => '短謎題'; + String get broadcastOfficialWebsite => '官網'; @override - String get puzzleThemeShortDescription => '兩步獲勝'; + String get broadcastStandings => '排行榜'; @override - String get puzzleThemeSkewer => '串擊'; + String broadcastIframeHelp(String param) { + return '更多選項在$param'; + } @override - String get puzzleThemeSmotheredMate => '悶殺'; + String get broadcastWebmastersPage => '網頁管理員頁面'; @override - String get puzzleThemeSuperGM => '超級大師賽局'; + String broadcastPgnSourceHelp(String param) { + return '這一輪的公開實時 PGN。我們還提供$param以實現更快和更高效的同步。'; + } @override - String get puzzleThemeSuperGMDescription => '來自世界各地優秀玩家對局的戰術題'; + String get broadcastEmbedThisBroadcast => '將此直播嵌入您的網站'; @override - String get puzzleThemeTrappedPiece => '被困的棋子'; + String broadcastEmbedThisRound(String param) { + return '將$param嵌入您的網站'; + } @override - String get puzzleThemeUnderPromotion => '升變'; + String get broadcastRatingDiff => '評級差異'; @override - String get puzzleThemeUnderPromotionDescription => '升變成騎士、象或車'; + String get broadcastGamesThisTournament => '此比賽的對局'; @override - String get puzzleThemeVeryLong => '非常長的謎題'; + String get broadcastScore => '分數'; @override - String get puzzleThemeVeryLongDescription => '四步或以上獲勝'; + String get broadcastAllTeams => '所有團隊'; @override - String get puzzleThemeXRayAttack => '穿透攻擊'; + String get broadcastTournamentFormat => '錦標賽格式'; @override - String get puzzleThemeZugzwang => '等著'; + String get broadcastTournamentLocation => '錦標賽地點'; @override - String get puzzleThemeHealthyMix => '綜合'; + String get broadcastTopPlayers => '排行榜'; @override - String get puzzleThemeHealthyMixDescription => '所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。'; + String get broadcastTimezone => '時區'; @override - String get searchSearch => '搜尋'; + String get broadcastFideRatingCategory => 'FIDE 評級類別'; @override - String get settingsSettings => '設定'; + String get broadcastOptionalDetails => '其他細節'; @override - String get settingsCloseAccount => '關閉帳戶'; + String get broadcastPastBroadcasts => '直播紀錄'; @override - String get settingsClosingIsDefinitive => '您確定要刪除帳號嗎?這是不能挽回的'; + String get broadcastAllBroadcastsByMonth => '以月份顯示所有直播'; @override - String get settingsCantOpenSimilarAccount => '即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 個直播', + ); + return '$_temp0'; + } @override - String get settingsChangedMindDoNotCloseAccount => '我改變主意了,不要關閉我的帳號'; + String challengeChallengesX(String param1) { + return '挑戰: $param1'; + } @override - String get settingsCloseAccountExplanation => '您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登錄。'; + String get challengeChallengeToPlay => '邀請對弈'; @override - String get settingsThisAccountIsClosed => '此帳號已被關閉。'; + String get challengeChallengeDeclined => '對弈邀請已拒絕'; @override - String get playWithAFriend => '和好友下棋'; + String get challengeChallengeAccepted => '對弈邀請已接受'; @override - String get playWithTheMachine => '和電腦下棋'; + String get challengeChallengeCanceled => '對弈邀請已撤銷'; @override - String get toInviteSomeoneToPlayGiveThisUrl => '邀人下棋,請分享這個網址'; + String get challengeRegisterToSendChallenges => '請登入以向其他人發出對弈邀請'; @override - String get gameOver => '遊戲結束'; + String challengeYouCannotChallengeX(String param) { + return '你無法向$param發出對弈邀請'; + } @override - String get waitingForOpponent => '等待對手'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param沒有接受對弈邀請'; + } @override - String get waiting => '請稍等'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return '您的$param1積分與$param2相差太多'; + } @override - String get yourTurn => '該你走'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return '由於您的$param積分不夠穩定,無法發出挑戰。'; + } @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1等級 $param2'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param只接受好友的對弈邀請'; } @override - String get level => '難度'; + String get challengeDeclineGeneric => '我目前不接受對弈'; @override - String get strength => '強度'; + String get challengeDeclineLater => '我現在不接受對弈,請晚點再詢問'; @override - String get toggleTheChat => '聊天開關'; + String get challengeDeclineTooFast => '這個時間控制對我來說太快了,請用慢一點的遊戲再次挑戰。'; @override - String get chat => '聊天'; + String get challengeDeclineTooSlow => '這個時間控制對我來說太慢了,請用快一點的遊戲再次挑戰。'; @override - String get resign => '認輸'; + String get challengeDeclineTimeControl => '我不接受這個挑戰的時間控制。'; @override - String get checkmate => '將死'; + String get challengeDeclineRated => '請向我發送積分對弈。'; @override - String get stalemate => '逼和'; + String get challengeDeclineCasual => '請向我發送休閒對弈。'; @override - String get white => '白方'; + String get challengeDeclineStandard => '我不接受變體對弈。'; @override - String get black => '黑方'; + String get challengeDeclineVariant => '我現在不想玩這個變體。'; @override - String get asWhite => '使用白棋'; + String get challengeDeclineNoBot => '我不接受機器人的對弈。'; @override - String get asBlack => '使用黑棋'; + String get challengeDeclineOnlyBot => '我目前只接受機器人的對弈。'; @override - String get randomColor => '隨機選色'; + String get challengeInviteLichessUser => '或邀請一位 Lichess 用户:'; @override - String get createAGame => '開始對局'; + String get contactContact => '聯繫我們'; @override - String get whiteIsVictorious => '白方勝'; + String get contactContactLichess => '聯繫 Lichess'; @override - String get blackIsVictorious => '黑方勝'; + String get patronDonate => '捐款'; @override - String get youPlayTheWhitePieces => '您執白棋'; + String get patronLichessPatron => 'Lichess 贊助者'; @override - String get youPlayTheBlackPieces => '您執黑棋'; + String perfStatPerfStats(String param) { + return '$param戰績'; + } @override - String get itsYourTurn => '輪到你了!'; + String get perfStatViewTheGames => '查看遊戲紀錄'; @override - String get cheatDetected => '偵測到作弊行為'; + String get perfStatProvisional => '臨時'; @override - String get kingInTheCenter => '王居中'; + String get perfStatNotEnoughRatedGames => '積分賽場次太少,無法計算準確積分。'; @override - String get threeChecks => '三次將軍'; + String perfStatProgressOverLastXGames(String param) { + return '最近$param場棋局之積分變化:'; + } @override - String get raceFinished => '競王結束'; + String perfStatRatingDeviation(String param) { + return '積分誤差: $param'; + } @override - String get variantEnding => '另類終局'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return '越低的數值代表積分越穩定。 數值高於$param1時的積分會被判定為浮動積分。\n要被列入排名之中,該數值需低於$param2(標準西洋棋) 或是$param3(西洋棋變體)。'; + } @override - String get newOpponent => '換個對手'; + String get perfStatTotalGames => '總計棋局'; @override - String get yourOpponentWantsToPlayANewGameWithYou => '你的對手想和你複賽'; + String get perfStatRatedGames => '積分棋局'; @override - String get joinTheGame => '加入這盤棋'; + String get perfStatTournamentGames => '聯賽棋局'; @override - String get whitePlays => '白方走棋'; + String get perfStatBerserkedGames => '狂暴模式棋局'; @override - String get blackPlays => '黑方走棋'; + String get perfStatTimeSpentPlaying => '奕棋時間'; @override - String get opponentLeftChoices => '對方可能已經離開遊戲。您可以選擇:取勝、和棋或等待對方走棋。'; + String get perfStatAverageOpponent => '對手平均積分'; @override - String get forceResignation => '取勝'; + String get perfStatVictories => '勝場'; @override - String get forceDraw => '和棋'; + String get perfStatDefeats => '敗場'; @override - String get talkInChat => '請在聊天室裡文明一點'; + String get perfStatDisconnections => '斷線場次'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一個訪問該網址的人將與您下棋。'; + String get perfStatNotEnoughGames => '棋局數不夠多'; @override - String get whiteResigned => '白方認輸'; + String perfStatHighestRating(String param) { + return '最高積分:$param'; + } @override - String get blackResigned => '黑方認輸'; + String perfStatLowestRating(String param) { + return '最低積分:$param'; + } @override - String get whiteLeftTheGame => '白方棄局'; + String perfStatFromXToY(String param1, String param2) { + return '從$param1到$param2'; + } @override - String get blackLeftTheGame => '黑方棄局'; + String get perfStatWinningStreak => '連勝場數'; @override - String get whiteDidntMove => '白方沒有走棋'; + String get perfStatLosingStreak => '連敗場數'; @override - String get blackDidntMove => '黑方沒有走棋'; + String perfStatLongestStreak(String param) { + return '最長紀錄:$param'; + } @override - String get requestAComputerAnalysis => '請求電腦分析'; + String perfStatCurrentStreak(String param) { + return '目前記錄:$param'; + } @override - String get computerAnalysis => '電腦分析'; + String get perfStatBestRated => '積分賽勝場之最強對手'; @override - String get computerAnalysisAvailable => '電腦分析可用'; + String get perfStatGamesInARow => '連續奕棋場數'; @override - String get computerAnalysisDisabled => '電腦分析未啟用'; + String get perfStatLessThanOneHour => '兩場間距不到一小時'; @override - String get analysis => '分析棋局'; + String get perfStatMaxTimePlaying => '最高奕棋時間'; @override - String depthX(String param) { - return '深度 $param'; - } + String get perfStatNow => '現在'; @override - String get usingServerAnalysis => '正在使用伺服器分析'; + String get preferencesPreferences => '偏好設定'; @override - String get loadingEngine => '正在載入引擎 ...'; + String get preferencesDisplay => '顯示'; @override - String get calculatingMoves => '計算著法中。。。'; + String get preferencesPrivacy => '隱私'; @override - String get engineFailed => '加載引擎出錯'; + String get preferencesNotifications => '通知'; @override - String get cloudAnalysis => '雲端分析'; + String get preferencesPieceAnimation => '棋子動畫'; @override - String get goDeeper => '深入分析'; + String get preferencesMaterialDifference => '子力差距'; @override - String get showThreat => '顯示敵方威脅'; + String get preferencesBoardHighlights => '國王紅色亮光(最後一步與將軍)'; @override - String get inLocalBrowser => '在本地瀏覽器'; + String get preferencesPieceDestinations => '棋子目的地(有效走法與預先走棋)'; @override - String get toggleLocalEvaluation => '使用您當地的伺服器分析'; + String get preferencesBoardCoordinates => '棋盤座標(A-H, 1-8)'; @override - String get promoteVariation => '增加變化'; + String get preferencesMoveListWhilePlaying => '遊戲進行時顯示棋譜'; @override - String get makeMainLine => '將這步棋導入主要流程中'; + String get preferencesPgnPieceNotation => '棋譜記法'; @override - String get deleteFromHere => '從這處開始刪除'; + String get preferencesChessPieceSymbol => '棋子符號'; @override - String get forceVariation => '移除變化'; + String get preferencesPgnLetter => '字母 (K, Q, R, B, N)'; @override - String get copyVariationPgn => '複製變體 PGN'; + String get preferencesZenMode => '專注模式'; @override - String get move => '走棋'; + String get preferencesShowPlayerRatings => '顯示玩家等級分'; @override - String get variantLoss => '您因特殊規則而輸了'; + String get preferencesShowFlairs => '顯示玩家身分'; @override - String get variantWin => '您因特殊規則而贏了'; + String get preferencesExplainShowPlayerRatings => '這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。'; @override - String get insufficientMaterial => '由於棋子不足而導致平局'; + String get preferencesDisplayBoardResizeHandle => '顯示盤面大小調整區塊'; @override - String get pawnMove => '小兵移動'; + String get preferencesOnlyOnInitialPosition => '只在起始局面'; @override - String get capture => '吃子'; + String get preferencesInGameOnly => '只在遊戲中'; @override - String get close => '關閉'; + String get preferencesChessClock => '棋鐘'; @override - String get winning => '贏棋'; + String get preferencesTenthsOfSeconds => '十分之一秒'; @override - String get losing => '輸棋'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => '當剩餘時間小於10秒'; @override - String get drawn => '平手'; + String get preferencesHorizontalGreenProgressBars => '綠色橫進度條'; @override - String get unknown => '未知'; + String get preferencesSoundWhenTimeGetsCritical => '時間不足時聲音提醒'; @override - String get database => '資料庫'; + String get preferencesGiveMoreTime => '給對方更多時間'; @override - String get whiteDrawBlack => '白棋獲勝 / 平局 / 黑棋獲勝'; + String get preferencesGameBehavior => '對局行為'; @override - String averageRatingX(String param) { - return '平均評分: $param'; - } + String get preferencesHowDoYouMovePieces => '移動棋子方式?'; @override - String get recentGames => '最近的棋局'; + String get preferencesClickTwoSquares => '點擊棋子及目標位置'; @override - String get topGames => '評分最高的棋局'; + String get preferencesDragPiece => '拖曳棋子'; @override - String masterDbExplanation(String param1, String param2, String param3) { - return '來自$param2到$param3年國際棋聯積分$param1以上的棋手對局棋譜'; - } + String get preferencesBothClicksAndDrag => '兩者都行'; @override - String get dtzWithRounding => '經過四捨五入的DTZ50\'\',是基於到下次吃子或兵動的半步數目。'; + String get preferencesPremovesPlayingDuringOpponentTurn => '預先走棋(在對手的回合走棋)'; @override - String get noGameFound => '未找到遊戲'; + String get preferencesTakebacksWithOpponentApproval => '悔棋(經過對手同意)'; @override - String get maxDepthReached => '已達到最大深度!'; + String get preferencesInCasualGamesOnly => '僅限非正式遊戲'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => '試著從偏好設置中加入更多棋局'; + String get preferencesPromoteToQueenAutomatically => '兵自動升為后'; @override - String get openings => '開局'; + String get preferencesExplainPromoteToQueenAutomatically => '升變的同時按住以暫時取消自動升變'; @override - String get openingExplorer => '開局瀏覽器'; + String get preferencesWhenPremoving => '預先走棋時'; @override - String get openingEndgameExplorer => '開局與終局瀏覽器'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '在三次重覆局面時自動要求和局'; @override - String xOpeningExplorer(String param) { - return '$param開局瀏覽器'; - } + String get preferencesWhenTimeRemainingLessThanThirtySeconds => '當剩餘時間小於30秒'; @override - String get playFirstOpeningEndgameExplorerMove => '在開局/殘局瀏覽器走第一步棋'; + String get preferencesMoveConfirmation => '走棋確認'; @override - String get winPreventedBy50MoveRule => '在不違反50步和局規則下贏得這局棋'; + String get preferencesExplainCanThenBeTemporarilyDisabled => '可以在遊戲中用棋盤選單中關閉此功能'; @override - String get lossSavedBy50MoveRule => '藉由50步和局規則來避免輸掉棋局'; + String get preferencesInCorrespondenceGames => '在長期對局中'; @override - String get winOr50MovesByPriorMistake => '贏棋或因先前錯誤50步作和'; + String get preferencesCorrespondenceAndUnlimited => '通信和無限'; @override - String get lossOr50MovesByPriorMistake => '輸棋或因先前錯誤50步作和'; + String get preferencesConfirmResignationAndDrawOffers => '確認投降或和局請求'; @override - String get unknownDueToRounding => '由上次吃子或兵動開始按殘局庫建議走法走才能保證勝敗的判斷正確。這是因為Syzygy殘局庫的DTZ數值可能經過四捨五入。'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '入堡方法'; @override - String get allSet => '一切就緒!'; + String get preferencesCastleByMovingTwoSquares => '移動國王兩格'; @override - String get importPgn => '匯入 PGN'; + String get preferencesCastleByMovingOntoTheRook => '移動國王到城堡上'; @override - String get delete => '刪除'; + String get preferencesInputMovesWithTheKeyboard => '使用鍵盤輸入著法'; @override - String get deleteThisImportedGame => '刪除此匯入的棋局?'; + String get preferencesInputMovesWithVoice => '用語音輸入著法'; @override - String get replayMode => '重播模式'; + String get preferencesSnapArrowsToValidMoves => '將右鍵標示箭頭鎖定到合法棋步'; @override - String get realtimeReplay => '實時'; + String get preferencesSayGgWpAfterLosingOrDrawing => '輸棋或和棋後自動發送 \"Good game, well played\"。'; @override - String get byCPL => 'CPL'; + String get preferencesYourPreferencesHaveBeenSaved => '已儲存您的設定。'; @override - String get openStudy => '打開研究視窗'; + String get preferencesScrollOnTheBoardToReplayMoves => '在騎盤上使用滑鼠滾輪以重新顯示過去棋步'; @override - String get enable => '開啟'; + String get preferencesCorrespondenceEmailNotification => '每日以電郵列出您當前的長期對局'; @override - String get bestMoveArrow => '最佳移動的箭頭'; + String get preferencesNotifyStreamStart => '追蹤的直播主開始直播'; @override - String get showVariationArrows => '顯示變體箭頭'; + String get preferencesNotifyInboxMsg => '收件夾有新訊息'; @override - String get evaluationGauge => '棋力估計表'; + String get preferencesNotifyForumMention => '論壇評論中提到您'; @override - String get multipleLines => '路線分析線'; + String get preferencesNotifyInvitedStudy => '研究邀請'; @override - String get cpus => 'CPU'; + String get preferencesNotifyGameEvent => '長期對局更新訊息'; @override - String get memory => '記憶體'; + String get preferencesNotifyChallenge => '挑戰'; @override - String get infiniteAnalysis => '無限分析'; + String get preferencesNotifyTournamentSoon => '比賽即將開始'; @override - String get removesTheDepthLimit => '取消深度限制,使您的電腦發熱。'; + String get preferencesNotifyTimeAlarm => '長期對局的時間即將耗盡'; @override - String get engineManager => '引擎管理'; + String get preferencesNotifyBell => 'Lichess 內的鈴聲通知'; @override - String get blunder => '嚴重錯誤'; + String get preferencesNotifyPush => 'Lichess 外的設備通知'; @override - String get mistake => '錯誤'; + String get preferencesNotifyWeb => '瀏覽器通知'; @override - String get inaccuracy => '輕微失誤'; + String get preferencesNotifyDevice => '設備通知'; @override - String get moveTimes => '走棋時間'; + String get preferencesBellNotificationSound => '通知鈴聲'; @override - String get flipBoard => '翻轉棋盤'; + String get preferencesBlindfold => '盲棋'; @override - String get threefoldRepetition => '三次重複局面'; + String get puzzlePuzzles => '謎題'; @override - String get claimADraw => '要求和棋'; + String get puzzlePuzzleThemes => '謎題主題'; @override - String get offerDraw => '提出和棋'; + String get puzzleRecommended => '推薦'; @override - String get draw => '和棋'; + String get puzzlePhases => '分類'; @override - String get drawByMutualAgreement => '雙方同意和局'; + String get puzzleMotifs => '主題'; @override - String get fiftyMovesWithoutProgress => '50步規則和局'; + String get puzzleAdvanced => '高級'; @override - String get currentGames => '當前對局'; + String get puzzleLengths => '長度'; @override - String get viewInFullSize => '在整個網頁裡觀看棋局'; + String get puzzleMates => '將軍'; @override - String get logOut => '登出'; + String get puzzleGoals => '目標'; @override - String get signIn => '登入'; + String get puzzleOrigin => '來源'; @override - String get rememberMe => '保持登入狀態'; + String get puzzleSpecialMoves => '特殊移動'; @override - String get youNeedAnAccountToDoThat => '請註冊以完成該操作'; + String get puzzleDidYouLikeThisPuzzle => '您喜歡這道謎題嗎?'; @override - String get signUp => '註冊'; + String get puzzleVoteToLoadNextOne => '告訴我們加載下一題!'; @override - String get computersAreNotAllowedToPlay => '電腦與電腦輔助棋手不允許參加對弈。對弈時,請勿從國際象棋引擎、資料庫以及其他棋手那裡獲取幫助。另外,強烈建議不要創建多個帳號;過分地使用多個帳號將導致封號。'; + String get puzzleUpVote => '投票為好謎題'; @override - String get games => '棋局'; + String get puzzleDownVote => '投票為壞謎題'; @override - String get forum => '論壇'; + String get puzzleYourPuzzleRatingWillNotChange => '您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。'; @override - String xPostedInForumY(String param1, String param2) { - return '$param1發帖:$param2'; - } + String get puzzleFindTheBestMoveForWhite => '為白方找出最佳移動'; @override - String get latestForumPosts => '最新論壇貼文'; + String get puzzleFindTheBestMoveForBlack => '為黑方找出最佳移動'; @override - String get players => '棋手'; + String get puzzleToGetPersonalizedPuzzles => '得到個人推薦題目:'; @override - String get friends => '朋友'; + String puzzlePuzzleId(String param) { + return '謎題 $param'; + } @override - String get discussions => '對話'; + String get puzzlePuzzleOfTheDay => '每日一題'; @override - String get today => '今天'; + String get puzzleDailyPuzzle => '每日謎題'; @override - String get yesterday => '昨天'; + String get puzzleClickToSolve => '點擊解題'; @override - String get minutesPerSide => '每方分鐘數'; + String get puzzleGoodMove => '好棋'; @override - String get variant => '變體'; + String get puzzleBestMove => '最佳走法!'; @override - String get variants => '變體'; + String get puzzleKeepGoing => '加油!'; @override - String get timeControl => '時間限制'; + String get puzzlePuzzleSuccess => '成功!'; @override - String get realTime => '實時棋局'; + String get puzzlePuzzleComplete => '解題完成!'; @override - String get correspondence => '通信棋局'; + String get puzzleByOpenings => '以開局區分'; @override - String get daysPerTurn => '每步允許天數'; + String get puzzlePuzzlesByOpenings => '以開局區分謎題'; @override - String get oneDay => '一天'; + String get puzzleOpeningsYouPlayedTheMost => '您最常使用的開局'; @override - String get time => '時間'; + String get puzzleUseFindInPage => '在瀏覽器中使用「在頁面中尋找」以尋找你最喜歡的開局!'; @override - String get rating => '評級'; + String get puzzleUseCtrlF => '按下 Ctrl+f 以找出您最喜歡的開局方式!'; @override - String get ratingStats => '評分數據'; + String get puzzleNotTheMove => '不是這步!'; @override - String get username => '用戶名'; + String get puzzleTrySomethingElse => '試試其他的移動'; @override - String get usernameOrEmail => '用戶名或電郵地址'; + String puzzleRatingX(String param) { + return '評級:$param'; + } @override - String get changeUsername => '更改用戶名'; + String get puzzleHidden => '隱藏'; @override - String get changeUsernameNotSame => '只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。'; + String puzzleFromGameLink(String param) { + return '來自對局 $param'; + } @override - String get changeUsernameDescription => '更改用戶名。您最多可以更改一次字母大小寫。'; + String get puzzleContinueTraining => '繼續訓練'; @override - String get signupUsernameHint => '請選擇一個和諧的用戶名,用戶名無法再次更改,並且不合規的用戶名會導致帳戶被封禁!'; + String get puzzleDifficultyLevel => '困難度'; @override - String get signupEmailHint => '僅用於密碼重置'; + String get puzzleNormal => '一般'; @override - String get password => '密碼'; + String get puzzleEasier => '簡單'; @override - String get changePassword => '更改密碼'; + String get puzzleEasiest => '超簡單'; @override - String get changeEmail => '更改電郵地址'; + String get puzzleHarder => '困難'; @override - String get email => '電郵地址'; + String get puzzleHardest => '超困難'; @override - String get passwordReset => '重置密碼'; + String get puzzleExample => '範例'; @override - String get forgotPassword => '忘記密碼?'; + String get puzzleAddAnotherTheme => '加入其他主題'; @override - String get error_weakPassword => '此密碼太常見,且很容易被猜到。'; + String get puzzleNextPuzzle => '下個謎題'; @override - String get error_namePassword => '請不要把密碼設為用戶名。'; + String get puzzleJumpToNextPuzzleImmediately => '立即跳到下一個謎題'; @override - String get blankedPassword => '你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。'; + String get puzzlePuzzleDashboard => '謎題能力分析'; @override - String get youAreLeavingLichess => '你正在離開 Lichess'; + String get puzzleImprovementAreas => '弱點'; @override - String get neverTypeYourPassword => '不要在其他網站輸入你的 Lichess 密碼!'; + String get puzzleStrengths => '強項'; @override - String proceedToX(String param) { - return '前往 $param'; - } + String get puzzleHistory => '解題紀錄'; @override - String get passwordSuggestion => '不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。'; + String get puzzleSolved => '解決'; @override - String get emailSuggestion => '不要使用他人提供的郵箱地址,他們會用它盜取你的帳戶。'; + String get puzzleFailed => '失敗'; @override - String get emailConfirmHelp => '協助郵件確認'; + String get puzzleStreakDescription => '累積你的連勝,解著漸漸變難的題目。 沒有時間限制,不要急。走錯一步,將會是遊戲結束!\n不過每一局中你都有跳過一步棋的機會。'; @override - String get emailConfirmNotReceived => '註冊後沒有收到確認郵件?'; + String puzzleYourStreakX(String param) { + return '您的連勝場數:$param'; + } @override - String get whatSignupUsername => '你用了什麼用戶名註冊?'; + String get puzzleStreakSkipExplanation => '跳過這一步來維持您的連勝紀錄!每次遊玩只能使用一次。'; @override - String usernameNotFound(String param) { - return '找不到用戶 $param。'; - } + String get puzzleContinueTheStreak => '繼續遊玩'; @override - String get usernameCanBeUsedForNewAccount => '你可以使用這個用戶名創建帳戶'; + String get puzzleNewStreak => '新的連勝紀錄'; @override - String emailSent(String param) { - return '我們向 $param 發送了電子郵件。'; - } + String get puzzleFromMyGames => '來自我的棋局'; @override - String get emailCanTakeSomeTime => '可能需要一些時間才能收到。'; + String get puzzleLookupOfPlayer => '尋找其他棋手的棋局謎題'; @override - String get refreshInboxAfterFiveMinutes => '等待5分鐘並刷新你的收件箱。'; + String puzzleFromXGames(String param) { + return '來自$param棋局的謎題'; + } @override - String get checkSpamFolder => '嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。'; + String get puzzleSearchPuzzles => '尋找謎題'; @override - String get emailForSignupHelp => '如果其他所有的方法都失敗了,給我們發這條短信:'; + String get puzzleFromMyGamesNone => '你在資料庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加從你的棋局中生成謎題的機會!'; @override - String copyTextToEmail(String param) { - return '複製並粘貼上面的文本然後把它發給$param'; + String puzzleFromXGamesFound(String param1, String param2) { + return '在$param2中找到$param1個謎題'; } @override - String get waitForSignupHelp => '我們很快就會給你回復,説明你完成註冊。'; + String get puzzlePuzzleDashboardDescription => '訓練、分析、改進'; @override - String accountConfirmed(String param) { - return '這個使用者 $param 成功地確認了'; + String puzzlePercentSolved(String param) { + return '$param 已解決'; } @override - String accountCanLogin(String param) { - return '你可以做為 $param 登入了。'; - } + String get puzzleNoPuzzlesToShow => '沒有什麼可展示的,先去玩一些謎題吧!'; @override - String get accountConfirmationEmailNotNeeded => '你不需要確認電子郵件。'; + String get puzzleImprovementAreasDescription => '訓練這些類型的謎題來優化你的進步!'; @override - String accountClosed(String param) { - return '帳戶 $param 被關閉。'; - } + String get puzzleStrengthDescription => '你在這些主題中表現最好'; @override - String accountRegisteredWithoutEmail(String param) { - return '帳戶 $param 未使用電子郵箱註冊。'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '已被嘗試$count次', + ); + return '$_temp0'; } @override - String get rank => '排名'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '低於你的謎題積分$count點', + ); + return '$_temp0'; + } @override - String rankX(String param) { - return '排名:$param'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '高於你的謎題積分$count點', + ); + return '$_temp0'; } @override - String get gamesPlayed => '盤棋已結束'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 已遊玩', + ); + return '$_temp0'; + } @override - String get cancel => '取消'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 重玩', + ); + return '$_temp0'; + } @override - String get whiteTimeOut => '白方時間到'; + String get puzzleThemeAdvancedPawn => '升變兵'; @override - String get blackTimeOut => '黑方時間到'; + String get puzzleThemeAdvancedPawnDescription => '你的其中一個兵已經深入了對方的棋位,或許要威脅升變。'; @override - String get drawOfferSent => '和棋請求已發送'; + String get puzzleThemeAdvantage => '取得優勢'; @override - String get drawOfferAccepted => '同意和棋'; + String get puzzleThemeAdvantageDescription => '把握機會以取得決定性優勢。(200 厘兵 ≤ 評估值 ≤ 600 厘兵)'; @override - String get drawOfferCanceled => '和棋取消'; + String get puzzleThemeAnastasiaMate => '阿納斯塔西亞殺法'; @override - String get whiteOffersDraw => '白方提出和棋'; + String get puzzleThemeAnastasiaMateDescription => '馬與車或后聯手在棋盤邊困住對手國王以及另一對手棋子'; @override - String get blackOffersDraw => '黑方提出和棋'; + String get puzzleThemeArabianMate => '阿拉伯殺法'; @override - String get whiteDeclinesDraw => '白方拒絕和棋'; + String get puzzleThemeArabianMateDescription => '馬和車聯手把對方的王困住在角落的位置'; @override - String get blackDeclinesDraw => '黑方拒絕和棋'; + String get puzzleThemeAttackingF2F7 => '攻擊 f2 或 f7'; @override - String get yourOpponentOffersADraw => '您的對手提出和棋'; + String get puzzleThemeAttackingF2F7Description => '專注於 f2 或 f7 兵的攻擊,像是 fried liver 攻擊'; @override - String get accept => '接受'; + String get puzzleThemeAttraction => '吸引'; @override - String get decline => '拒絕'; + String get puzzleThemeAttractionDescription => '一種換子或犧牲強迫對手旗子到某格以好進行接下來的戰術。'; @override - String get playingRightNow => '正在對局'; + String get puzzleThemeBackRankMate => '後排將死'; @override - String get eventInProgress => '正在進行'; + String get puzzleThemeBackRankMateDescription => '在對方的王於底線被自身的棋子困住時,將死對方的王'; @override - String get finished => '已結束'; + String get puzzleThemeBishopEndgame => '主教殘局'; @override - String get abortGame => '中止本局'; + String get puzzleThemeBishopEndgameDescription => '只剩象和兵的殘局'; @override - String get gameAborted => '棋局已中止'; + String get puzzleThemeBodenMate => '波登殺法'; @override - String get standard => '標準'; + String get puzzleThemeBodenMateDescription => '以在對角線上的兩個主教將死被自身棋子困住的王。'; @override - String get unlimited => '無限'; + String get puzzleThemeCastling => '易位'; @override - String get mode => '模式'; + String get puzzleThemeCastlingDescription => '讓國王回到安全,並讓車發動攻擊。'; @override - String get casual => '休閒'; + String get puzzleThemeCapturingDefender => '吃子 - 防守者'; @override - String get rated => '排位賽'; + String get puzzleThemeCapturingDefenderDescription => '移除防守其他棋子的防守者以攻擊未被保護的棋子'; @override - String get casualTournament => '休閒'; + String get puzzleThemeCrushing => '壓倒性優勢'; @override - String get ratedTournament => '排位'; + String get puzzleThemeCrushingDescription => '察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)'; @override - String get thisGameIsRated => '此對局是排位賽'; + String get puzzleThemeDoubleBishopMate => '雙主教將死'; @override - String get rematch => '重賽'; + String get puzzleThemeDoubleBishopMateDescription => '相鄰對角線上的兩個主教將將死被自身棋子困住的王。'; @override - String get rematchOfferSent => '重賽請求已發送'; + String get puzzleThemeDovetailMate => '柯齊奧將死'; @override - String get rematchOfferAccepted => '重賽請求被接受'; + String get puzzleThemeDovetailMateDescription => '以皇后將死被自身棋子困住的國王'; @override - String get rematchOfferCanceled => '重賽請求被取消'; + String get puzzleThemeEquality => '均勢'; @override - String get rematchOfferDeclined => '重賽請求被拒絕'; + String get puzzleThemeEqualityDescription => '從劣勢中反敗為和。(分析值 ≤ 200厘兵)'; @override - String get cancelRematchOffer => '取消重賽請求'; + String get puzzleThemeKingsideAttack => '王翼攻擊'; @override - String get viewRematch => '觀看重賽'; + String get puzzleThemeKingsideAttackDescription => '在對方於王翼易位後的攻擊。'; @override - String get confirmMove => '確認移動'; + String get puzzleThemeClearance => '騰挪'; @override - String get play => '下棋'; + String get puzzleThemeClearanceDescription => '為了施展戰術而清除我方攻擊格上的障礙物。'; @override - String get inbox => '收件箱'; + String get puzzleThemeDefensiveMove => '加強防守'; @override - String get chatRoom => '聊天室'; + String get puzzleThemeDefensiveMoveDescription => '一種為了避免遺失棋子或優勢而採取的必要行動。'; @override - String get loginToChat => '登入以聊天'; + String get puzzleThemeDeflection => '引離'; @override - String get youHaveBeenTimedOut => '由於時間原因您不能發言'; + String get puzzleThemeDeflectionDescription => '為了分散敵人專注力所採取的戰術,容易搗亂敵人原本的計畫。'; @override - String get spectatorRoom => '觀眾室'; + String get puzzleThemeDiscoveredAttack => '閃擊'; @override - String get composeMessage => '寫信息'; + String get puzzleThemeDiscoveredAttackDescription => '將一子(例如騎士)移開長程攻擊格(例如城堡)。'; @override - String get subject => '主題'; + String get puzzleThemeDoubleCheck => '雙將'; @override - String get send => '發送'; + String get puzzleThemeDoubleCheckDescription => '雙重將軍,讓我方能攻擊敵人的他子。'; @override - String get incrementInSeconds => '增加秒數'; + String get puzzleThemeEndgame => '殘局'; @override - String get freeOnlineChess => '免費線上國際象棋'; + String get puzzleThemeEndgameDescription => '棋局中最後階段的戰術'; @override - String get exportGames => '導出棋局'; + String get puzzleThemeEnPassantDescription => '一種食敵方過路兵的戰略。'; @override - String get ratingRange => '對方級別範圍'; + String get puzzleThemeExposedKing => '未被保護的國王'; @override - String get thisAccountViolatedTos => '此帳號違反了Lichess的使用規定'; + String get puzzleThemeExposedKingDescription => '攻擊未被保護的國王之戰術,常常導致將死。'; @override - String get openingExplorerAndTablebase => '開局瀏覽器 & 總局資料庫'; + String get puzzleThemeFork => '捉雙'; @override - String get takeback => '悔棋'; + String get puzzleThemeForkDescription => '一種同時攻擊敵方多個子,使敵方只能犧牲一子的戰術。'; @override - String get proposeATakeback => '請求悔棋'; + String get puzzleThemeHangingPiece => '懸子'; @override - String get takebackPropositionSent => '悔棋請求已發送'; + String get puzzleThemeHangingPieceDescription => '「免費」取得他子的戰術'; @override - String get takebackPropositionDeclined => '悔棋請求被拒絕'; + String get puzzleThemeHookMate => '鉤將死'; @override - String get takebackPropositionAccepted => '同意悔棋'; + String get puzzleThemeHookMateDescription => '利用車馬兵與一敵方兵以限制敵方國王的逃生路線。'; @override - String get takebackPropositionCanceled => '悔棋請求已取消'; + String get puzzleThemeInterference => '干擾'; @override - String get yourOpponentProposesATakeback => '對手請求悔棋'; + String get puzzleThemeInterferenceDescription => '將一子擋在兩個敵方子之間以切斷防護,例如以騎士在兩車之間阻擋。'; @override - String get bookmarkThisGame => '收藏該棋局'; + String get puzzleThemeIntermezzo => 'Intermezzo'; @override - String get tournament => '錦標賽'; + String get puzzleThemeIntermezzoDescription => '與其走正常的棋譜,不如威脅敵方子吧!這樣不但可以破壞敵方原先計畫,還可以讓敵人必須對威脅採取對應的動作。這種戰術又稱為「Zwischenzug」或「In between」。'; @override - String get tournaments => '錦標賽'; + String get puzzleThemeKnightEndgame => '馬殘局'; @override - String get tournamentPoints => '錦標賽得分'; + String get puzzleThemeKnightEndgameDescription => '只剩馬和兵的殘局'; @override - String get viewTournament => '觀看錦標賽'; + String get puzzleThemeLong => '長謎題'; @override - String get backToTournament => '返回錦標賽主頁'; + String get puzzleThemeLongDescription => '三步獲勝'; @override - String get noDrawBeforeSwissLimit => '在瑞士錦標賽中,在下三十步棋前你不能提和.'; + String get puzzleThemeMaster => '大師棋局'; @override - String get thematic => '特殊開局'; + String get puzzleThemeMasterDescription => '從頭銜玩家的棋局中生成的謎題。'; @override - String yourPerfRatingIsProvisional(String param) { - return '您目前的評分$param為臨時評分'; - } + String get puzzleThemeMasterVsMaster => '大師對局'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return '您的 $param1 積分 ($param2) 過高'; - } + String get puzzleThemeMasterVsMasterDescription => '從兩位頭銜玩家的棋局中生成的謎題。'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return '您本週最高 $param1 積分 ($param2) 過高'; - } + String get puzzleThemeMate => '將軍'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return '您的 $param1 積分 ($param2) 過低'; - } + String get puzzleThemeMateDescription => '以你的技能贏得勝利'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return '在$param2模式下的評分大於$param1'; - } + String get puzzleThemeMateIn1 => '一步殺棋'; @override - String ratedLessThanInPerf(String param1, String param2) { - return '在$param2模式下的評分小於$param1'; - } + String get puzzleThemeMateIn1Description => '一步將軍'; @override - String mustBeInTeam(String param) { - return '需要在$param團隊內'; - } + String get puzzleThemeMateIn2 => '兩步殺棋'; @override - String youAreNotInTeam(String param) { - return '您不在$param團隊中'; - } + String get puzzleThemeMateIn2Description => '走兩步以達到將軍'; @override - String get backToGame => '返回棋局'; + String get puzzleThemeMateIn3 => '三步殺棋'; @override - String get siteDescription => '界面清新的免費線上國際象棋平台。不用註冊,沒有廣告,無需插件。快來與電腦、朋友和陌生人一起對戰吧!'; + String get puzzleThemeMateIn3Description => '走三步以達到將軍'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1加入$param2隊'; - } + String get puzzleThemeMateIn4 => '四步殺棋'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1組建$param2隊'; - } + String get puzzleThemeMateIn4Description => '走四步以達到將軍'; @override - String get startedStreaming => '開始直播'; + String get puzzleThemeMateIn5 => '五步或更高 將軍'; @override - String xStartedStreaming(String param) { - return '$param開始直播'; - } + String get puzzleThemeMateIn5Description => '看出較長的將死步驟。'; @override - String get averageElo => '平均級別'; + String get puzzleThemeMiddlegame => '中局'; @override - String get location => '所在地'; + String get puzzleThemeMiddlegameDescription => '棋局中第二階段的戰術'; @override - String get filterGames => '篩選棋局'; + String get puzzleThemeOneMove => '一步題'; @override - String get reset => '重置'; + String get puzzleThemeOneMoveDescription => '只有一步長的題目'; @override - String get apply => '套用'; + String get puzzleThemeOpening => '開局'; @override - String get save => '儲存'; + String get puzzleThemeOpeningDescription => '棋局中起始階段的戰術'; @override - String get leaderboard => '排行榜'; + String get puzzleThemePawnEndgame => '兵殘局'; @override - String get screenshotCurrentPosition => '截圖當前頁面'; + String get puzzleThemePawnEndgameDescription => '只剩兵的殘局'; @override - String get gameAsGIF => '保存棋局為 GIF'; + String get puzzleThemePin => '牽制'; @override - String get pasteTheFenStringHere => '在此處黏貼FEN棋譜'; + String get puzzleThemePinDescription => '一種涉及「牽制」,讓一敵方子無法在讓其他更高價值的子不被受到攻擊下移動的戰術。'; @override - String get pasteThePgnStringHere => '在此處黏貼PGN棋譜'; + String get puzzleThemePromotion => '升變'; @override - String get orUploadPgnFile => '或者上傳一個PGN文件'; + String get puzzleThemePromotionDescription => '讓兵走到後排升變為皇后或其他高價值的子。'; @override - String get fromPosition => '自定義局面'; + String get puzzleThemeQueenEndgame => '后殘局'; @override - String get continueFromHere => '从此處繼續'; + String get puzzleThemeQueenEndgameDescription => '只剩后和兵的殘局'; @override - String get toStudy => '研究'; + String get puzzleThemeQueenRookEndgame => '后與車'; @override - String get importGame => '導入棋局'; + String get puzzleThemeQueenRookEndgameDescription => '只剩后、車和兵的殘局'; @override - String get importGameExplanation => '貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。'; + String get puzzleThemeQueensideAttack => '后翼攻擊'; @override - String get importGameCaveat => '變著分支將被刪除。 若要保存這些變著,請通過導入PGN棋譜創建一個研究。'; + String get puzzleThemeQueensideAttackDescription => '在對方於后翼易位後的攻擊。'; @override - String get thisIsAChessCaptcha => '這是一個國際象棋驗證碼。'; + String get puzzleThemeQuietMove => '安靜的一着'; @override - String get clickOnTheBoardToMakeYourMove => '點擊棋盤走棋以證明您是人類。'; + String get puzzleThemeQuietMoveDescription => '隱藏在未來敵方無法避免的攻擊。'; @override - String get captcha_fail => '請完成驗證。'; + String get puzzleThemeRookEndgame => '車殘局'; @override - String get notACheckmate => '沒有將死'; + String get puzzleThemeRookEndgameDescription => '只剩車和兵的殘局'; @override - String get whiteCheckmatesInOneMove => '白方一步將死'; + String get puzzleThemeSacrifice => '棄子'; @override - String get blackCheckmatesInOneMove => '黑方一步棋將死對手'; + String get puzzleThemeSacrificeDescription => '犧牲我方子以在一系列的移動後得到優勢。'; @override - String get retry => '重試'; + String get puzzleThemeShort => '短謎題'; @override - String get reconnecting => '重新連接中'; + String get puzzleThemeShortDescription => '兩步獲勝'; @override - String get favoriteOpponents => '最喜歡的對手'; + String get puzzleThemeSkewer => '串擊'; @override - String get follow => '關注'; + String get puzzleThemeSkewerDescription => '攻擊敵方高價值的子以讓敵方移開,以攻擊背後較為低價值未受保護的他子。為一種反向的「牽制」。'; @override - String get following => '已關注'; + String get puzzleThemeSmotheredMate => '悶殺'; @override - String get unfollow => '取消關注'; + String get puzzleThemeSmotheredMateDescription => '一種以馬將死被自身棋子所圍困的國王。'; @override - String followX(String param) { - return '追蹤$param'; - } + String get puzzleThemeSuperGM => '超級大師賽局'; @override - String unfollowX(String param) { - return '取消追蹤$param'; - } + String get puzzleThemeSuperGMDescription => '來自世界各地優秀玩家對局的戰術題'; @override - String get block => '加入黑名單'; + String get puzzleThemeTrappedPiece => '被困的棋子'; @override - String get blocked => '已加入黑名單'; + String get puzzleThemeTrappedPieceDescription => '一子因為被限制逃生路線而無法逃離被犧牲的命運。'; @override - String get unblock => '移除出黑名單'; + String get puzzleThemeUnderPromotion => '升變'; @override - String get followsYou => '關注您'; + String get puzzleThemeUnderPromotionDescription => '升變成騎士、象或車'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1開始關注$param2'; - } + String get puzzleThemeVeryLong => '非常長的謎題'; @override - String get more => '更多'; + String get puzzleThemeVeryLongDescription => '四步或以上獲勝'; @override - String get memberSince => '註冊日期'; + String get puzzleThemeXRayAttack => '穿透攻擊'; @override - String lastSeenActive(String param) { - return '最近登入 $param'; - } + String get puzzleThemeXRayAttackDescription => '以敵方子攻擊或防守的戰術。'; @override - String get player => '棋手'; + String get puzzleThemeZugzwang => '等著'; @override - String get list => '列表'; + String get puzzleThemeZugzwangDescription => '對方的棋子因為所移動的空間有限所以所到之處都會增加對方劣勢'; @override - String get graph => '圖表'; + String get puzzleThemeMix => '綜合'; @override - String get required => '必填項目。'; + String get puzzleThemeMixDescription => '所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。'; @override - String get openTournaments => '公開錦標賽'; + String get puzzleThemePlayerGames => '玩家謎題'; @override - String get duration => '持續時間'; + String get puzzleThemePlayerGamesDescription => '查詢從你或其他玩家的對奕所生成的謎題。'; @override - String get winner => '勝利者'; + String puzzleThemePuzzleDownloadInformation(String param) { + return '這些為公開謎題,並且在 $param 提供下載管道。'; + } @override - String get standing => '名次'; + String get searchSearch => '搜尋'; @override - String get createANewTournament => '建立新的錦標賽'; + String get settingsSettings => '設定'; @override - String get tournamentCalendar => '錦標賽日程'; + String get settingsCloseAccount => '關閉帳戶'; @override - String get conditionOfEntry => '加入限制:'; + String get settingsManagedAccountCannotBeClosed => '您的帳號已被管理並且無法關閉。'; @override - String get advancedSettings => '高級設定'; + String get settingsClosingIsDefinitive => '您確定要刪除帳號嗎?這是無法挽回的。'; @override - String get safeTournamentName => '幫錦標賽挑選一個適合的名字'; + String get settingsCantOpenSimilarAccount => '即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶'; @override - String get inappropriateNameWarning => '即便只是一點點的違規都有可能導致您的帳號被封鎖。'; + String get settingsChangedMindDoNotCloseAccount => '我改變主意了,不要關閉我的帳號'; @override - String get emptyTournamentName => '若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。'; + String get settingsCloseAccountExplanation => '您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登入。'; @override - String get makePrivateTournament => '把錦標賽設定為私人,並設定密碼來限制進入。'; + String get settingsThisAccountIsClosed => '此帳號已被關閉。'; @override - String get join => '加入'; + String get playWithAFriend => '和好友下棋'; @override - String get withdraw => '離開'; + String get playWithTheMachine => '和電腦下棋'; @override - String get points => '分數'; + String get toInviteSomeoneToPlayGiveThisUrl => '請分享此網址以邀人下棋'; @override - String get wins => '勝'; + String get gameOver => '遊戲結束'; @override - String get losses => '負'; + String get waitingForOpponent => '等待對手'; @override - String get createdBy => '創建者:'; + String get orLetYourOpponentScanQrCode => '或是讓對手掃描這個 QR code'; @override - String get tournamentIsStarting => '錦標賽即將開始'; + String get waiting => '等待對手確認中'; @override - String get tournamentPairingsAreNowClosed => '此錦標賽的對手配對已結束。'; + String get yourTurn => '該您走'; @override - String standByX(String param) { - return '$param準備好,你馬上要開始對棋了!'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1等級 $param2'; } @override - String get pause => '暫停'; + String get level => '難度'; @override - String get resume => '繼續'; + String get strength => '強度'; @override - String get youArePlaying => '等待對手中'; + String get toggleTheChat => '聊天開關'; @override - String get winRate => '勝率'; + String get chat => '聊天'; @override - String get berserkRate => '快棋率'; + String get resign => '認輸'; @override - String get performance => '表現'; + String get checkmate => '將死'; @override - String get tournamentComplete => '錦標賽結束'; + String get stalemate => '逼和'; @override - String get movesPlayed => '步數'; + String get white => '執白'; @override - String get whiteWins => '白方獲勝'; + String get black => '執黑'; @override - String get blackWins => '黑方獲勝'; + String get asWhite => '作為白方'; @override - String get drawRate => '和棋率'; + String get asBlack => '作為黑方'; @override - String get draws => '和棋'; + String get randomColor => '隨機選色'; @override - String nextXTournament(String param) { - return '下一個$param錦標賽'; - } + String get createAGame => '開始對局'; @override - String get averageOpponent => '平均對手評分'; + String get whiteIsVictorious => '白方勝'; @override - String get boardEditor => '棋盤編輯器'; + String get blackIsVictorious => '黑方勝'; @override - String get setTheBoard => '設定版型'; + String get youPlayTheWhitePieces => '您執白棋'; @override - String get popularOpenings => '使用率最高的開局'; + String get youPlayTheBlackPieces => '您執黑棋'; @override - String get endgamePositions => '殘局局面'; + String get itsYourTurn => '該您走!'; @override - String chess960StartPosition(String param) { - return '960棋局開局位置: $param'; - } + String get cheatDetected => '偵測到作弊行為'; @override - String get startPosition => '初始佈局'; + String get kingInTheCenter => '王居中'; @override - String get clearBoard => '清空棋盤'; + String get threeChecks => '三次將軍'; @override - String get loadPosition => '裝入佈局'; + String get raceFinished => '王至第八排'; @override - String get isPrivate => '私人'; + String get variantEnding => '變體終局'; @override - String reportXToModerators(String param) { - return '將$param報告給管理人員'; - } + String get newOpponent => '換個對手'; @override - String profileCompletion(String param) { - return '個人檔案完成度:$param'; - } + String get yourOpponentWantsToPlayANewGameWithYou => '您的對手想和你複賽'; @override - String xRating(String param) { - return '$param評分'; - } + String get joinTheGame => '加入這盤棋'; @override - String get ifNoneLeaveEmpty => '如果沒有,請留空'; + String get whitePlays => '白方走棋'; @override - String get profile => '資料'; + String get blackPlays => '黑方走棋'; @override - String get editProfile => '編輯資料'; + String get opponentLeftChoices => '對方可能已經離開遊戲。您可以選擇:取勝、和棋或等待對方走棋。'; @override - String get setFlair => '設置你的圖標'; + String get forceResignation => '取勝'; @override - String get flair => '圖標'; + String get forceDraw => '和棋'; @override - String get youCanHideFlair => '有一個設置可以隱藏整個網站上所有用户圖標。'; + String get talkInChat => '請在聊天室裡文明一點'; @override - String get biography => '個人簡介'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一個訪問該網址的人將與您下棋。'; @override - String get countryRegion => '國家或地區'; + String get whiteResigned => '白方認輸'; @override - String get thankYou => '謝謝!'; + String get blackResigned => '黑方認輸'; @override - String get socialMediaLinks => '官方社群連結'; + String get whiteLeftTheGame => '白方棄賽'; @override - String get oneUrlPerLine => '每行一個網址'; + String get blackLeftTheGame => '黑方棄賽'; @override - String get inlineNotation => '棋譜集中顯示'; + String get whiteDidntMove => '白方沒有走棋'; @override - String get makeAStudy => '為了安全保管和分享,考慮創建一項研討.'; + String get blackDidntMove => '黑方沒有走棋'; @override - String get clearSavedMoves => '清空著法儲存'; + String get requestAComputerAnalysis => '請求電腦分析'; @override - String get previouslyOnLichessTV => '過去的Lichess TV直播'; + String get computerAnalysis => '電腦分析'; @override - String get onlinePlayers => '在線棋手'; + String get computerAnalysisAvailable => '電腦分析可用'; @override - String get activePlayers => '活躍棋手'; + String get computerAnalysisDisabled => '未啟用電腦分析'; @override - String get bewareTheGameIsRatedButHasNoClock => '注意,這棋局是排位賽,但是不計時!'; + String get analysis => '分析棋局'; @override - String get success => '大功告成!'; + String depthX(String param) { + return '深度 $param'; + } @override - String get automaticallyProceedToNextGameAfterMoving => '移动棋子后自动进入下一盘棋'; + String get usingServerAnalysis => '正在使用伺服器分析'; @override - String get autoSwitch => '自动更换'; + String get loadingEngine => '正在載入引擎 ...'; @override - String get puzzles => '謎題'; + String get calculatingMoves => '計算著法中...'; @override - String get name => '名'; + String get engineFailed => '加載引擎出錯'; @override - String get description => '描述'; + String get cloudAnalysis => '雲端分析'; @override - String get descPrivate => '內部簡介'; + String get goDeeper => '深入分析'; @override - String get descPrivateHelp => '僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。'; + String get showThreat => '顯示敵方威脅'; @override - String get no => '否'; + String get inLocalBrowser => '在本地瀏覽器'; @override - String get yes => '是'; + String get toggleLocalEvaluation => '使用您當地的伺服器分析'; @override - String get help => '幫助:'; + String get promoteVariation => '增加變化'; @override - String get createANewTopic => '新话题'; + String get makeMainLine => '將這步棋導入主要流程中'; @override - String get topics => '话题'; + String get deleteFromHere => '從這處開始刪除'; @override - String get posts => '貼文'; + String get collapseVariations => '隱藏變體'; @override - String get lastPost => '最近貼文'; + String get expandVariations => '顯示變體'; @override - String get views => '浏览'; + String get forceVariation => '移除變化'; @override - String get replies => '回复'; + String get copyVariationPgn => '複製變體 PGN'; @override - String get replyToThisTopic => '回复此话题'; + String get move => '走棋'; @override - String get reply => '回复'; + String get variantLoss => '您因特殊規則而輸了'; @override - String get message => '信息'; + String get variantWin => '您因特殊規則而贏了'; @override - String get createTheTopic => '创建话题'; + String get insufficientMaterial => '由於棋子不足而導致平局'; @override - String get reportAUser => '举报用户'; + String get pawnMove => '小兵移動'; @override - String get user => '用户'; + String get capture => '吃子'; @override - String get reason => '原因'; + String get close => '關閉'; @override - String get whatIsIheMatter => '举报原因?'; + String get winning => '贏棋'; @override - String get cheat => '作弊'; + String get losing => '輸棋'; @override - String get troll => '钓鱼'; + String get drawn => '平手'; @override - String get other => '其他'; + String get unknown => '未知'; @override - String get reportDescriptionHelp => '附上游戏的网址解释该用户的行为问题'; + String get database => '資料庫'; @override - String get error_provideOneCheatedGameLink => '請提供至少一局作弊棋局的連結。'; + String get whiteDrawBlack => '白棋獲勝 / 平局 / 黑棋獲勝'; @override - String by(String param) { - return '$param作'; + String averageRatingX(String param) { + return '平均評分: $param'; } @override - String importedByX(String param) { - return '由$param滙入'; - } + String get recentGames => '最近的棋局'; @override - String get thisTopicIsNowClosed => '本话题已关闭。'; + String get topGames => '評分最高的棋局'; @override - String get blog => '博客'; + String masterDbExplanation(String param1, String param2, String param3) { + return '來自$param2到$param3年國際棋聯積分$param1以上的棋手對局棋譜'; + } @override - String get notes => '笔记'; + String get dtzWithRounding => '經過四捨五入的DTZ50\'\',是基於到下次吃子或兵動的半步數目。'; @override - String get typePrivateNotesHere => '在此輸入私人筆記'; + String get noGameFound => '未找到遊戲'; @override - String get writeAPrivateNoteAboutThisUser => '備註用戶資訊'; + String get maxDepthReached => '已達到最大深度!'; @override - String get noNoteYet => '尚無筆記'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => '試著從設定中加入更多棋局'; @override - String get invalidUsernameOrPassword => '用户名或密碼錯誤'; + String get openings => '開局'; @override - String get incorrectPassword => '舊密碼錯誤'; + String get openingExplorer => '開局瀏覽器'; @override - String get invalidAuthenticationCode => '驗證碼無效'; + String get openingEndgameExplorer => '開局與終局瀏覽器'; @override - String get emailMeALink => '通過電郵發送連結給我'; + String xOpeningExplorer(String param) { + return '$param開局瀏覽器'; + } @override - String get currentPassword => '目前密碼'; + String get playFirstOpeningEndgameExplorerMove => '在開局/殘局瀏覽器走第一步棋'; @override - String get newPassword => '新密碼'; + String get winPreventedBy50MoveRule => '在不違反50步和局規則下贏得這局棋'; @override - String get newPasswordAgain => '重複新密碼'; + String get lossSavedBy50MoveRule => '藉由50步和局規則來避免輸掉棋局'; @override - String get newPasswordsDontMatch => '新密碼不符合'; + String get winOr50MovesByPriorMistake => '贏棋或因先前錯誤50步作和'; @override - String get newPasswordStrength => '密碼強度'; + String get lossOr50MovesByPriorMistake => '輸棋或因先前錯誤50步作和'; @override - String get clockInitialTime => '棋鐘起始時間'; + String get unknownDueToRounding => '由上次吃子或兵動開始按殘局庫建議走法走才能保證勝敗的判斷正確。這是因為Syzygy殘局庫的DTZ數值可能經過四捨五入。'; @override - String get clockIncrement => '加秒'; + String get allSet => '一切就緒!'; @override - String get privacy => '隱私'; + String get importPgn => '匯入 PGN'; @override - String get privacyPolicy => '隱私條款'; + String get delete => '刪除'; @override - String get letOtherPlayersFollowYou => '允许其他玩家关注'; + String get deleteThisImportedGame => '刪除此匯入的棋局?'; @override - String get letOtherPlayersChallengeYou => '允许其他玩家挑战'; + String get replayMode => '重播模式'; @override - String get letOtherPlayersInviteYouToStudy => '允許其他棋手邀請你參加研討'; + String get realtimeReplay => '實時'; @override - String get sound => '聲音'; + String get byCPL => '以厘兵損失'; @override - String get none => '無'; + String get enable => '啟用'; @override - String get fast => '快'; + String get bestMoveArrow => '最佳移動的箭頭'; @override - String get normal => '普通'; + String get showVariationArrows => '顯示變體箭頭'; @override - String get slow => '慢'; + String get evaluationGauge => '評估條'; @override - String get insideTheBoard => '棋盤內'; + String get multipleLines => '路線分析線'; @override - String get outsideTheBoard => '棋盤外'; + String get cpus => 'CPU 數量'; @override - String get onSlowGames => '慢棋時'; + String get memory => '記憶體'; @override - String get always => '總是'; + String get infiniteAnalysis => '無限分析'; @override - String get never => '永不'; + String get removesTheDepthLimit => '取消深度限制,可能會使您的電腦發熱。'; @override - String xCompetesInY(String param1, String param2) { - return '$param1参加$param2'; - } + String get blunder => '漏著'; @override - String get victory => '成功!'; + String get mistake => '錯誤'; @override - String get defeat => '戰敗'; + String get inaccuracy => '輕微失誤'; @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下贏了$param2'; - } + String get moveTimes => '走棋時間'; @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下輸給了$param2'; - } + String get flipBoard => '翻轉棋盤'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下和$param2平手'; - } + String get threefoldRepetition => '三次重複局面'; @override - String get timeline => '时间线'; + String get claimADraw => '要求和棋'; @override - String get starting => '开始时间:'; + String get offerDraw => '提出和棋'; @override - String get allInformationIsPublicAndOptional => '所有資料是公開的,同時是可選的。'; + String get draw => '和棋'; @override - String get biographyDescription => '給我們一個您的自我介紹,像是您的興趣、您喜愛的選手等'; + String get drawByMutualAgreement => '雙方同意和局'; @override - String get listBlockedPlayers => '显示黑名单用户列表'; + String get fiftyMovesWithoutProgress => '50步規則和局'; @override - String get human => '人类'; + String get currentGames => '當前對局'; @override - String get computer => '電腦'; + String get viewInFullSize => '在整個網頁裡觀看棋局'; @override - String get side => '方'; + String get logOut => '登出'; @override - String get clock => '鐘'; + String get signIn => '登入'; @override - String get opponent => '对手'; + String get rememberMe => '保持登入狀態'; @override - String get learnMenu => '學棋'; + String get youNeedAnAccountToDoThat => '請註冊以完成該操作'; @override - String get studyMenu => '研究'; + String get signUp => '註冊'; @override - String get practice => '練習'; + String get computersAreNotAllowedToPlay => '電腦與電腦輔助棋手不允許參加對弈。對弈時,請勿從國際象棋引擎、資料庫以及其他棋手那裡獲取幫助。另外,強烈建議不要創建多個帳號;過分地使用多個帳號將導致封號。'; @override - String get community => '社區'; + String get games => '棋局'; @override - String get tools => '工具'; + String get forum => '論壇'; @override - String get increment => '加秒'; + String xPostedInForumY(String param1, String param2) { + return '$param1發帖:$param2'; + } @override - String get error_unknown => '無效值'; + String get latestForumPosts => '最新論壇貼文'; @override - String get error_required => '本项必填'; + String get players => '棋手'; @override - String get error_email => '這個電子郵件地址無效'; + String get friends => '朋友'; @override - String get error_email_acceptable => '該電子郵件地址是不可用。請重新檢查後重試。'; + String get otherPlayers => '其他玩家'; @override - String get error_email_unique => '電子郵件地址無效或已被使用'; + String get discussions => '對話'; @override - String get error_email_different => '這已經是您的電子郵件地址'; + String get today => '今天'; @override - String error_minLength(String param) { - return '至少應有 $param 個字元長'; - } + String get yesterday => '昨天'; @override - String error_maxLength(String param) { - return '最多不能超過 $param 個字元長'; - } + String get minutesPerSide => '每方分鐘數'; @override - String error_min(String param) { - return '最少 $param 個字符'; - } + String get variant => '變體'; @override - String error_max(String param) { - return '最大不能超過 $param'; - } + String get variants => '變體'; @override - String ifRatingIsPlusMinusX(String param) { - return '允许评级范围±$param'; - } + String get timeControl => '時間限制'; @override - String get ifRegistered => '如已註冊'; + String get realTime => '實時棋局'; @override - String get onlyExistingConversations => '僅目前對話'; + String get correspondence => '通信棋局'; @override - String get onlyFriends => '只允許好友'; + String get daysPerTurn => '每步允許天數'; @override - String get menu => '菜单'; + String get oneDay => '一天'; @override - String get castling => '王车易位'; + String get time => '時間'; @override - String get whiteCastlingKingside => '白方短易位'; + String get rating => '評級'; @override - String get blackCastlingKingside => '黑方短易位'; + String get ratingStats => '評分數據'; @override - String tpTimeSpentPlaying(String param) { - return '花在下棋上的時間:$param'; - } + String get username => '使用者名稱'; @override - String get watchGames => '觀看對局直播'; + String get usernameOrEmail => '使用者名稱或電郵地址'; @override - String tpTimeSpentOnTV(String param) { - return '花在Lichess TV觀看直播的時間:$param'; - } + String get changeUsername => '更改使用者名稱'; @override - String get watch => '觀看'; + String get changeUsernameNotSame => '只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。'; @override - String get videoLibrary => '影片庫'; + String get changeUsernameDescription => '更改使用者名稱。您最多可以更改一次字母大小寫。'; @override - String get streamersMenu => '實況主'; + String get signupUsernameHint => '請選擇一個妥當的使用者名稱。請注意使用者名稱無法再次更改,並且不妥當的名稱會導致帳號被封禁!'; @override - String get mobileApp => '行動應用程式'; + String get signupEmailHint => '僅用於密碼重置'; @override - String get webmasters => '網站管理員'; + String get password => '密碼'; @override - String get about => '關於'; + String get changePassword => '更改密碼'; @override - String aboutX(String param) { - return '關於 $param'; - } + String get changeEmail => '更改電子郵件'; @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1是一個免費的($param2),開放性的,無廣告,開放資源的網站'; - } + String get email => '電子郵件'; @override - String get really => '真的'; + String get passwordReset => '重置密碼'; @override - String get contribute => '協助'; + String get forgotPassword => '忘記密碼?'; @override - String get termsOfService => '服務條款'; + String get error_weakPassword => '此密碼太常見,且很容易被猜到。'; @override - String get sourceCode => '原始碼'; + String get error_namePassword => '請不要把密碼設為使用者名稱。'; @override - String get simultaneousExhibitions => '車輪戰'; + String get blankedPassword => '你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。'; @override - String get host => '主持'; + String get youAreLeavingLichess => '你正要離開 Lichess'; @override - String hostColorX(String param) { - return '主持者使用旗子顏色:$param'; - } + String get neverTypeYourPassword => '不要在其他網站輸入你的 Lichess 密碼!'; @override - String get yourPendingSimuls => '你待處理的車輪戰'; + String proceedToX(String param) { + return '前往 $param'; + } @override - String get createdSimuls => '最近开始的同步赛'; + String get passwordSuggestion => '不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。'; @override - String get hostANewSimul => '主持新同步赛'; + String get emailSuggestion => '不要使用他人提供的電子郵件,他們會用它盜取您的帳號。'; @override - String get signUpToHostOrJoinASimul => '註冊以舉辦或參與車輪戰'; + String get emailConfirmHelp => '協助電郵確認'; @override - String get noSimulFound => '找不到该同步赛'; + String get emailConfirmNotReceived => '註冊後沒有收到確認郵件?'; @override - String get noSimulExplanation => '此車輪戰不存在。'; + String get whatSignupUsername => '你用了什麼使用者名稱註冊?'; @override - String get returnToSimulHomepage => '返回表演赛主页'; + String usernameNotFound(String param) { + return '找不到使用者名稱 $param。'; + } @override - String get aboutSimul => '車輪戰涉及到一個人同時和幾位棋手下棋。'; + String get usernameCanBeUsedForNewAccount => '你可以使用這個用戶名創建帳戶'; @override - String get aboutSimulImage => '在50个对手中,菲舍尔赢了47局,和了2局,输了1局。'; + String emailSent(String param) { + return '我們向 $param 發送了電子郵件。'; + } @override - String get aboutSimulRealLife => '这个概念来自真实的国际赛事。 在现实中,这涉及到主持在桌与桌之间来回穿梭走棋。'; + String get emailCanTakeSomeTime => '可能需要一些時間才能收到。'; @override - String get aboutSimulRules => '当表演赛开始的时候, 每个玩家都与主持开始对弈, 而主持用白方。 当所有的对局都结束时,表演赛就结束了。'; + String get refreshInboxAfterFiveMinutes => '等待5分鐘並刷新你的收件箱。'; @override - String get aboutSimulSettings => '表演赛总是不定级的。 复赛、悔棋和\"加时\"功能将被禁用。'; + String get checkSpamFolder => '嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。'; @override - String get create => '创建'; + String get emailForSignupHelp => '如果其他所有的方法都失敗了,給我們發這條短信:'; @override - String get whenCreateSimul => '當您創建車輪戰時,您要同時跟幾個棋手一起下棋。'; + String copyTextToEmail(String param) { + return '複製並貼上上面的文字然後把它發給$param'; + } @override - String get simulVariantsHint => '如果您选择几个变体,每个玩家都要选择下哪一种。'; + String get waitForSignupHelp => '我們很快就會給你回覆,説明你完成註冊。'; @override - String get simulClockHint => '菲舍爾時鐘設定。棋手越多,您需要的時間可能就越多。'; + String accountConfirmed(String param) { + return '使用者 $param 認證成功'; + } @override - String get simulAddExtraTime => '您可以給您的時鍾多加點時間以幫助您應對車輪戰。'; + String accountCanLogin(String param) { + return '你可以做為 $param 登入了。'; + } @override - String get simulHostExtraTime => '主持人的额外时间'; + String get accountConfirmationEmailNotNeeded => '你不需要確認電子郵件。'; @override - String get simulAddExtraTimePerPlayer => '每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。'; + String accountClosed(String param) { + return '帳戶 $param 被關閉。'; + } @override - String get simulHostExtraTimePerPlayer => '每個玩家加入后棋鐘增加的額外時間'; + String accountRegisteredWithoutEmail(String param) { + return '帳戶 $param 未使用電子郵箱註冊。'; + } @override - String get lichessTournaments => 'Lichess比赛'; + String get rank => '排名'; @override - String get tournamentFAQ => '比赛常见问题'; + String rankX(String param) { + return '排名:$param'; + } @override - String get timeBeforeTournamentStarts => '比赛准备时间'; + String get gamesPlayed => '下過局數'; @override - String get averageCentipawnLoss => '平均厘兵损失'; + String get cancel => '取消'; @override - String get accuracy => '精準度'; + String get whiteTimeOut => '白方時間到'; @override - String get keyboardShortcuts => '快捷键'; + String get blackTimeOut => '黑方時間到'; @override - String get keyMoveBackwardOrForward => '后退/前进'; + String get drawOfferSent => '和棋請求已發送'; @override - String get keyGoToStartOrEnd => '跳到开始/结束'; + String get drawOfferAccepted => '同意和棋'; @override - String get keyCycleSelectedVariation => '循環已選取的變體'; + String get drawOfferCanceled => '和棋取消'; @override - String get keyShowOrHideComments => '显示/隐藏评论'; + String get whiteOffersDraw => '白方提出和棋'; @override - String get keyEnterOrExitVariation => '进入/退出变体'; + String get blackOffersDraw => '黑方提出和棋'; @override - String get keyRequestComputerAnalysis => '請求引擎分析,從你的失誤中學習'; + String get whiteDeclinesDraw => '白方拒絕和棋'; @override - String get keyNextLearnFromYourMistakes => '下一個 (從你的失誤中學習)'; + String get blackDeclinesDraw => '黑方拒絕和棋'; @override - String get keyNextBlunder => '下一個漏著'; + String get yourOpponentOffersADraw => '您的對手提出和棋'; @override - String get keyNextMistake => '下一個錯著'; + String get accept => '接受'; @override - String get keyNextInaccuracy => '下一個疑著'; + String get decline => '拒絕'; @override - String get keyPreviousBranch => '上一個分支'; + String get playingRightNow => '正在對局'; @override - String get keyNextBranch => '下一個分支'; + String get eventInProgress => '正在進行'; @override - String get toggleVariationArrows => '切換變體箭頭'; + String get finished => '已結束'; @override - String get cyclePreviousOrNextVariation => '循環上一個/下一個變體'; + String get abortGame => '中止本局'; @override - String get toggleGlyphAnnotations => '切換圖形標註'; + String get gameAborted => '棋局已中止'; @override - String get variationArrowsInfo => '變體箭頭讓你不需棋步列表導航'; + String get standard => '標準'; @override - String get playSelectedMove => '走已選的棋步'; + String get customPosition => '自定義局面'; @override - String get newTournament => '新比赛'; + String get unlimited => '無限'; @override - String get tournamentHomeTitle => '国际象棋赛事均设有不同的时间控制和变体'; + String get mode => '模式'; @override - String get tournamentHomeDescription => '加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。'; + String get casual => '休閒'; @override - String get tournamentNotFound => '找不到该比赛'; + String get rated => '排位賽'; @override - String get tournamentDoesNotExist => '这个比赛不存在。'; + String get casualTournament => '休閒'; @override - String get tournamentMayHaveBeenCanceled => '它可能已被取消,假如所有的对手在比赛开始之前离开。'; + String get ratedTournament => '排位'; @override - String get returnToTournamentsHomepage => '返回比赛主页'; + String get thisGameIsRated => '此對局是排位賽'; @override - String weeklyPerfTypeRatingDistribution(String param) { - return '本月$param的分数分布'; - } + String get rematch => '重賽'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return '您的$param1分数是$param2分。'; - } + String get rematchOfferSent => '重賽請求已發送'; @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return '您比$param1的$param2棋手更强。'; - } + String get rematchOfferAccepted => '重賽請求被接受'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1比$param3之中的$param2棋手強。'; - } + String get rematchOfferCanceled => '重賽請求被取消'; @override - String betterThanPercentPlayers(String param1, String param2) { - return '您比$param1的$param2棋手更強。'; - } + String get rematchOfferDeclined => '重賽請求被拒絕'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return '您没有准确的$param评级。'; - } + String get cancelRematchOffer => '取消重賽請求'; @override - String get yourRating => '您的評分'; + String get viewRematch => '觀看重賽'; @override - String get cumulative => '平均累積'; + String get confirmMove => '確認移動'; @override - String get glicko2Rating => 'Glicko-2 積分'; + String get play => '下棋'; @override - String get checkYourEmail => '請檢查您的電子郵件'; + String get inbox => '收件箱'; @override - String get weHaveSentYouAnEmailClickTheLink => '我們已經發送了一封電子郵件到你的郵箱. 點擊郵件中的連結以激活您的賬號.'; + String get chatRoom => '聊天室'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => '若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。'; + String get loginToChat => '登入以聊天'; @override - String weHaveSentYouAnEmailTo(String param) { - return '我們發送了一封郵件到 $param。點擊郵件中的連結來重置您的密碼。'; - } + String get youHaveBeenTimedOut => '您已被禁言'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return '您一登记,我们就假设您同意尊重我们的使用规则($param)。'; - } + String get spectatorRoom => '觀眾室'; @override - String readAboutOur(String param) { - return '閱讀我們的$param'; - } + String get composeMessage => '寫信息'; @override - String get networkLagBetweenYouAndLichess => '您和 lichess 之間的網絡時滯'; + String get subject => '主題'; @override - String get timeToProcessAMoveOnLichessServer => 'lichess 伺服器上處理走棋的時間'; + String get send => '發送'; @override - String get downloadAnnotated => '下载带笔记的记录'; + String get incrementInSeconds => '增加秒數'; @override - String get downloadRaw => '下载无笔记的记录'; + String get freeOnlineChess => '免費線上西洋棋'; @override - String get downloadImported => '下载已导入棋局'; + String get exportGames => '導出棋局'; @override - String get crosstable => '历史表'; + String get ratingRange => '對方級別範圍'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => '您也可以用滚动键在棋盘游戏中移动。'; + String get thisAccountViolatedTos => '此帳號違反了Lichess的使用規定'; @override - String get scrollOverComputerVariationsToPreviewThem => '將鼠標移到電腦分析變招上進行預覽'; + String get openingExplorerAndTablebase => '開局瀏覽器 & 總局資料庫'; @override - String get analysisShapesHowTo => '按shift点击或右键棋盘上绘制圆圈和箭头。'; + String get takeback => '悔棋'; @override - String get letOtherPlayersMessageYou => '允許其他人發送私訊給您'; + String get proposeATakeback => '請求悔棋'; @override - String get receiveForumNotifications => '在論壇中被提及時接收通知'; + String get takebackPropositionSent => '悔棋請求已發送'; @override - String get shareYourInsightsData => '分享您的慧眼数据'; + String get takebackPropositionDeclined => '悔棋請求被拒絕'; @override - String get withNobody => '不分享'; + String get takebackPropositionAccepted => '悔棋請求被接受'; @override - String get withFriends => '與好友分享'; + String get takebackPropositionCanceled => '悔棋請求已取消'; @override - String get withEverybody => '與所有人分享'; + String get yourOpponentProposesATakeback => '對手請求悔棋'; @override - String get kidMode => '兒童模式'; + String get bookmarkThisGame => '收藏該棋局'; @override - String get kidModeIsEnabled => '已啓用兒童模式'; + String get tournament => '錦標賽'; @override - String get kidModeExplanation => '考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。'; + String get tournaments => '錦標賽'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return '在兒童模式下,Lichess的標誌會有一個$param圖示,讓你知道你的孩子是安全的。'; - } + String get tournamentPoints => '錦標賽得分'; @override - String get askYourChessTeacherAboutLiftingKidMode => '你的帳戶被管理,詢問你的老師解除兒童模式。'; + String get viewTournament => '觀看錦標賽'; @override - String get enableKidMode => '啟用兒童模式'; + String get backToTournament => '返回錦標賽主頁'; @override - String get disableKidMode => '停用兒童模式'; + String get noDrawBeforeSwissLimit => '在積分循環制錦標賽中,在下三十步棋前無法和局。'; @override - String get security => '資訊安全相關設定'; + String get thematic => '特殊開局'; @override - String get sessions => '會話'; + String yourPerfRatingIsProvisional(String param) { + return '您目前的評分$param為臨時評分'; + } @override - String get revokeAllSessions => '登出所有裝置'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return '您的 $param1 積分 ($param2) 過高'; + } @override - String get playChessEverywhere => '随处下棋!'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return '您本週最高 $param1 積分 ($param2) 過高'; + } @override - String get asFreeAsLichess => '完全又永遠的免費。'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return '您的 $param1 積分 ($param2) 過低'; + } @override - String get builtForTheLoveOfChessNotMoney => '不是為了錢,是為了國際象棋所創建。'; + String ratedMoreThanInPerf(String param1, String param2) { + return '在$param2模式下的評分大於$param1'; + } @override - String get everybodyGetsAllFeaturesForFree => '每個人都能免費使用所有功能'; + String ratedLessThanInPerf(String param1, String param2) { + return '在$param2模式下的評分小於$param1'; + } @override - String get zeroAdvertisement => '沒有廣告'; + String mustBeInTeam(String param) { + return '需要在$param團隊內'; + } @override - String get fullFeatured => '功能全面'; + String youAreNotInTeam(String param) { + return '您不在$param團隊中'; + } @override - String get phoneAndTablet => '手機和平板電腦'; + String get backToGame => '返回棋局'; @override - String get bulletBlitzClassical => '子彈,閃電,經典'; + String get siteDescription => '界面清新的免費線上國際象棋平台。不用註冊,沒有廣告,無需插件。快來與電腦、朋友和陌生人一起對戰吧!'; @override - String get correspondenceChess => '通訊賽'; + String xJoinedTeamY(String param1, String param2) { + return '$param1加入$param2隊'; + } @override - String get onlineAndOfflinePlay => '線上或離線下棋'; + String xCreatedTeamY(String param1, String param2) { + return '$param1組建$param2隊'; + } @override - String get viewTheSolution => '看解答'; + String get startedStreaming => '開始直播'; @override - String get followAndChallengeFriends => '添加好友並與他們對戰'; + String xStartedStreaming(String param) { + return '$param開始直播'; + } @override - String get gameAnalysis => '棋局分析研究'; + String get averageElo => '平均級別'; @override - String xHostsY(String param1, String param2) { - return '$param1主持$param2'; - } + String get location => '所在地'; @override - String xJoinsY(String param1, String param2) { - return '$param1加入$param2'; - } + String get filterGames => '篩選棋局'; @override - String xLikesY(String param1, String param2) { - return '$param1對$param2按讚'; - } + String get reset => '重置'; @override - String get quickPairing => '快速配對'; + String get apply => '套用'; @override - String get lobby => '大廳'; + String get save => '儲存'; @override - String get anonymous => '匿名用户'; + String get leaderboard => '排行榜'; @override - String yourScore(String param) { - return '您的分數:$param'; - } + String get screenshotCurrentPosition => '截圖當前頁面'; @override - String get language => '語言'; + String get gameAsGIF => '保存棋局為 GIF'; @override - String get background => '背景'; + String get pasteTheFenStringHere => '在此處貼上 FEN 棋譜'; @override - String get light => '亮'; + String get pasteThePgnStringHere => '在此處貼上 PGN 棋譜'; @override - String get dark => '暗'; + String get orUploadPgnFile => '或者上傳一個PGN文件'; @override - String get transparent => '透明度'; + String get fromPosition => '自定義局面'; @override - String get deviceTheme => '設備主題'; + String get continueFromHere => '從此處繼續'; @override - String get backgroundImageUrl => '背景圖片網址:'; + String get toStudy => '研究'; @override - String get pieceSet => '棋子外觀設定'; + String get importGame => '導入棋局'; @override - String get embedInYourWebsite => '嵌入您的網站'; + String get importGameExplanation => '貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。'; @override - String get usernameAlreadyUsed => '此用戶名已經有人在使用,請嘗試使用別的'; + String get importGameCaveat => '變種分支將被刪除。 若要保存這些變種,請透過導入 PGN 棋譜建立一個研究。'; @override - String get usernamePrefixInvalid => '使用者名稱必須以字母開頭'; + String get importGameDataPrivacyWarning => '此為公開 PGN。若要導入私人棋局,請使用研究。'; @override - String get usernameSuffixInvalid => '使用者名稱的結尾必須為字母或數字'; + String get thisIsAChessCaptcha => '此為西洋棋驗證碼。'; @override - String get usernameCharsInvalid => '使用者名稱只能包含字母、 數字、 底線和短劃線。'; + String get clickOnTheBoardToMakeYourMove => '點擊棋盤走棋以證明您是人類。'; @override - String get usernameUnacceptable => '此使用者名稱不可用'; + String get captcha_fail => '請完成驗證。'; @override - String get playChessInStyle => '下棋也要穿得好看'; + String get notACheckmate => '沒有將死'; @override - String get chessBasics => '基本知識'; + String get whiteCheckmatesInOneMove => '白方一步將死'; @override - String get coaches => '教練'; + String get blackCheckmatesInOneMove => '黑方一步棋將死對手'; @override - String get invalidPgn => '無效的PGN'; + String get retry => '重試'; @override - String get invalidFen => '無效的FEN'; + String get reconnecting => '重新連接中'; @override - String get custom => '自訂設定'; + String get noNetwork => '離線'; @override - String get notifications => '通知'; + String get favoriteOpponents => '最喜歡的對手'; @override - String notificationsX(String param1) { - return '通知: $param1'; - } + String get follow => '關注'; @override - String perfRatingX(String param) { - return '評分:$param'; - } + String get following => '已關注'; @override - String get practiceWithComputer => '和電腦練習'; + String get unfollow => '取消關注'; @override - String anotherWasX(String param) { - return '另一個是$param'; + String followX(String param) { + return '追蹤$param'; } @override - String bestWasX(String param) { - return '最好的一步是$param'; + String unfollowX(String param) { + return '取消追蹤$param'; } @override - String get youBrowsedAway => '您暫停了剛剛的進度'; + String get block => '加入黑名單'; @override - String get resumePractice => '繼續練習'; + String get blocked => '已加入黑名單'; @override - String get drawByFiftyMoves => '對局因 50 步規則判和。'; + String get unblock => '移除出黑名單'; @override - String get theGameIsADraw => '這是一場平局'; + String xStartedFollowingY(String param1, String param2) { + return '$param1開始關注$param2'; + } @override - String get computerThinking => '電腦運算中...'; + String get more => '更多'; @override - String get seeBestMove => '觀看最佳移動'; + String get memberSince => '註冊日期'; @override - String get hideBestMove => '隱藏最佳移動'; + String lastSeenActive(String param) { + return '最近登入 $param'; + } @override - String get getAHint => '得到提示'; + String get player => '棋手'; @override - String get evaluatingYourMove => '分析您的移動'; + String get list => '列表'; @override - String get whiteWinsGame => '白方獲勝'; + String get graph => '圖表'; @override - String get blackWinsGame => '黑方獲勝'; + String get required => '必填項目。'; @override - String get learnFromYourMistakes => '從您的失誤中學習'; + String get openTournaments => '公開錦標賽'; @override - String get learnFromThisMistake => '從您的失誤中學習'; + String get duration => '持續時間'; @override - String get skipThisMove => '跳過這一步'; + String get winner => '贏家'; @override - String get next => '下一個'; + String get standing => '名次'; @override - String xWasPlayed(String param) { - return '走了$param'; - } + String get createANewTournament => '建立新的錦標賽'; @override - String get findBetterMoveForWhite => '找出白方的最佳著法'; + String get tournamentCalendar => '錦標賽日程'; @override - String get findBetterMoveForBlack => '找出黑方的最佳著法'; + String get conditionOfEntry => '加入限制:'; @override - String get resumeLearning => '回復學習'; + String get advancedSettings => '進階設定'; @override - String get youCanDoBetter => '您還可以做得更好'; + String get safeTournamentName => '幫錦標賽挑選一個適合的名字'; @override - String get tryAnotherMoveForWhite => '嘗試白方更好其他的著法'; + String get inappropriateNameWarning => '即便只是一點點的違規都有可能導致您的帳號被封鎖。'; @override - String get tryAnotherMoveForBlack => '嘗試黑方更好其他的著法'; + String get emptyTournamentName => '若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。'; @override - String get solution => '解決方案'; + String get makePrivateTournament => '把錦標賽設定為私人,並設定密碼來限制進入。'; @override - String get waitingForAnalysis => '等待分析'; + String get join => '加入'; @override - String get noMistakesFoundForWhite => '沒有找到白方的失誤'; + String get withdraw => '離開'; @override - String get noMistakesFoundForBlack => '沒有找到黑方的失誤'; + String get points => '分數'; @override - String get doneReviewingWhiteMistakes => '已完成觀看白方的失誤'; + String get wins => '勝'; @override - String get doneReviewingBlackMistakes => '已完成觀看黑方的失誤'; + String get losses => '負'; @override - String get doItAgain => '重作一次'; + String get createdBy => '創建者:'; @override - String get reviewWhiteMistakes => '複習白方失誤'; + String get tournamentIsStarting => '錦標賽即將開始'; @override - String get reviewBlackMistakes => '複習黑方失誤'; + String get tournamentPairingsAreNowClosed => '此錦標賽的對手配對已結束。'; @override - String get advantage => '優勢'; + String standByX(String param) { + return '$param準備好,你馬上要開始對棋了!'; + } @override - String get opening => '開局'; + String get pause => '暫停'; @override - String get middlegame => '中場'; + String get resume => '繼續'; @override - String get endgame => '殘局'; + String get youArePlaying => '等待對手中'; @override - String get conditionalPremoves => '預設棋譜'; + String get winRate => '勝率'; @override - String get addCurrentVariation => '加入現有變化'; + String get berserkRate => '快棋率'; @override - String get playVariationToCreateConditionalPremoves => '著一步不同的位置以創建預估走位'; + String get performance => '表現'; @override - String get noConditionalPremoves => '無預設棋譜'; + String get tournamentComplete => '錦標賽結束'; @override - String playX(String param) { - return '移動至$param'; - } + String get movesPlayed => '步數'; @override - String get showUnreadLichessMessage => '你收到一個來自 Lichess 的私人信息。'; + String get whiteWins => '白方獲勝'; @override - String get clickHereToReadIt => '點擊閱讀'; + String get blackWins => '黑方獲勝'; @override - String get sorry => '抱歉:('; + String get drawRate => '和棋率'; @override - String get weHadToTimeYouOutForAWhile => '您被封鎖了,在一陣子的時間內將不能下棋'; + String get draws => '和棋'; @override - String get why => '為什麼?'; + String nextXTournament(String param) { + return '下一個$param錦標賽'; + } @override - String get pleasantChessExperience => '我們的目的在於為所有人提供愉快的國際象棋體驗'; + String get averageOpponent => '平均對手評分'; @override - String get goodPractice => '為此,我們必須確保所有參與者都遵循良好做法'; + String get boardEditor => '棋盤編輯器'; @override - String get potentialProblem => '當檢測到不良行為時,我們將顯示此消息'; + String get setTheBoard => '設定版型'; @override - String get howToAvoidThis => '如何避免這件事發生?'; + String get popularOpenings => '使用率最高的開局'; @override - String get playEveryGame => '下好每一盤您加入的棋'; + String get endgamePositions => '殘局局面'; @override - String get tryToWin => '試著在每個棋局裡獲勝(或至少平手)'; + String chess960StartPosition(String param) { + return '960棋局開局位置: $param'; + } @override - String get resignLostGames => '棄權(不要讓時間耗盡)'; + String get startPosition => '初始佈局'; @override - String get temporaryInconvenience => '對於給您帶來的不便,我們深表歉意'; + String get clearBoard => '清空棋盤'; @override - String get wishYouGreatGames => '並祝您在lichess.org上玩得開心。'; + String get loadPosition => '載入佈局'; @override - String get thankYouForReading => '感謝您的閱讀!'; + String get isPrivate => '私人'; @override - String get lifetimeScore => '帳戶總分'; + String reportXToModerators(String param) { + return '將$param報告給管理人員'; + } @override - String get currentMatchScore => '現時的對局分數'; + String profileCompletion(String param) { + return '個人檔案完成度:$param'; + } @override - String get agreementAssistance => '我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)'; + String xRating(String param) { + return '$param評分'; + } @override - String get agreementNice => '我會一直尊重其他的玩家'; + String get ifNoneLeaveEmpty => '如果沒有,請留空'; @override - String agreementMultipleAccounts(String param) { - return '我同意我不會開設多個帳號(除了於$param列明的原因以外)'; - } + String get profile => '資料'; @override - String get agreementPolicy => '我同意我將會遵守Lichess的規則'; + String get editProfile => '編輯資料'; @override - String get searchOrStartNewDiscussion => '尋找或開始聊天'; + String get realName => '真實名稱'; @override - String get edit => '編輯'; + String get setFlair => '設置你的身分'; @override - String get blitz => '快棋'; + String get flair => '身分'; @override - String get rapid => '快速模式'; + String get youCanHideFlair => '你可以在設定中隱藏使用者身分。'; @override - String get classical => '經典'; + String get biography => '個人簡介'; @override - String get ultraBulletDesc => '瘋狂速度模式: 低於30秒'; + String get countryRegion => '國家或地區'; @override - String get bulletDesc => '非常速度模式:低於3分鐘'; + String get thankYou => '謝謝!'; @override - String get blitzDesc => '快速模式:3到8分鐘'; + String get socialMediaLinks => '官方社群連結'; @override - String get rapidDesc => '一般模式:8到25分鐘'; + String get oneUrlPerLine => '每行一個網址'; @override - String get classicalDesc => '經典模式:25分鐘以上'; + String get inlineNotation => '棋譜集中顯示'; @override - String get correspondenceDesc => '長期模式:一天或好幾天一步'; + String get makeAStudy => '為了安全保管和分享,考慮創建一項研討.'; @override - String get puzzleDesc => '西洋棋戰術教練'; + String get clearSavedMoves => '清空著法儲存'; @override - String get important => '重要'; + String get previouslyOnLichessTV => '過去的Lichess TV直播'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return '您的問題可能已經有答案了$param1'; - } + String get onlinePlayers => '在線棋手'; @override - String get inTheFAQ => '在F.A.Q裡'; + String get activePlayers => '活躍棋手'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return '舉報一位作弊或者是不良行為的玩家,$param1'; - } + String get bewareTheGameIsRatedButHasNoClock => '注意,這棋局是排位賽,但是不計時!'; @override - String get useTheReportForm => '請造訪回報頁面'; + String get success => '大功告成!'; @override - String toRequestSupport(String param1) { - return '需要請求協助,$param1'; - } + String get automaticallyProceedToNextGameAfterMoving => '移动棋子后自动进入下一盘棋'; @override - String get tryTheContactPage => '請到協助頁面'; + String get autoSwitch => '自动更换'; @override - String makeSureToRead(String param1) { - return '確保你已閱讀 $param1'; - } + String get puzzles => '謎題'; @override - String get theForumEtiquette => '論壇禮儀'; + String get onlineBots => '線上機器人'; @override - String get thisTopicIsArchived => '該討論已封存,不能再留言'; + String get name => '名稱'; @override - String joinTheTeamXToPost(String param1) { - return '請先加入$param1團隊,才能在這則討論裡發表留言'; - } + String get description => '描述'; @override - String teamNamedX(String param1) { - return '$param1團隊'; - } + String get descPrivate => '內部簡介'; @override - String get youCannotPostYetPlaySomeGames => '您目前不能發表文章在論壇裡,先下幾盤棋吧!'; + String get descPrivateHelp => '僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。'; @override - String get subscribe => '訂閱'; + String get no => '否'; @override - String get unsubscribe => '取消訂閱'; + String get yes => '是'; @override - String mentionedYouInX(String param1) { - return '在 \"$param1\" 中提到了您。'; - } + String get website => '網頁版'; @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 在 \"$param2\" 中提到了您。'; - } + String get mobile => '行動裝置'; @override - String invitedYouToX(String param1) { - return '邀請您至\"$param1\"。'; - } + String get help => '幫助:'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 邀請您至\"$param2\"。'; - } + String get createANewTopic => '新話題'; @override - String get youAreNowPartOfTeam => '您現在是團隊的成員了。'; + String get topics => '話題'; @override - String youHaveJoinedTeamX(String param1) { - return '您已加入 \"$param1\"。'; - } + String get posts => '貼文'; @override - String get someoneYouReportedWasBanned => '您檢舉的玩家已被封鎖帳號'; + String get lastPost => '最近貼文'; @override - String get congratsYouWon => '恭喜,您贏了!'; + String get views => '瀏覽'; @override - String gameVsX(String param1) { - return '與$param1對局'; - } + String get replies => '回覆'; @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; - } + String get replyToThisTopic => '回覆此話題'; @override - String get lostAgainstTOSViolator => '你輸給了違反了服務挑款的棋手'; + String get reply => '回覆'; @override - String refundXpointsTimeControlY(String param1, String param2) { - return '退回 $param1 $param2 等級分。'; - } + String get message => '訊息'; @override - String get timeAlmostUp => '時間快到了!'; + String get createTheTopic => '建立話題'; @override - String get clickToRevealEmailAddress => '[按下展示電郵位置]'; + String get reportAUser => '舉報使用者'; @override - String get download => '下載'; + String get user => '使用者'; @override - String get coachManager => '教練管理'; + String get reason => '原因'; @override - String get streamerManager => '直播管理'; + String get whatIsIheMatter => '舉報原因?'; @override - String get cancelTournament => '取消錦標賽'; + String get cheat => '作弊'; @override - String get tournDescription => '錦標賽敘述'; + String get troll => '搗亂'; @override - String get tournDescriptionHelp => '有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用Markdown網址 [name](https://url)。'; + String get other => '其他'; @override - String get ratedFormHelp => '比賽為積分賽\n會影響到棋手的積分'; + String get reportCheatBoostHelp => '請詳細說明你舉報此使用者的具體原因並貼上遊戲連結。「他作弊」等簡短說明是不被接受的。'; @override - String get onlyMembersOfTeam => '只限隊員'; + String get reportUsernameHelp => '請詳細說明你舉報此使用者的具體原因。若必要請解釋其名詞的歷史意義、網路用語、或是此使用者名稱如何指桑罵槐。「他的使用者名稱不妥」等簡短說明是不被接受的。'; @override - String get noRestriction => '沒有限制'; + String get reportProcessedFasterInEnglish => '若舉報內容為英文將會更快的被處理。'; @override - String get minimumRatedGames => '評分局遊玩次數下限'; + String get error_provideOneCheatedGameLink => '請提供至少一局作弊棋局的連結。'; @override - String get minimumRating => '評分下限'; + String by(String param) { + return '作者:$param'; + } @override - String get maximumWeeklyRating => '每週最高評分'; + String importedByX(String param) { + return '由$param導入'; + } @override - String positionInputHelp(String param) { - return '將一個有效的 FEN 粘貼於此作為所有對局的起始位置。\n僅適用於標準國際象棋,對變體無效。\n你可以試用 $param 來生成 FEN,然後將其粘貼到這裡。\n置空表示以標準位置開始比賽。'; - } + String get thisTopicIsNowClosed => '此話題已關閉'; @override - String get cancelSimul => '取消車輪戰'; + String get blog => '部落格'; @override - String get simulHostcolor => '主持所執方'; + String get notes => '備註'; @override - String get estimatedStart => '預計開始時間'; + String get typePrivateNotesHere => '在此輸入私人備註'; @override - String simulFeatured(String param) { - return '展示在 $param'; - } + String get writeAPrivateNoteAboutThisUser => '備註用戶資訊'; @override - String simulFeaturedHelp(String param) { - return '在 $param 上向所有人展示您主持的車輪戰,對私人車輪戰無效。'; - } + String get noNoteYet => '尚無備註'; @override - String get simulDescription => '車輪戰描述'; + String get invalidUsernameOrPassword => '使用者名稱或密碼錯誤'; @override - String get simulDescriptionHelp => '有甚麼要告訴參賽者的嗎?'; + String get incorrectPassword => '舊密碼錯誤'; @override - String markdownAvailable(String param) { - return '$param 可用於更高級的格式。'; - } + String get invalidAuthenticationCode => '驗證碼無效'; @override - String get embedsAvailable => '粘貼對局URL或學習章節URL來嵌入。'; + String get emailMeALink => '通過電郵發送連結給我'; @override - String get inYourLocalTimezone => '在你的時區內'; + String get currentPassword => '目前密碼'; @override - String get tournChat => '錦標賽聊天室'; + String get newPassword => '新密碼'; @override - String get noChat => '無聊天室'; + String get newPasswordAgain => '重複新密碼'; @override - String get onlyTeamLeaders => '僅限各隊隊長'; + String get newPasswordsDontMatch => '新密碼不符合'; @override - String get onlyTeamMembers => '僅限各隊伍'; + String get newPasswordStrength => '密碼強度'; @override - String get navigateMoveTree => '定位'; + String get clockInitialTime => '棋鐘起始時間'; @override - String get mouseTricks => '滑鼠功能'; + String get clockIncrement => '加秒'; @override - String get toggleLocalAnalysis => '切換本地計算機分析'; + String get privacy => '隱私'; @override - String get toggleAllAnalysis => '切換所有(本地+服務器) 的電腦分析'; + String get privacyPolicy => '隱私條款'; @override - String get playComputerMove => '走電腦推薦的最佳著法'; + String get letOtherPlayersFollowYou => '允許其他玩家關注'; @override - String get analysisOptions => '分析局面'; + String get letOtherPlayersChallengeYou => '允許其他玩家發起挑戰'; @override - String get focusChat => '聚焦聊天'; + String get letOtherPlayersInviteYouToStudy => '允許其他棋手邀請你參加研討'; @override - String get showHelpDialog => '顯示此說明欄'; + String get sound => '音效'; @override - String get reopenYourAccount => '重新開啟帳戶'; + String get none => '無'; @override - String get closedAccountChangedMind => '如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。'; + String get fast => '快'; @override - String get onlyWorksOnce => '這只能復原一次'; + String get normal => '普通'; @override - String get cantDoThisTwice => '如果你決定再次停用你的帳號,則不會有任何方式去復原。'; + String get slow => '慢'; @override - String get emailAssociatedToaccount => '和此帳號相關的電子信箱'; + String get insideTheBoard => '棋盤內'; @override - String get sentEmailWithLink => '我們已將網址寄送至你的信箱'; + String get outsideTheBoard => '棋盤外'; @override - String get tournamentEntryCode => '錦標賽參賽碼'; + String get allSquaresOfTheBoard => '包括所有棋盤內的格子'; @override - String get hangOn => '等一下!'; + String get onSlowGames => '慢棋時'; @override - String gameInProgress(String param) { - return '您正在與 $param 進行對局。'; - } + String get always => '總是'; @override - String get abortTheGame => '中止本局'; + String get never => '永不'; @override - String get resignTheGame => '認輸'; + String xCompetesInY(String param1, String param2) { + return '$param1在$param2參加'; + } @override - String get youCantStartNewGame => '直到當下這局下完之前,你無法開始新的棋局'; + String get victory => '勝利'; @override - String get since => '自'; + String get defeat => '戰敗'; @override - String get until => '直到'; + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下贏了$param2'; + } @override - String get lichessDbExplanation => '來自 Lichess 用戶的所有評分遊戲'; + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下輸給了$param2'; + } @override - String get switchSides => '更換所持顏色'; + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下和$param2和棋'; + } @override - String get closingAccountWithdrawAppeal => '關閉帳戶將會收回你的上訴'; + String get timeline => '時間軸'; @override - String get ourEventTips => '舉辦賽事的小建議'; + String get starting => '起始時間:'; @override - String get instructions => '說明'; + String get allInformationIsPublicAndOptional => '所有資料為公開並且可被隱藏。'; @override - String get showMeEverything => '全部顯示'; + String get biographyDescription => '給一個自我介紹,例如興趣或您喜愛的選手等'; @override - String get lichessPatronInfo => 'Lichess是個慈善、完全免費之開源軟件。\n一切營運成本、開發和內容皆來自用戶之捐贈。'; + String get listBlockedPlayers => '顯示黑名單'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '您的對手已經離開了遊戲。您將在 $count 秒後獲勝。', - ); - return '$_temp0'; - } + String get human => '人類'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$count步內將死對手', - ); - return '$_temp0'; - } + String get computer => '電腦'; @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次漏著', - ); - return '$_temp0'; - } + String get side => '方'; @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次失誤', - ); - return '$_temp0'; - } + String get clock => '棋鐘'; @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次輕微失誤', - ); - return '$_temp0'; - } + String get opponent => '對手'; @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count位棋手目前在線', - ); - return '$_temp0'; - } + String get learnMenu => '學習'; @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '查看所有$count盤棋', - ); - return '$_temp0'; - } + String get studyMenu => '研究'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$param2 場對局後的 $count 等級分', - ); - return '$_temp0'; + String get practice => '練習'; + + @override + String get community => '社群'; + + @override + String get tools => '工具'; + + @override + String get increment => '加秒'; + + @override + String get error_unknown => '無效值'; + + @override + String get error_required => '本項必填'; + + @override + String get error_email => '無效電子郵件'; + + @override + String get error_email_acceptable => '該電子郵件地址無效。請重新檢查後重試。'; + + @override + String get error_email_unique => '電子郵件地址無效或已被使用'; + + @override + String get error_email_different => '這已經是您的電子郵件地址'; + + @override + String error_minLength(String param) { + return '至少包含 $param 個字元'; } @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個收藏', - ); - return '$_temp0'; + String error_maxLength(String param) { + return '最多包含 $param 個字元'; } @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count天', - ); - return '$_temp0'; + String error_min(String param) { + return '最少包含 $param 個字符'; } @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count小時', - ); - return '$_temp0'; + String error_max(String param) { + return '最多不能超過 $param'; } @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 分鐘', - ); - return '$_temp0'; + String ifRatingIsPlusMinusX(String param) { + return '允許評級範圍±$param'; } @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '評分每 $count 分鐘更新一次', - ); - return '$_temp0'; + String get ifRegistered => '已登入者'; + + @override + String get onlyExistingConversations => '僅目前對話'; + + @override + String get onlyFriends => '只允許好友'; + + @override + String get menu => '選單'; + + @override + String get castling => '王車易位'; + + @override + String get whiteCastlingKingside => '白方短易位'; + + @override + String get blackCastlingKingside => '黑方短易位'; + + @override + String tpTimeSpentPlaying(String param) { + return '花在下棋上的時間:$param'; } @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個題目', - ); - return '$_temp0'; + String get watchGames => '觀看對局直播'; + + @override + String tpTimeSpentOnTV(String param) { + return '花在Lichess TV的時間:$param'; } @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '與您下過$count盤棋', - ); - return '$_temp0'; + String get watch => '觀看'; + + @override + String get videoLibrary => '影片庫'; + + @override + String get streamersMenu => '實況主'; + + @override + String get mobileApp => '行動應用程式'; + + @override + String get webmasters => '網站管理員'; + + @override + String get about => '關於'; + + @override + String aboutX(String param) { + return '關於 $param'; } @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 場排位賽', - ); - return '$_temp0'; + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1是一個完全免費($param2)、開放性、無廣告、並且開源的網站'; } @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count局勝', - ); - return '$_temp0'; + String get really => '真的'; + + @override + String get contribute => '協助'; + + @override + String get termsOfService => '服務條款'; + + @override + String get sourceCode => '原始碼'; + + @override + String get simultaneousExhibitions => '車輪戰'; + + @override + String get host => '主持'; + + @override + String hostColorX(String param) { + return '主持人所使用旗子顏色:$param'; } @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count局負', - ); - return '$_temp0'; - } + String get yourPendingSimuls => '正在載入比賽'; + + @override + String get createdSimuls => '觀看最近開始的車輪戰'; + + @override + String get hostANewSimul => '主持車輪戰'; + + @override + String get signUpToHostOrJoinASimul => '註冊以舉辦或參與車輪戰'; + + @override + String get noSimulFound => '找不到該車輪戰'; + + @override + String get noSimulExplanation => '此車輪戰不存在。'; + + @override + String get returnToSimulHomepage => '返回車輪戰首頁'; + + @override + String get aboutSimul => '車輪戰涉及到一個人同時和幾位棋手下棋。'; + + @override + String get aboutSimulImage => '在50位對手中,費雪贏了47局、和了2局、並輸了1局。'; + + @override + String get aboutSimulRealLife => '這種賽制來自於真實的國際賽事。 在現實中,這涉及到主持人在棋局與棋局之間來回走棋。'; + + @override + String get aboutSimulRules => '當車輪賽開始時,每個玩家都會與主持人對奕,主持人持白。當所有對局結束表示車輪賽也一併結束。'; + + @override + String get aboutSimulSettings => '車輪賽事較為非正式的賽制。重賽、悔棋、以及加時功能皆會被禁用。'; + + @override + String get create => '建立'; + + @override + String get whenCreateSimul => '當您創建車輪戰時,您要同時跟幾個棋手一起下棋。'; + + @override + String get simulVariantsHint => '如果您選擇多個變體,每個玩家可以選擇自己所好的變體。'; + + @override + String get simulClockHint => '費雪棋鐘設定。棋手越多,您所需的時間可能就越多。'; + + @override + String get simulAddExtraTime => '您可以給您的時鍾多加點時間以幫助您應對車輪戰。'; + + @override + String get simulHostExtraTime => '主持人的額外時間'; + + @override + String get simulAddExtraTimePerPlayer => '每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。'; + + @override + String get simulHostExtraTimePerPlayer => '於每位玩家加入後棋鐘增加的額外時間'; + + @override + String get lichessTournaments => 'Lichess 錦標賽'; + + @override + String get tournamentFAQ => '競技場錦標賽常見問題'; + + @override + String get timeBeforeTournamentStarts => '錦標賽準備時間'; + + @override + String get averageCentipawnLoss => '平均厘兵損失'; + + @override + String get accuracy => '精準度'; + + @override + String get keyboardShortcuts => '快捷鍵'; + + @override + String get keyMoveBackwardOrForward => '後退/前進'; + + @override + String get keyGoToStartOrEnd => '跳轉到開始/結束'; + + @override + String get keyCycleSelectedVariation => '循環已選取的變體'; + + @override + String get keyShowOrHideComments => '顯示/隱藏評論'; + + @override + String get keyEnterOrExitVariation => '進入/退出變體'; + + @override + String get keyRequestComputerAnalysis => '請求引擎分析,從你的失誤中學習'; + + @override + String get keyNextLearnFromYourMistakes => '下一個 (從你的失誤中學習)'; + + @override + String get keyNextBlunder => '下一個漏著'; + + @override + String get keyNextMistake => '下一個錯誤'; + + @override + String get keyNextInaccuracy => '下一個輕微失誤'; + + @override + String get keyPreviousBranch => '上一個分支'; + + @override + String get keyNextBranch => '下一個分支'; + + @override + String get toggleVariationArrows => '顯示變體箭頭'; + + @override + String get cyclePreviousOrNextVariation => '循環上一個/下一個變體'; + + @override + String get toggleGlyphAnnotations => '顯示圖形標註'; + + @override + String get togglePositionAnnotations => '顯示位置標註'; + + @override + String get variationArrowsInfo => '變體箭頭讓你不需棋步列表導航'; + + @override + String get playSelectedMove => '走已選取的棋步'; + + @override + String get newTournament => '新比賽'; + + @override + String get tournamentHomeTitle => '富有各種時間以及變體的西洋棋錦標賽'; + + @override + String get tournamentHomeDescription => '加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。'; + + @override + String get tournamentNotFound => '找不到該錦標賽'; + + @override + String get tournamentDoesNotExist => '這個錦標賽不存在。'; + + @override + String get tournamentMayHaveBeenCanceled => '錦標賽可能因為沒有其他玩家而取消。'; + + @override + String get returnToTournamentsHomepage => '返回錦標賽首頁'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return '本月$param的分數分布'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return '您的$param1目前$param2分。'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return '您比$param1的$param2棋手更強。'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1比$param3之中的$param2棋手強。'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return '您比$param1的$param2棋手更強。'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return '您沒有準確的$param評級。'; + } + + @override + String get yourRating => '您的評分'; + + @override + String get cumulative => '平均累積'; + + @override + String get glicko2Rating => 'Glicko-2 積分'; + + @override + String get checkYourEmail => '請檢查您的電子郵件'; + + @override + String get weHaveSentYouAnEmailClickTheLink => '我們已經發送了一封電子郵件到你的郵箱。點擊郵件中的連結以啟用帳號。'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => '若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return '我們發送了一封郵件到 $param。點擊郵件中的連結來重置您的密碼。'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return '註冊帳號表示同意並且遵守 $param'; + } + + @override + String readAboutOur(String param) { + return '閱讀我們的$param'; + } + + @override + String get networkLagBetweenYouAndLichess => '您和 Lichess 之間的網路停滯'; + + @override + String get timeToProcessAMoveOnLichessServer => 'lichess 伺服器上處理走棋的時間'; + + @override + String get downloadAnnotated => '下載含有棋子走動方向的棋局'; + + @override + String get downloadRaw => '下載純文字'; + + @override + String get downloadImported => '下載導入的棋局'; + + @override + String get crosstable => '歷程表'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => '您也可以捲動棋盤以移動。'; + + @override + String get scrollOverComputerVariationsToPreviewThem => '將鼠標移到電腦分析變種上進行預覽'; + + @override + String get analysisShapesHowTo => '按 shift 點及或右鍵棋盤上以繪製圓圈與箭頭。'; + + @override + String get letOtherPlayersMessageYou => '允許其他人發送私訊給您'; + + @override + String get receiveForumNotifications => '在論壇中被提及時接收通知'; + + @override + String get shareYourInsightsData => '顯示您的洞察數據'; + + @override + String get withNobody => '不顯示'; + + @override + String get withFriends => '好友'; + + @override + String get withEverybody => '所有人'; + + @override + String get kidMode => '兒童模式'; + + @override + String get kidModeIsEnabled => '已啟用兒童模式'; + + @override + String get kidModeExplanation => '考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return '在兒童模式下,Lichess的標誌會有一個$param圖示,讓你知道你的孩子是安全的。'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => '你的帳戶被管理,詢問你的老師解除兒童模式。'; + + @override + String get enableKidMode => '啟用兒童模式'; + + @override + String get disableKidMode => '停用兒童模式'; + + @override + String get security => '資訊安全相關設定'; + + @override + String get sessions => '裝置'; + + @override + String get revokeAllSessions => '登出所有裝置'; + + @override + String get playChessEverywhere => '隨處下棋!'; + + @override + String get asFreeAsLichess => '完全、永遠免費。'; + + @override + String get builtForTheLoveOfChessNotMoney => '不是為了錢,是為了西洋棋所創建。'; + + @override + String get everybodyGetsAllFeaturesForFree => '每個人都能免費使用所有功能'; + + @override + String get zeroAdvertisement => '沒有廣告'; + + @override + String get fullFeatured => '功能全面'; + + @override + String get phoneAndTablet => '手機和平板電腦'; + + @override + String get bulletBlitzClassical => '快或慢都隨你!'; + + @override + String get correspondenceChess => '通訊賽'; + + @override + String get onlineAndOfflinePlay => '線上或離線下棋'; + + @override + String get viewTheSolution => '看解答'; + + @override + String get followAndChallengeFriends => '添加好友並與他們對戰'; + + @override + String get gameAnalysis => '棋局分析研究'; + + @override + String xHostsY(String param1, String param2) { + return '$param1主持$param2'; + } + + @override + String xJoinsY(String param1, String param2) { + return '$param1加入$param2'; + } + + @override + String xLikesY(String param1, String param2) { + return '$param1對$param2按讚'; + } + + @override + String get quickPairing => '快速配對'; + + @override + String get lobby => '大廳'; + + @override + String get anonymous => '匿名用户'; + + @override + String yourScore(String param) { + return '您的分數:$param'; + } + + @override + String get language => '語言'; + + @override + String get background => '背景'; + + @override + String get light => '亮'; + + @override + String get dark => '暗'; + + @override + String get transparent => '透明度'; + + @override + String get deviceTheme => '設備主題'; + + @override + String get backgroundImageUrl => '背景圖片網址:'; + + @override + String get board => '棋盤外觀'; + + @override + String get size => '大小'; + + @override + String get opacity => '透明度'; + + @override + String get brightness => '亮度'; + + @override + String get hue => '色調'; + + @override + String get boardReset => '回復預設顏色設定'; + + @override + String get pieceSet => '棋子外觀設定'; + + @override + String get embedInYourWebsite => '嵌入您的網站'; + + @override + String get usernameAlreadyUsed => '該使用者名稱已被使用,請換一個試試!'; + + @override + String get usernamePrefixInvalid => '使用者名稱必須以字母開頭'; + + @override + String get usernameSuffixInvalid => '使用者名稱的結尾必須為字母或數字'; + + @override + String get usernameCharsInvalid => '使用者名稱只能包含字母、 數字、 底線和短劃線。'; + + @override + String get usernameUnacceptable => '無法套用此使用者名稱'; + + @override + String get playChessInStyle => '下棋也要穿得好看'; + + @override + String get chessBasics => '基本常識'; + + @override + String get coaches => '教練'; + + @override + String get invalidPgn => '無效的 PGN'; + + @override + String get invalidFen => '無效的 FEN'; + + @override + String get custom => '自訂設定'; + + @override + String get notifications => '通知'; + + @override + String notificationsX(String param1) { + return '通知:$param1'; + } + + @override + String perfRatingX(String param) { + return '評分:$param'; + } + + @override + String get practiceWithComputer => '和電腦練習'; + + @override + String anotherWasX(String param) { + return '另一個是$param'; + } + + @override + String bestWasX(String param) { + return '最好的一步是$param'; + } + + @override + String get youBrowsedAway => '您暫停了剛剛的進度'; + + @override + String get resumePractice => '繼續練習'; + + @override + String get drawByFiftyMoves => '對局因 50 步規則判和。'; + + @override + String get theGameIsADraw => '這是一場平局'; + + @override + String get computerThinking => '電腦運算中...'; + + @override + String get seeBestMove => '觀看最佳移動'; + + @override + String get hideBestMove => '隱藏最佳移動'; + + @override + String get getAHint => '得到提示'; + + @override + String get evaluatingYourMove => '分析您的移動'; + + @override + String get whiteWinsGame => '白方獲勝'; + + @override + String get blackWinsGame => '黑方獲勝'; + + @override + String get learnFromYourMistakes => '從您的失誤中學習'; + + @override + String get learnFromThisMistake => '從您的失誤中學習'; + + @override + String get skipThisMove => '跳過這一步'; + + @override + String get next => '下一個'; + + @override + String xWasPlayed(String param) { + return '走了$param'; + } + + @override + String get findBetterMoveForWhite => '找出白方的最佳著法'; + + @override + String get findBetterMoveForBlack => '找出黑方的最佳著法'; + + @override + String get resumeLearning => '繼續學習'; + + @override + String get youCanDoBetter => '還有更好的移動'; + + @override + String get tryAnotherMoveForWhite => '嘗試白方更好其他的著法'; + + @override + String get tryAnotherMoveForBlack => '嘗試黑方更好其他的著法'; + + @override + String get solution => '解決方案'; + + @override + String get waitingForAnalysis => '等待分析'; + + @override + String get noMistakesFoundForWhite => '沒有找到白方的失誤'; + + @override + String get noMistakesFoundForBlack => '沒有找到黑方的失誤'; + + @override + String get doneReviewingWhiteMistakes => '已完成觀看白方的失誤'; + + @override + String get doneReviewingBlackMistakes => '已完成觀看黑方的失誤'; + + @override + String get doItAgain => '再試一次'; + + @override + String get reviewWhiteMistakes => '複習白方失誤'; + + @override + String get reviewBlackMistakes => '複習黑方失誤'; + + @override + String get advantage => '優勢'; + + @override + String get opening => '開局'; + + @override + String get middlegame => '中場'; + + @override + String get endgame => '殘局'; + + @override + String get conditionalPremoves => '預設棋譜'; + + @override + String get addCurrentVariation => '加入現有變種'; + + @override + String get playVariationToCreateConditionalPremoves => '走一種變種以建立棋譜'; + + @override + String get noConditionalPremoves => '無預設棋譜'; + + @override + String playX(String param) { + return '移動至$param'; + } + + @override + String get showUnreadLichessMessage => '你收到一個來自 Lichess 的私訊。'; + + @override + String get clickHereToReadIt => '點擊以閱讀'; + + @override + String get sorry => '抱歉:('; + + @override + String get weHadToTimeYouOutForAWhile => '我們必須將您暫時封鎖'; + + @override + String get why => '為什麼?'; + + @override + String get pleasantChessExperience => '我們的目的在於維持良好的下棋環境'; + + @override + String get goodPractice => '為此,我們必須確保所有參與者都遵循良好做法'; + + @override + String get potentialProblem => '當檢測到不良行為時,我們將顯示此消息'; + + @override + String get howToAvoidThis => '如何避免這件事發生?'; + + @override + String get playEveryGame => '避免在棋局中任意退出'; + + @override + String get tryToWin => '試著在每個棋局裡獲勝(或至少平手)'; + + @override + String get resignLostGames => '投降(不要讓時間耗盡)'; + + @override + String get temporaryInconvenience => '我們對於給您帶來的不便深表歉意'; + + @override + String get wishYouGreatGames => '並祝您在 lichess.org 上玩得開心。'; + + @override + String get thankYouForReading => '感謝您的閱讀!'; + + @override + String get lifetimeScore => '帳戶總分'; + + @override + String get currentMatchScore => '現時的對局分數'; + + @override + String get agreementAssistance => '我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)'; + + @override + String get agreementNice => '我會一直尊重其他的玩家'; + + @override + String agreementMultipleAccounts(String param) { + return '我同意我不會開設多個帳號(除了於$param列明的原因以外)'; + } + + @override + String get agreementPolicy => '我同意我將會遵守Lichess的規則'; + + @override + String get searchOrStartNewDiscussion => '尋找或開始聊天'; + + @override + String get edit => '編輯'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => '快速模式'; + + @override + String get classical => '經典'; + + @override + String get ultraBulletDesc => '瘋狂速度模式:低於30秒'; + + @override + String get bulletDesc => '極快速模式:低於3分鐘'; + + @override + String get blitzDesc => '快速模式:3到8分鐘'; + + @override + String get rapidDesc => '一般模式:8到25分鐘'; + + @override + String get classicalDesc => '經典模式:25分鐘以上'; + + @override + String get correspondenceDesc => '通信模式:一天或好幾天一步'; + + @override + String get puzzleDesc => '西洋棋戰術教練'; + + @override + String get important => '重要'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return '您的問題可能已經有答案了$param1'; + } + + @override + String get inTheFAQ => '在常見問答內'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return '舉報一位作弊或違反善良風俗的玩家,$param1'; + } + + @override + String get useTheReportForm => '請填寫回報表單'; + + @override + String toRequestSupport(String param1) { + return '$param1以獲取協助'; + } + + @override + String get tryTheContactPage => '請到協助頁面'; + + @override + String makeSureToRead(String param1) { + return '確保你已閱讀 $param1'; + } + + @override + String get theForumEtiquette => '論壇禮儀'; + + @override + String get thisTopicIsArchived => '該討論已封存,不能再留言'; + + @override + String joinTheTeamXToPost(String param1) { + return '請先加入$param1團隊,才能在這則討論裡發表留言'; + } + + @override + String teamNamedX(String param1) { + return '$param1團隊'; + } + + @override + String get youCannotPostYetPlaySomeGames => '您目前不能發表文章在論壇裡,先下幾盤棋吧!'; + + @override + String get subscribe => '訂閱'; + + @override + String get unsubscribe => '取消訂閱'; + + @override + String mentionedYouInX(String param1) { + return '在「$param1」中提到了您。'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 在「$param2」中提到了您。'; + } + + @override + String invitedYouToX(String param1) { + return '邀請您至「$param1」。'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 邀請您至「$param2」。'; + } + + @override + String get youAreNowPartOfTeam => '您現在是團隊的成員了。'; + + @override + String youHaveJoinedTeamX(String param1) { + return '您已加入「$param1」。'; + } + + @override + String get someoneYouReportedWasBanned => '您檢舉的玩家已被封鎖帳號'; + + @override + String get congratsYouWon => '恭喜,您贏了!'; + + @override + String gameVsX(String param1) { + return '與$param1對局'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => '你輸給了違反了服務條款的棋手'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return '退回 $param1 $param2 等級分。'; + } + + @override + String get timeAlmostUp => '時間快到了!'; + + @override + String get clickToRevealEmailAddress => '[點擊以顯示電子郵件]'; + + @override + String get download => '下載'; + + @override + String get coachManager => '教練管理'; + + @override + String get streamerManager => '直播管理'; + + @override + String get cancelTournament => '取消錦標賽'; + + @override + String get tournDescription => '錦標賽敘述'; + + @override + String get tournDescriptionHelp => '有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用 Markdown 網址 [name](https://url)。'; + + @override + String get ratedFormHelp => '比賽為積分賽\n會影響到棋手的積分'; + + @override + String get onlyMembersOfTeam => '只限隊員'; + + @override + String get noRestriction => '沒有限制'; + + @override + String get minimumRatedGames => '評分局遊玩次數下限'; + + @override + String get minimumRating => '評分下限'; + + @override + String get maximumWeeklyRating => '每週最高評分'; + + @override + String positionInputHelp(String param) { + return '將一個有效的 FEN 貼上於此作為所有對局的起始位置。\n僅適用於標準西洋棋,對變種無效。\n你可以試用 $param 來生成 FEN,然後將其貼上到這裡。\n置空表示以預設位置開始比賽。'; + } + + @override + String get cancelSimul => '取消車輪戰'; + + @override + String get simulHostcolor => '主持所執方'; + + @override + String get estimatedStart => '預計開始時間'; + + @override + String simulFeatured(String param) { + return '展示在 $param'; + } + + @override + String simulFeaturedHelp(String param) { + return '在 $param 上向所有人展示您主持的車輪戰,對私人車輪戰無效。'; + } + + @override + String get simulDescription => '車輪戰描述'; + + @override + String get simulDescriptionHelp => '有甚麼要告訴參賽者的嗎?'; + + @override + String markdownAvailable(String param) { + return '$param 可用於更高級的格式。'; + } + + @override + String get embedsAvailable => '貼上對局或學習章節網址來嵌入。'; + + @override + String get inYourLocalTimezone => '在您的時區內'; + + @override + String get tournChat => '錦標賽聊天室'; + + @override + String get noChat => '無聊天室'; + + @override + String get onlyTeamLeaders => '僅限各隊隊長'; + + @override + String get onlyTeamMembers => '僅限各隊伍'; + + @override + String get navigateMoveTree => '定位'; + + @override + String get mouseTricks => '滑鼠功能'; + + @override + String get toggleLocalAnalysis => '切換本地計算機分析'; + + @override + String get toggleAllAnalysis => '切換所有(本地+服務器) 的電腦分析'; + + @override + String get playComputerMove => '走電腦推薦的最佳著法'; + + @override + String get analysisOptions => '分析局面'; + + @override + String get focusChat => '聚焦聊天'; + + @override + String get showHelpDialog => '顯示此說明欄'; + + @override + String get reopenYourAccount => '重新開啟帳戶'; + + @override + String get closedAccountChangedMind => '如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。'; + + @override + String get onlyWorksOnce => '這只能復原一次。'; + + @override + String get cantDoThisTwice => '如果你決定再次停用你的帳號,則不會有任何方式去復原。'; + + @override + String get emailAssociatedToaccount => '和此帳號相關的電子信箱'; + + @override + String get sentEmailWithLink => '我們已將網址寄送至你的信箱'; + + @override + String get tournamentEntryCode => '錦標賽參賽碼'; + + @override + String get hangOn => '等一下!'; + + @override + String gameInProgress(String param) { + return '您正在與 $param 進行對局。'; + } + + @override + String get abortTheGame => '中止本局'; + + @override + String get resignTheGame => '認輸'; + + @override + String get youCantStartNewGame => '直到當下這局下完之前,你無法開始新的棋局'; + + @override + String get since => '自'; + + @override + String get until => '直到'; + + @override + String get lichessDbExplanation => '來自 Lichess 用戶的所有評分遊戲'; + + @override + String get switchSides => '更換所持顏色'; + + @override + String get closingAccountWithdrawAppeal => '關閉帳戶將會收回你的上訴'; + + @override + String get ourEventTips => '舉辦賽事的小建議'; + + @override + String get instructions => '說明'; + + @override + String get showMeEverything => '全部顯示'; + + @override + String get lichessPatronInfo => 'Lichess是個慈善、完全免費且開源的軟體。\n一切營運成本、開發和內容皆來自用戶之捐贈。'; + + @override + String get nothingToSeeHere => '目前這裡沒有什麼好看的。'; + + @override + String get stats => '統計'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '您的對手已經離開了遊戲。您將在 $count 秒後獲勝。', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在$count步內將死對手', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次漏著', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次失誤', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次輕微失誤', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count位棋手目前在線', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '查看所有$count盤棋', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$param2 場對局後的 $count 等級分', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個收藏', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count天', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count小時', + ); + return '$_temp0'; + } + + @override + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分鐘', + ); + return '$_temp0'; + } + + @override + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '評分每 $count 分鐘更新一次', + ); + return '$_temp0'; + } + + @override + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個題目', + ); + return '$_temp0'; + } + + @override + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '與您下過$count盤棋', + ); + return '$_temp0'; + } + + @override + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 場排位賽', + ); + return '$_temp0'; + } + + @override + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局勝', + ); + return '$_temp0'; + } + + @override + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局負', + ); + return '$_temp0'; + } + + @override + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局和', + ); + return '$_temp0'; + } + + @override + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count下棋中', + ); + return '$_temp0'; + } + + @override + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '給對方加$count秒', + ); + return '$_temp0'; + } + + @override + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count錦標賽得分', + ); + return '$_temp0'; + } + + @override + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個研究', + ); + return '$_temp0'; + } + + @override + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 個進行的車輪戰棋局', + ); + return '$_temp0'; + } + + @override + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '完成至少 $count 場排位賽', + ); + return '$_temp0'; + } + + @override + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 排位賽', + ); + return '$_temp0'; + } + + @override + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '你需要再下 $count 局 $param2 變體的排位賽', + ); + return '$_temp0'; + } + + @override + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '您需要再完成 $count 場排位賽', + ); + return '$_temp0'; + } + + @override + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '已導入$count盤棋局', + ); + return '$_temp0'; + } + + @override + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 位好友在線', + ); + return '$_temp0'; + } + + @override + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個關注者', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '關注$count人', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '小於$count分鐘', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count場對局正在進行中', + ); + return '$_temp0'; + } + + @override + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '最多包含 $count 個字符', + ); + return '$_temp0'; + } + + @override + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count位黑名單使用者', + ); + return '$_temp0'; + } + + @override + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個論壇貼文', + ); + return '$_temp0'; + } + + @override + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '本周$count位棋手下了$param2模式的棋局', + ); + return '$_temp0'; + } + + @override + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '支援$count種語言!', + ); + return '$_temp0'; + } + + @override + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在$count秒前須下出第一步', + ); + return '$_temp0'; + } + + @override + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 秒', + ); + return '$_temp0'; + } + + @override + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '以省略$count個預走的棋步', + ); + return '$_temp0'; + } + + @override + String get stormMoveToStart => '移動以開始'; + + @override + String get stormYouPlayTheWhitePiecesInAllPuzzles => '您將在所有謎題中執白'; + + @override + String get stormYouPlayTheBlackPiecesInAllPuzzles => '您將在所有謎題中執黑'; + + @override + String get stormPuzzlesSolved => '已解決題目!'; + + @override + String get stormNewDailyHighscore => '新的每日紀錄!'; + + @override + String get stormNewWeeklyHighscore => '新的每周紀錄!'; + + @override + String get stormNewMonthlyHighscore => '新的每月紀錄!'; + + @override + String get stormNewAllTimeHighscore => '新歷史紀錄!'; + + @override + String stormPreviousHighscoreWasX(String param) { + return '之前的最高紀錄:$param'; + } + + @override + String get stormPlayAgain => '再玩一次'; + + @override + String stormHighscoreX(String param) { + return '最高紀錄:$param'; + } + + @override + String get stormScore => '得分'; + + @override + String get stormMoves => '走棋'; + + @override + String get stormAccuracy => '精準度'; + + @override + String get stormCombo => '連擊'; + + @override + String get stormTime => '時間'; + + @override + String get stormTimePerMove => '平均走棋時間'; + + @override + String get stormHighestSolved => '最難解決的題目'; + + @override + String get stormPuzzlesPlayed => '解決過的題目'; + + @override + String get stormNewRun => '新的一輪 (快捷鍵:空白鍵)'; + + @override + String get stormEndRun => '結束此輪 (快捷鍵:Enter 鍵)'; + + @override + String get stormHighscores => '最高紀錄'; + + @override + String get stormViewBestRuns => '顯示最佳的一輪'; + + @override + String get stormBestRunOfDay => '今日最佳的一輪'; + + @override + String get stormRuns => '輪'; + + @override + String get stormGetReady => '做好準備!'; + + @override + String get stormWaitingForMorePlayers => '等待更多玩家加入...'; + + @override + String get stormRaceComplete => '完賽!'; + + @override + String get stormSpectating => '觀戰中'; + + @override + String get stormJoinTheRace => '加入競賽!'; + + @override + String get stormStartTheRace => '開始比賽'; + + @override + String stormYourRankX(String param) { + return '你的排名: $param'; + } + + @override + String get stormWaitForRematch => '等候重賽'; + + @override + String get stormNextRace => '下一場競賽'; + + @override + String get stormJoinRematch => '再來一局'; + + @override + String get stormWaitingToStart => '等待開始'; + + @override + String get stormCreateNewGame => '開始新遊戲'; + + @override + String get stormJoinPublicRace => '加入公開比賽'; + + @override + String get stormRaceYourFriends => '和好友比賽'; + + @override + String get stormSkip => '跳過'; + + @override + String get stormSkipHelp => '每場賽可略一手棋'; + + @override + String get stormSkipExplanation => '跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。'; + + @override + String get stormFailedPuzzles => '失敗的謎題'; + + @override + String get stormSlowPuzzles => '耗時謎題'; + + @override + String get stormSkippedPuzzle => '已跳過的謎題'; + + @override + String get stormThisWeek => '本星期'; + + @override + String get stormThisMonth => '本月'; + + @override + String get stormAllTime => '總計'; + + @override + String get stormClickToReload => '點擊以重新加載'; + + @override + String get stormThisRunHasExpired => '本輪已過期!'; + + @override + String get stormThisRunWasOpenedInAnotherTab => '本輪已經在另一個分頁中打開!'; + + @override + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count輪', + ); + return '$_temp0'; + } + + @override + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '玩了$count輪的$param2', + ); + return '$_temp0'; + } + + @override + String get streamerLichessStreamers => 'Lichess 實況主'; + + @override + String get studyPrivate => '私人的'; + + @override + String get studyMyStudies => '我的研究'; + + @override + String get studyStudiesIContributeTo => '我有貢獻的研究'; + + @override + String get studyMyPublicStudies => '我的公開研究'; + + @override + String get studyMyPrivateStudies => '我的私人研究'; + + @override + String get studyMyFavoriteStudies => '我最愛的研究'; + + @override + String get studyWhatAreStudies => '研究是什麼?'; + + @override + String get studyAllStudies => '所有研究'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param創建的研究'; + } + + @override + String get studyNoneYet => '暫時沒有...'; + + @override + String get studyHot => '熱門的'; + + @override + String get studyDateAddedNewest => '新增日期(由新到舊)'; + + @override + String get studyDateAddedOldest => '新增日期(由舊到新)'; + + @override + String get studyRecentlyUpdated => '最近更新'; + + @override + String get studyMostPopular => '最受歡迎'; + + @override + String get studyAlphabetical => '按字母順序'; + + @override + String get studyAddNewChapter => '加入新章節'; + + @override + String get studyAddMembers => '新增成員'; + + @override + String get studyInviteToTheStudy => '邀請加入研究'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '只邀請你所認識的人,以及願意積極投入的人來共同研究'; + + @override + String get studySearchByUsername => '透過使用者名稱搜尋'; + + @override + String get studySpectator => '觀眾'; + + @override + String get studyContributor => '共同研究者'; + + @override + String get studyKick => '踢出'; + + @override + String get studyLeaveTheStudy => '退出研究'; + + @override + String get studyYouAreNowAContributor => '你現在是一位研究者了'; + + @override + String get studyYouAreNowASpectator => '你現在是觀眾'; + + @override + String get studyPgnTags => 'PGN 標籤'; + + @override + String get studyLike => '喜歡'; + + @override + String get studyUnlike => '取消喜歡'; + + @override + String get studyNewTag => '新標籤'; + + @override + String get studyCommentThisPosition => '對於目前局面的評論'; + + @override + String get studyCommentThisMove => '對於此棋步的評論'; + + @override + String get studyAnnotateWithGlyphs => '以圖形標註'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '因為太短,所以此章節無法被分析'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => '只有研究專案編輯者才能要求電腦分析'; + + @override + String get studyGetAFullComputerAnalysis => '請求伺服器完整的分析主要走法'; + + @override + String get studyMakeSureTheChapterIsComplete => '確認此章節已完成,您只能要求分析一次'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '所有的SYNC成員處於相同局面'; + + @override + String get studyShareChanges => '向旁觀者分享這些變動並將其保留在伺服器中'; + + @override + String get studyPlaying => '下棋中'; + + @override + String get studyShowEvalBar => '評估條'; + + @override + String get studyFirst => '第一頁'; + + @override + String get studyPrevious => '上一頁'; + + @override + String get studyNext => '下一頁'; + + @override + String get studyLast => '最後一頁'; + + @override + String get studyShareAndExport => '分享 & 導出'; + + @override + String get studyCloneStudy => '複製'; + + @override + String get studyStudyPgn => '研究 PGN'; + + @override + String get studyDownloadAllGames => '下載所有棋局'; + + @override + String get studyChapterPgn => '章節PGN'; + + @override + String get studyCopyChapterPgn => '複製PGN'; + + @override + String get studyDownloadGame => '下載棋局'; + + @override + String get studyStudyUrl => '研究連結'; + + @override + String get studyCurrentChapterUrl => '目前章節連結'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => '您可以將此複製到論壇以嵌入'; + + @override + String get studyStartAtInitialPosition => '從起始局面開始'; + + @override + String studyStartAtX(String param) { + return '從$param開始'; + } + + @override + String get studyEmbedInYourWebsite => '嵌入到您的網站或部落格'; + + @override + String get studyReadMoreAboutEmbedding => '閱讀更多與嵌入有關的內容'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '只有公開的研究可以嵌入!'; + + @override + String get studyOpen => '打開'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1,由$param2提供'; + } + + @override + String get studyStudyNotFound => '找無此研究'; + + @override + String get studyEditChapter => '編輯章節'; + + @override + String get studyNewChapter => '建立新章節'; + + @override + String studyImportFromChapterX(String param) { + return '從 $param 導入'; + } + + @override + String get studyOrientation => '視角'; + + @override + String get studyAnalysisMode => '分析模式'; + + @override + String get studyPinnedChapterComment => '置頂留言'; + + @override + String get studySaveChapter => '儲存章節'; + + @override + String get studyClearAnnotations => '清除註記'; + + @override + String get studyClearVariations => '清除變化'; + + @override + String get studyDeleteChapter => '刪除章節'; + + @override + String get studyDeleteThisChapter => '刪除此章節? 此動作將無法取消!'; + + @override + String get studyClearAllCommentsInThisChapter => '清除此章節中的所有註釋和圖形嗎?'; + + @override + String get studyRightUnderTheBoard => '棋盤下方'; + + @override + String get studyNoPinnedComment => '無'; + + @override + String get studyNormalAnalysis => '一般分析'; + + @override + String get studyHideNextMoves => '隱藏下一步'; + + @override + String get studyInteractiveLesson => '互動課程'; + + @override + String studyChapterX(String param) { + return '章節$param'; + } + + @override + String get studyEmpty => '空的'; + + @override + String get studyStartFromInitialPosition => '從起始局面開始'; + + @override + String get studyEditor => '編輯器'; + + @override + String get studyStartFromCustomPosition => '從自定的局面開始'; + + @override + String get studyLoadAGameByUrl => '以連結導入棋局'; + + @override + String get studyLoadAPositionFromFen => '透過FEN讀取局面'; + + @override + String get studyLoadAGameFromPgn => '以PGN文件導入棋局'; + + @override + String get studyAutomatic => '自動'; + + @override + String get studyUrlOfTheGame => '棋局連結,一行一個'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '從$param1或$param2載入棋局'; + } + + @override + String get studyCreateChapter => '建立章節'; + + @override + String get studyCreateStudy => '建立研究'; + + @override + String get studyEditStudy => '編輯此研究'; + + @override + String get studyVisibility => '權限'; + + @override + String get studyPublic => '公開的'; + + @override + String get studyUnlisted => '不公開'; + + @override + String get studyInviteOnly => '僅限邀請'; + + @override + String get studyAllowCloning => '可以複製'; + + @override + String get studyNobody => '没有人'; + + @override + String get studyOnlyMe => '僅自己'; + + @override + String get studyContributors => '貢獻者'; + + @override + String get studyMembers => '成員'; + + @override + String get studyEveryone => '所有人'; + + @override + String get studyEnableSync => '允許同步'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => '同步:讓所有人停留在同一個局面'; + + @override + String get studyNoLetPeopleBrowseFreely => '不同步:允許所有人自由進行瀏覽'; + + @override + String get studyPinnedStudyComment => '置頂研究留言'; + + @override + String get studyStart => '開始'; + + @override + String get studySave => '存檔'; + + @override + String get studyClearChat => '清空對話紀錄'; + + @override + String get studyDeleteTheStudyChatHistory => '確定要清空課程對話紀錄嗎?此操作無法還原!'; + + @override + String get studyDeleteStudy => '刪除此研究'; + + @override + String studyConfirmDeleteStudy(String param) { + return '你確定要刪除整個研究?此動作無法反悔。輸入研究名稱確認:$param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => '要從哪裡開始研究呢?'; + + @override + String get studyGoodMove => '好棋'; + + @override + String get studyMistake => '失誤'; + + @override + String get studyBrilliantMove => '妙着'; + + @override + String get studyBlunder => '嚴重失誤'; + + @override + String get studyInterestingMove => '有趣的一着'; + + @override + String get studyDubiousMove => '值得商榷的一着'; + + @override + String get studyOnlyMove => '唯一著法'; + + @override + String get studyZugzwang => '等著'; + + @override + String get studyEqualPosition => '勢均力敵'; + + @override + String get studyUnclearPosition => '局勢不明'; + + @override + String get studyWhiteIsSlightlyBetter => '白方稍占優勢'; + + @override + String get studyBlackIsSlightlyBetter => '黑方稍占優勢'; + + @override + String get studyWhiteIsBetter => '白方占優勢'; + + @override + String get studyBlackIsBetter => '黑方占優勢'; + + @override + String get studyWhiteIsWinning => '白方要取得勝利了'; + + @override + String get studyBlackIsWinning => '黑方要取得勝利了'; + + @override + String get studyNovelty => '新奇的'; + + @override + String get studyDevelopment => '發展'; + + @override + String get studyInitiative => '佔據主動'; + + @override + String get studyAttack => '攻擊'; + + @override + String get studyCounterplay => '反擊'; + + @override + String get studyTimeTrouble => '時間壓力'; + + @override + String get studyWithCompensation => '優勢補償'; + + @override + String get studyWithTheIdea => '教科書式的'; + + @override + String get studyNextChapter => '下一章'; + + @override + String get studyPrevChapter => '上一章'; + + @override + String get studyStudyActions => '研討操作'; + + @override + String get studyTopics => '主題'; + + @override + String get studyMyTopics => '我的主題'; + + @override + String get studyPopularTopics => '熱門主題'; + + @override + String get studyManageTopics => '管理主題'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count局和', - ); - return '$_temp0'; - } + String get studyBack => '返回'; @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count下棋中', - ); - return '$_temp0'; - } + String get studyPlayAgain => '再玩一次'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '給對方加$count秒', - ); - return '$_temp0'; - } + String get studyWhatWouldYouPlay => '你會在這個位置上怎麼走?'; @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count錦標賽得分', - ); - return '$_temp0'; - } + String get studyYouCompletedThisLesson => '恭喜!您完成了這個課程。'; @override - String nbStudies(int count) { + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count個研究', + other: '第$count章', ); return '$_temp0'; } @override - String nbSimuls(int count) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 個進行的車輪戰棋局', + other: '$count對局', ); return '$_temp0'; } @override - String moreThanNbRatedGames(int count) { + String studyNbMembers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '完成至少 $count 場排位賽', + other: '$count位成員', ); return '$_temp0'; } @override - String moreThanNbPerfRatedGames(int count, String param2) { + String studyPasteYourPgnTextHereUpToNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count $param2 排位賽', + other: '在此貼上PGN文本,最多可導入$count個棋局', ); return '$_temp0'; } @override - String needNbMorePerfGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '你需要再下 $count 局 $param2 變體的排位賽', - ); - return '$_temp0'; - } + String get timeagoJustNow => '剛剛'; @override - String needNbMoreGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '您需要再完成 $count 場排位賽', - ); - return '$_temp0'; - } + String get timeagoRightNow => '現在'; @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '已導入$count盤棋局', - ); - return '$_temp0'; - } + String get timeagoCompleted => '已結束'; @override - String nbFriendsOnline(int count) { + String timeagoInNbSeconds(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 位好友在線', + other: '$count秒後', ); return '$_temp0'; } @override - String nbFollowers(int count) { + String timeagoInNbMinutes(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count個關注者', + other: '$count分後', ); return '$_temp0'; } @override - String nbFollowing(int count) { + String timeagoInNbHours(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '關注$count人', + other: '$count小時後', ); return '$_temp0'; } @override - String lessThanNbMinutes(int count) { + String timeagoInNbDays(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '小於$count分鐘', + other: '$count天後', ); return '$_temp0'; } @override - String nbGamesInPlay(int count) { + String timeagoInNbWeeks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count場對局正在進行中', + other: '$count週後', ); return '$_temp0'; } @override - String maximumNbCharacters(int count) { + String timeagoInNbMonths(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '最多$count个字符', + other: '$count個月後', ); return '$_temp0'; } @override - String blocks(int count) { + String timeagoInNbYears(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count位黑名单用户', + other: '$count年後', ); return '$_temp0'; } @override - String nbForumPosts(int count) { + String timeagoNbMinutesAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count個論壇貼文', + other: '$count分前', ); return '$_temp0'; } @override - String nbPerfTypePlayersThisWeek(int count, String param2) { + String timeagoNbHoursAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '本周$count位棋手下了$param2模式的棋局', + other: '$count小時前', ); return '$_temp0'; } @override - String availableInNbLanguages(int count) { + String timeagoNbDaysAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '支援$count種語言!', + other: '$count天前', ); return '$_temp0'; } @override - String nbSecondsToPlayTheFirstMove(int count) { + String timeagoNbWeeksAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '在$count秒前須下出第一步', + other: '$count週前', ); return '$_temp0'; } @override - String nbSeconds(int count) { + String timeagoNbMonthsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 秒', + other: '$count個月前', ); return '$_temp0'; } @override - String andSaveNbPremoveLines(int count) { + String timeagoNbYearsAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '以儲存$count列預走的棋步', + other: '$count年前', ); return '$_temp0'; } @override - String get stormMoveToStart => '移動以開始'; - - @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => '您將在所有謎題中執白'; - - @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => '您將在所有謎題中執黑'; - - @override - String get stormPuzzlesSolved => '已解決題目!'; - - @override - String get stormNewDailyHighscore => '新的每日紀錄!'; - - @override - String get stormNewWeeklyHighscore => '新的每周紀錄!'; - - @override - String get stormNewMonthlyHighscore => '新的每月紀錄!'; - - @override - String get stormNewAllTimeHighscore => '新歷史紀錄!'; - - @override - String stormPreviousHighscoreWasX(String param) { - return '之前的紀錄:$param'; - } - - @override - String get stormPlayAgain => '再玩一次'; - - @override - String stormHighscoreX(String param) { - return '最高紀錄:$param'; - } - - @override - String get stormScore => '得分'; - - @override - String get stormMoves => '走棋'; - - @override - String get stormAccuracy => '精準度'; - - @override - String get stormCombo => '連擊'; - - @override - String get stormTime => '時間'; - - @override - String get stormTimePerMove => '平均走棋時間'; - - @override - String get stormHighestSolved => '最難解決的題目'; - - @override - String get stormPuzzlesPlayed => '解決過的題目'; - - @override - String get stormNewRun => '新的一輪 (快捷鍵:空白鍵)'; - - @override - String get stormEndRun => '結束此輪 (快捷鍵:Enter鍵)'; - - @override - String get stormHighscores => '最高紀錄'; - - @override - String get stormViewBestRuns => '顯示最佳的一輪'; - - @override - String get stormBestRunOfDay => '今日最佳的一輪'; - - @override - String get stormRuns => '輪'; - - @override - String get stormGetReady => '做好準備!'; - - @override - String get stormWaitingForMorePlayers => '等待更多玩家加入...'; - - @override - String get stormRaceComplete => '完賽!'; - - @override - String get stormSpectating => '觀戰中'; - - @override - String get stormJoinTheRace => '加入競賽!'; - - @override - String get stormStartTheRace => '開始比賽'; - - @override - String stormYourRankX(String param) { - return '你的排名: $param'; - } - - @override - String get stormWaitForRematch => '等候重賽'; - - @override - String get stormNextRace => '下一場競賽'; - - @override - String get stormJoinRematch => '再來一局'; - - @override - String get stormWaitingToStart => '等待開始'; - - @override - String get stormCreateNewGame => '開始新遊戲'; - - @override - String get stormJoinPublicRace => '加入公開比賽'; - - @override - String get stormRaceYourFriends => '和好友比賽'; - - @override - String get stormSkip => '跳過'; - - @override - String get stormSkipHelp => '每場賽可略一手棋'; - - @override - String get stormSkipExplanation => '跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。'; - - @override - String get stormFailedPuzzles => '失敗了的謎題'; - - @override - String get stormSlowPuzzles => '慢 謎題'; - - @override - String get stormThisWeek => '本星期'; - - @override - String get stormThisMonth => '本月'; - - @override - String get stormAllTime => '總計'; - - @override - String get stormClickToReload => '點擊以重新加載'; - - @override - String get stormThisRunHasExpired => '本次比賽已過期!'; - - @override - String get stormThisRunWasOpenedInAnotherTab => '本次沖刺已經在另一個標籤頁中打開!'; - - @override - String stormXRuns(int count) { + String timeagoNbMinutesRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count輪', + other: '剩下 $count 分鐘', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String timeagoNbHoursRemaining(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '玩了$count輪的$param2', + other: '剩下 $count 小時', ); return '$_temp0'; } - - @override - String get streamerLichessStreamers => 'Lichess實況主'; - - @override - String get studyShareAndExport => '分享 & 導出'; - - @override - String get studyStart => '開始'; } diff --git a/lib/l10n/lila_af.arb b/lib/l10n/lila_af.arb index 8f6f276407..8fb26c6a3b 100644 --- a/lib/l10n/lila_af.arb +++ b/lib/l10n/lila_af.arb @@ -1,40 +1,41 @@ { + "mobileAllGames": "Alle spelle", + "mobileAreYouSure": "Is jy seker?", + "mobileBlindfoldMode": "Geblinddoek", + "mobileCorrespondenceClearSavedMove": "Vee gestoorde skuif uit", + "mobileCustomGameJoinAGame": "Sluit aan by 'n spel", + "mobileFeedbackButton": "Terugvoer", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Verberg variasie", "mobileHomeTab": "Tuis", - "mobilePuzzlesTab": "Kopkrappers", - "mobileToolsTab": "Hulpmiddels", - "mobileWatchTab": "Hou dop", - "mobileSettingsTab": "Instellings", "mobileMustBeLoggedIn": "Jy moet ingeteken wees om hierdie bladsy te kan sien.", - "mobileSystemColors": "Stelselkleure", - "mobileFeedbackButton": "Terugvoer", + "mobileNoSearchResults": "Geen resultate nie", + "mobileNotFollowingAnyUser": "Jy volg nie enige gebruikers nie.", "mobileOkButton": "Reg", + "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Vergroot gesleepte stuk", + "mobilePuzzleStormConfirmEndRun": "Wil jy hierdie lopie beëindig?", + "mobilePuzzleStormFilterNothingToShow": "Niks om te wys nie; verander asb. die filters", + "mobilePuzzleStormSubtitle": "Los soveel kopkrappers moontlik op in 3 minute.", + "mobilePuzzleThemesSubtitle": "Doen kopkrappers van jou gunstelingopenings, of kies 'n tema.", + "mobilePuzzlesTab": "Kopkrappers", + "mobileRecentSearches": "Onlangse soektogte", "mobileSettingsHapticFeedback": "Vibrasieterugvoer", "mobileSettingsImmersiveMode": "Volskermmodus", - "mobileNotFollowingAnyUser": "Jy volg nie enige gebruikers nie.", - "mobileAllGames": "Alle spelle", - "mobileRecentSearches": "Onlangse soektogte", - "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", - "mobileNoSearchResults": "Geen resultate nie", - "mobileAreYouSure": "Is jy seker?", - "mobileSharePuzzle": "Deel hierdie kopkrapper", - "mobileShareGameURL": "Deel spel se bronadres", + "mobileSettingsTab": "Instellings", "mobileShareGamePGN": "Deel PGN", + "mobileShareGameURL": "Deel spel se bronadres", "mobileSharePositionAsFEN": "Deel posisie as FEN", - "mobileShowVariations": "Wys variasies", - "mobileHideVariation": "Verberg variasie", + "mobileSharePuzzle": "Deel hierdie kopkrapper", "mobileShowComments": "Wys kommentaar", - "mobilePuzzleStormConfirmEndRun": "Wil jy hierdie lopie beëindig?", - "mobilePuzzleStormFilterNothingToShow": "Niks om te wys nie; verander asb. die filters", - "mobileWaitingForOpponentToJoin": "Wag vir opponent om aan te sluit...", - "mobileBlindfoldMode": "Geblinddoek", - "mobileCustomGameJoinAGame": "Sluit aan by 'n spel", - "mobileCorrespondenceClearSavedMove": "Vee gestoorde skuif uit", - "mobileSomethingWentWrong": "Iets het skeefgeloop.", "mobileShowResult": "Wys resultaat", - "mobilePuzzleThemesSubtitle": "Doen kopkrappers van jou gunstelingopenings, of kies 'n tema.", - "mobilePuzzleStormSubtitle": "Los soveel kopkrappers moontlik op in 3 minute.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", + "mobileShowVariations": "Wys variasies", + "mobileSomethingWentWrong": "Iets het skeefgeloop.", + "mobileSystemColors": "Stelselkleure", + "mobileToolsTab": "Hulpmiddels", + "mobileWaitingForOpponentToJoin": "Wag vir opponent om aan te sluit...", + "mobileWatchTab": "Hou dop", "activityActivity": "Aktiwiteite", "activityHostedALiveStream": "Het 'n lewendige uitsending aangebied", "activityRankedInSwissTournament": "Rang van #{param1} uit {param2}", @@ -57,7 +58,40 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Het aan {count} swiss toernooi deelgeneem} other{Het aan {count} swiss toernooie deelgeneem}}", "activityJoinedNbTeams": "{count, plural, =1{Het aangesluit by {count} span} other{Het aangesluit by {count} spanne}}", "broadcastBroadcasts": "Uitsendings", + "broadcastMyBroadcasts": "My uitsendings", "broadcastLiveBroadcasts": "Regstreekse toernooi uitsendings", + "broadcastNewBroadcast": "Nuwe regstreekse uitsendings", + "broadcastAddRound": "Voeg 'n ronde by", + "broadcastOngoing": "Deurlopend", + "broadcastUpcoming": "Opkomend", + "broadcastCompleted": "Voltooi", + "broadcastRoundName": "Ronde se naam", + "broadcastRoundNumber": "Ronde getal", + "broadcastTournamentName": "Toernooi se naam", + "broadcastTournamentDescription": "Kort beskrywing van die toernooi", + "broadcastFullDescription": "Volle geleentheid beskrywing", + "broadcastFullDescriptionHelp": "Opsionele lang beskrywing van die uitsending. {param1} is beskikbaar. Lengte moet minder as {param2} karakters.", + "broadcastSourceSingleUrl": "PGN-Bronskakel", + "broadcastSourceUrlHelp": "URL wat Lichess sal nagaan vir PGN opdaterings. Dit moet openbaar beskikbaar wees vanaf die Internet.", + "broadcastStartDateHelp": "Optioneel, indien jy weet wanner die geleentheid begin", + "broadcastCurrentGameUrl": "Huidige spel se bronadres", + "broadcastDownloadAllRounds": "Laai al die rondes af", + "broadcastResetRound": "Herstel die ronde", + "broadcastDeleteRound": "Skrap die ronde", + "broadcastDefinitivelyDeleteRound": "Skrap die rondte en sy spelle beslis uit.", + "broadcastDeleteAllGamesOfThisRound": "Skrap alle spelle van hierdie rondte. Die bron sal aktief moet wees om hulle te kan herskep.", + "broadcastDeleteTournament": "Vee hierdie toernooi uit", + "broadcastDefinitivelyDeleteTournament": "Vee beslis die hele toernooi uit, met al sy rondtes en spelle.", + "broadcastReplacePlayerTags": "Opsioneel: vervang spelername, graderings en titels", + "broadcastFideFederations": "FIDE-federasies", + "broadcastTop10Rating": "Top 10 gradering", + "broadcastFidePlayers": "FIDE-deelnemers", + "broadcastFidePlayerNotFound": "FIDE-deelnemer nie gevind nie", + "broadcastFideProfile": "FIDE-profiel", + "broadcastFederation": "Federasie", + "broadcastAgeThisYear": "Ouderdom vanjaar", + "broadcastUnrated": "Ongegradeerd", + "broadcastRecentTournaments": "Onlangse toernooie", "challengeChallengesX": "Uitdagings: {param1}", "challengeChallengeToPlay": "Daag uit tot 'n spel", "challengeChallengeDeclined": "Uitdaging afgewys.", @@ -178,6 +212,7 @@ "preferencesNotifyWeb": "Blaaier", "preferencesNotifyDevice": "Toestel", "preferencesBellNotificationSound": "Klokkie kennisgewing klank", + "preferencesBlindfold": "Blinddoek", "puzzlePuzzles": "Raaisels", "puzzlePuzzleThemes": "Raaisel temas", "puzzleRecommended": "Aanbeveeldede", @@ -373,8 +408,8 @@ "puzzleThemeXRayAttackDescription": "'N Stuk val of verdedig 'n vierkant deur 'n vyandige stuk.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Die opponent is beperk in die bewegings wat hulle kan maak, en alle bewegings vererger hul posisie.", - "puzzleThemeHealthyMix": "Gesonde mengsel", - "puzzleThemeHealthyMixDescription": "'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.", + "puzzleThemeMix": "Gesonde mengsel", + "puzzleThemeMixDescription": "'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.", "puzzleThemePlayerGames": "Speler se spelle", "puzzleThemePlayerGamesDescription": "Beloer kopkrappers wat ontstaan van jou spelle, of van ander se spelle af.", "puzzleThemePuzzleDownloadInformation": "Die raaisels is in openbare domain en kan afgelaai word vanaf {param}.", @@ -495,7 +530,6 @@ "replayMode": "Oorspeel modus", "realtimeReplay": "Ware Tyd", "byCPL": "Met CPL", - "openStudy": "Open studie", "enable": "Aktief", "bestMoveArrow": "Beste skuif pyl", "showVariationArrows": "Wys variasiepyle", @@ -505,7 +539,6 @@ "memory": "Geheue", "infiniteAnalysis": "Oneindige analise", "removesTheDepthLimit": "Verwyder dieptelimiet, en hou jou rekenaar warm", - "engineManager": "Enjinbestuurder", "blunder": "Flater", "mistake": "Fout", "inaccuracy": "Onakkuraatheid", @@ -606,6 +639,7 @@ "abortGame": "Staak spel", "gameAborted": "Spel gestaak", "standard": "Standaard", + "customPosition": "Gebruiklike Posisie", "unlimited": "Oneindig", "mode": "Modus", "casual": "Vriendskaplik", @@ -692,6 +726,7 @@ "blackCheckmatesInOneMove": "Swart om te skaakmat in een skuif", "retry": "Probeer weer", "reconnecting": "Konnekteer weer", + "noNetwork": "Vanlyn af", "favoriteOpponents": "Gunsteling opponente", "follow": "Volg", "following": "Besig om te volg", @@ -701,7 +736,6 @@ "block": "Blokeer", "blocked": "Geblok", "unblock": "Ontblok", - "followsYou": "Volg jou", "xStartedFollowingY": "{param1} het begin om {param2} te volg", "more": "Meer", "memberSince": "Lid sedert", @@ -760,6 +794,9 @@ "ifNoneLeaveEmpty": "As geen, los oop", "profile": "Profiel", "editProfile": "Verander profiel", + "realName": "Regte naam", + "setFlair": "Stel jou Vlam", + "flair": "Vlam", "biography": "Biografie", "countryRegion": "Land of streek", "thankYou": "Dankie!", @@ -801,7 +838,6 @@ "cheat": "Kul", "troll": "Boelie", "other": "Ander", - "reportDescriptionHelp": "Plak skakel na die spel(le) en verduidelik wat skort met die lid se gedrag. Moenie net sê hulle kroek nie, maar verduidelik hoe daardie gevolgtrekking bereik is. Jou verslag sal vinniger geantwoord word as dit in Engels geskryf is.", "error_provideOneCheatedGameLink": "Verskaf asseblief ten minste een skakel na 'n spel waar hulle gekroek het.", "by": "deur {param}", "importedByX": "Ingevoer deur {param}", @@ -1212,6 +1248,7 @@ "giveNbSeconds": "{count, plural, =1{Gee {count} sekonde} other{Gee {count} sekondes}}", "nbTournamentPoints": "{count, plural, =1{{count} toernooipunt} other{{count} toernooipunte}}", "nbStudies": "{count, plural, =1{{count} studie} other{{count} studies}}", + "nbSimuls": "{count, plural, =1{{count} simulasie} other{{count} simulasies}}", "moreThanNbRatedGames": "{count, plural, =1{≥ {count} gegradeerde spel} other{≥ {count} gegradeerde spelle}}", "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} {param2} gegradeerde spel} other{≥ {count} {param2} gegradeerde spelle}}", "needNbMorePerfGames": "{count, plural, =1{Jy het nodig om {count} meer gegradeerde {param2} spel te speel} other{Jy het nodig om {count} meer gegradeerde {param2} spelle te speel}}", @@ -1284,6 +1321,176 @@ "stormXRuns": "{count, plural, =1{1 lopie} other{{count} lopies}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Speel een lopie van {param2}} other{Speel {count} lopies van {param2}}}", "streamerLichessStreamers": "Lichess aanbieders", + "studyPrivate": "Privaat", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies waartoe ek bydra", + "studyMyPublicStudies": "My publieke studies", + "studyMyPrivateStudies": "My privaat studies", + "studyMyFavoriteStudies": "My gunsteling studies", + "studyWhatAreStudies": "Wat is studies?", + "studyAllStudies": "Alle studies", + "studyStudiesCreatedByX": "Studies gemaak deur {param}", + "studyNoneYet": "Nog geen.", + "studyHot": "Gewild", + "studyDateAddedNewest": "Datum bygevoeg (nuutste)", + "studyDateAddedOldest": "Datum bygevoeg (oudste)", + "studyRecentlyUpdated": "Onlangs opgedateer", + "studyMostPopular": "Mees gewilde", + "studyAlphabetical": "Alfabeties", + "studyAddNewChapter": "Voeg 'n nuwe hoofstuk by", + "studyAddMembers": "Voeg iemand by", + "studyInviteToTheStudy": "Nooi uit om deel te wees van die studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Nooi asseblief net mense uit wat jy ken of wat aktief wil deelneem aan die studie.", + "studySearchByUsername": "Soek vir gebruikersnaam", + "studySpectator": "Toeskouer", + "studyContributor": "Bydraer", + "studyKick": "Verwyder", + "studyLeaveTheStudy": "Verlaat die studie", + "studyYouAreNowAContributor": "Jy is nou 'n bydraer", + "studyYouAreNowASpectator": "Jy is nou 'n toeskouer", + "studyPgnTags": "PGN etikette", + "studyLike": "Hou van", + "studyUnlike": "Afkeur", + "studyNewTag": "Nuwe etiket", + "studyCommentThisPosition": "Lewer kommentaar op hierdie posisie", + "studyCommentThisMove": "Lewer kommentaar op hierdie skuif", + "studyAnnotateWithGlyphs": "Annoteer met karakters", + "studyTheChapterIsTooShortToBeAnalysed": "Die hoofstuk is te kort om geanaliseer te word.", + "studyOnlyContributorsCanRequestAnalysis": "Slegs die studie bydraers kan versoek om 'n rekenaar analise te doen.", + "studyGetAFullComputerAnalysis": "Kry 'n vol-bediener rekenaar analise van die hooflyn.", + "studyMakeSureTheChapterIsComplete": "Maak seker dat die hoofstuk volledig is. Jy kan slegs eenkeer 'n analise versoek.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC lede bly op dieselfde posisie", + "studyShareChanges": "Deel veranderinge met toeskouers en stoor dit op die bediener", + "studyPlaying": "Besig om te speel", + "studyFirst": "Eerste", + "studyPrevious": "Vorige", + "studyNext": "Volgende", + "studyLast": "Laaste", "studyShareAndExport": "Deel & voer uit", - "studyStart": "Begin" + "studyCloneStudy": "Kloneer", + "studyStudyPgn": "Studie PGN", + "studyDownloadAllGames": "Laai alle speletjies af", + "studyChapterPgn": "Hoofstuk PGN", + "studyCopyChapterPgn": "Kopieer PGN", + "studyDownloadGame": "Aflaai spel", + "studyStudyUrl": "Bestudeer URL", + "studyCurrentChapterUrl": "Huidige hoofstuk URL", + "studyYouCanPasteThisInTheForumToEmbed": "U kan dit in die forum plak om in te bed", + "studyStartAtInitialPosition": "Begin by die oorspronklike posisie", + "studyStartAtX": "Begin by {param}", + "studyEmbedInYourWebsite": "Bed in u webwerf of blog", + "studyReadMoreAboutEmbedding": "Lees meer oor inbedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Slegs openbare studies kan ingebed word!", + "studyOpen": "Maak oop", + "studyXBroughtToYouByY": "{param1}, aan jou beskikbaar gestel deur {param2}", + "studyStudyNotFound": "Studie kon nie gevind word nie", + "studyEditChapter": "Verander die hoofstuk", + "studyNewChapter": "Nuwe hoofstuk", + "studyImportFromChapterX": "Voer in vanaf {param}", + "studyOrientation": "Oriëntasie", + "studyAnalysisMode": "Analiseer mode", + "studyPinnedChapterComment": "Vasgepende hoofstuk kommentaar", + "studySaveChapter": "Stoor hoofstuk", + "studyClearAnnotations": "Vee annotasies uit", + "studyClearVariations": "Verwyder variasies", + "studyDeleteChapter": "Vee hoofstuk uit", + "studyDeleteThisChapter": "Vee die hoofstuk uit? Jy gaan dit nie kan terugvat nie!", + "studyClearAllCommentsInThisChapter": "Vee al die kommentaar, karakters en getekende vorms in die hoofstuk uit?", + "studyRightUnderTheBoard": "Reg onder die bord", + "studyNoPinnedComment": "Geen", + "studyNormalAnalysis": "Normale analise", + "studyHideNextMoves": "Versteek die volgende skuiwe", + "studyInteractiveLesson": "Interaktiewe les", + "studyChapterX": "Hoofstuk {param}", + "studyEmpty": "Leeg", + "studyStartFromInitialPosition": "Begin vanaf oorspronklike posisie", + "studyEditor": "Redakteur", + "studyStartFromCustomPosition": "Begin vanaf eie posisie", + "studyLoadAGameByUrl": "Laai 'n wedstryd op deur die URL", + "studyLoadAPositionFromFen": "Laai posisie vanaf FEN", + "studyLoadAGameFromPgn": "Laai wedstryd vanaf PGN", + "studyAutomatic": "Outomaties", + "studyUrlOfTheGame": "URL van die wedstryd", + "studyLoadAGameFromXOrY": "Laai 'n wedstryd van {param1} of {param2}", + "studyCreateChapter": "Skep 'n hoofstuk", + "studyCreateStudy": "Skep 'n studie", + "studyEditStudy": "Verander studie", + "studyVisibility": "Sigbaarheid", + "studyPublic": "Publiek", + "studyUnlisted": "Ongelys", + "studyInviteOnly": "Slegs op uitnodiging", + "studyAllowCloning": "Laat kloning toe", + "studyNobody": "Niemand", + "studyOnlyMe": "Net ek", + "studyContributors": "Bydraers", + "studyMembers": "Lede", + "studyEveryone": "Almal", + "studyEnableSync": "Maak sync beskikbaar", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: hou almal op dieselfde posisie", + "studyNoLetPeopleBrowseFreely": "Nee: laat mense toe om vrylik deur te gaan", + "studyPinnedStudyComment": "Vasgepende studie opmerking", + "studyStart": "Begin", + "studySave": "Stoor", + "studyClearChat": "Maak die gesprek skoon", + "studyDeleteTheStudyChatHistory": "Vee die gesprek uit? Onthou, jy kan dit nie terug kry nie!", + "studyDeleteStudy": "Vee die studie uit", + "studyConfirmDeleteStudy": "Skrap die hele studie? Daar is geen terugkeer nie! Tik die naam van die studie om te bevesting: {param}", + "studyWhereDoYouWantToStudyThat": "Waar wil jy dit bestudeer?", + "studyGoodMove": "Goeie skuif", + "studyMistake": "Fout", + "studyBrilliantMove": "Skitterende skuif", + "studyBlunder": "Flater", + "studyInterestingMove": "Interesante skuif", + "studyDubiousMove": "Twyfelagte skuif", + "studyOnlyMove": "Eenigste skuif", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Gelyke posisie", + "studyUnclearPosition": "Onduidelike posise", + "studyWhiteIsSlightlyBetter": "Wit is effens beter", + "studyBlackIsSlightlyBetter": "Swart is effens beter", + "studyWhiteIsBetter": "Wit is beter", + "studyBlackIsBetter": "Swart is beter", + "studyWhiteIsWinning": "Wit is beter", + "studyBlackIsWinning": "Swart is beter", + "studyNovelty": "Nuwigheid", + "studyDevelopment": "Ontwikkeling", + "studyInitiative": "Inisiatief", + "studyAttack": "Aanval", + "studyCounterplay": "Teenstoot", + "studyTimeTrouble": "Tydskommer", + "studyWithCompensation": "Met vergoeding", + "studyWithTheIdea": "Met die idee", + "studyNextChapter": "Volgende hoofstuk", + "studyPrevChapter": "Vorige hoofstuk", + "studyStudyActions": "Studie aksie", + "studyTopics": "Onderwerpe", + "studyMyTopics": "My onderwerpe", + "studyPopularTopics": "Gewilde onderwerpe", + "studyManageTopics": "Bestuur onderwerpe", + "studyBack": "Terug", + "studyPlayAgain": "Speel weer", + "studyWhatWouldYouPlay": "Wat sal jy in hierdie posisie speel?", + "studyYouCompletedThisLesson": "Geluk! Jy het hierdie les voltooi.", + "studyNbChapters": "{count, plural, =1{{count} Hoofstuk} other{{count} Hoofstukke}}", + "studyNbGames": "{count, plural, =1{{count} Wedstryd} other{{count} Wedstryde}}", + "studyNbMembers": "{count, plural, =1{{count} Lid} other{{count} Lede}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak jou PGN teks hier, tot by {count} spel} other{Plak jou PGN teks hier, tot by {count} spelle}}", + "timeagoJustNow": "sopas", + "timeagoRightNow": "nou", + "timeagoCompleted": "voltooi", + "timeagoInNbSeconds": "{count, plural, =1{in {count} sekonde} other{in {count} sekondes}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} minuut} other{in {count} minute}}", + "timeagoInNbHours": "{count, plural, =1{in {count} uur} other{in {count} ure}}", + "timeagoInNbDays": "{count, plural, =1{in {count} dag} other{in {count} dae}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weke}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} maand} other{in {count} maande}}", + "timeagoInNbYears": "{count, plural, =1{in {count} jaar} other{in {count} jare}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuut gelede} other{{count} minute gelede}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} uur gelede} other{{count} ure gelede}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag gelede} other{{count} dae gelede}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week gelede} other{{count} weke gelede}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} maand gelede} other{{count} maande gelede}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} jaar gelede} other{{count} jare gelede}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{nog {count} minuut oor} other{nog {count} minute oor}}", + "timeagoNbHoursRemaining": "{count, plural, =1{nog {count} uur oor} other{nog {count} ure oor}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ar.arb b/lib/l10n/lila_ar.arb index 1e090d3489..70584812da 100644 --- a/lib/l10n/lila_ar.arb +++ b/lib/l10n/lila_ar.arb @@ -1,42 +1,47 @@ { - "mobileHomeTab": "الرئيسية", - "mobilePuzzlesTab": "ألغاز", - "mobileToolsTab": "أدوات", - "mobileWatchTab": "شاهد", - "mobileSettingsTab": "الإعدادات", - "mobileMustBeLoggedIn": "لعرض هذه الصفحة، قم بتسجيل الدخول.", - "mobileSystemColors": "ألوان النظام", + "mobileAllGames": "جميع الألعاب", + "mobileAreYouSure": "هل أنت واثق؟", + "mobileBlindfoldMode": "معصوب العينين", + "mobileCancelTakebackOffer": "إلغاء عرض الاسترداد", + "mobileClearButton": "مسح", + "mobileCorrespondenceClearSavedMove": "مسح النقل المحفوظ", + "mobileCustomGameJoinAGame": "الانضمام إلى لُعْبَة", "mobileFeedbackButton": "الملاحظات", + "mobileGreeting": "مرحبا، {param}", + "mobileGreetingWithoutName": "مرحبا", + "mobileHideVariation": "إخفاء سلسلة النقلات المرشحة", + "mobileHomeTab": "الرئيسية", + "mobileLiveStreamers": "البث المباشر", + "mobileMustBeLoggedIn": "سجل الدخول لعرض هذه الصفحة.", + "mobileNoSearchResults": "لا توجد نتائج", + "mobileNotFollowingAnyUser": "أنت لا تتبع أي مستخدم.", "mobileOkButton": "موافق", + "mobilePlayersMatchingSearchTerm": "لاعبين مع \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "تكبير القطعة المسحوبة", + "mobilePuzzleStormConfirmEndRun": "هل تريد إنهاء هذا التشغيل؟", + "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير المرشح", + "mobilePuzzleStormNothingToShow": "لا شيء لإظهاره. العب بعض الألغاز.", + "mobilePuzzleStormSubtitle": "حل أكبر عدد ممكن من الألغاز في 3 دقائق.", + "mobilePuzzleStreakAbortWarning": "سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.", + "mobilePuzzleThemesSubtitle": "حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.", + "mobilePuzzlesTab": "ألغاز", + "mobileRecentSearches": "عمليات البحث الأخيرة", "mobileSettingsHapticFeedback": "التعليقات اللمسية", "mobileSettingsImmersiveMode": "وضع ملء الشاشة", "mobileSettingsImmersiveModeSubtitle": "إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.", - "mobileNotFollowingAnyUser": "أنت لا تتبع أي مستخدم.", - "mobileAllGames": "جميع الألعاب", - "mobileRecentSearches": "عمليات البحث الأخيرة", - "mobileClearButton": "مسح", - "mobilePlayersMatchingSearchTerm": "لاعبين مع \"{param}\"", - "mobileNoSearchResults": "لا توجد نتائج", - "mobileAreYouSure": "هل أنت متأكد؟", - "mobilePuzzleStreakAbortWarning": "سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.", - "mobilePuzzleStormNothingToShow": "لا شيء لإظهاره. العب بعض الألغاز.", - "mobileSharePuzzle": "شارك هذا اللغز", - "mobileShareGameURL": "شارك رابط المباراة", + "mobileSettingsTab": "الإعدادات", "mobileShareGamePGN": "شارك الPGN", + "mobileShareGameURL": "شارك رابط المباراة", "mobileSharePositionAsFEN": "مشاركة الموضع كFEN", - "mobileShowVariations": "أظهر سلسلة النقلات المرشحة", - "mobileHideVariation": "إخفاء سلسلة النقلات المرشحة", + "mobileSharePuzzle": "شارك هذا اللغز", "mobileShowComments": "عرض التعليقات", - "mobilePuzzleStormConfirmEndRun": "هل تريد إنهاء هذا التشغيل؟", - "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير الفلاتر", - "mobileCancelTakebackOffer": "إلغاء عرض الاسترداد", - "mobileCancelDrawOffer": "إلغاء عرض التعادل", - "mobileWaitingForOpponentToJoin": "في انتظار انضمام الطرف الآخر...", - "mobileBlindfoldMode": "عصب العينين", - "mobileLiveStreamers": "البثوث المباشرة", - "mobileCustomGameJoinAGame": "الانضمام إلى لعبة", - "mobileCorrespondenceClearSavedMove": "مسح النقل المحفوظ", + "mobileShowResult": "إظهار النتيجة", + "mobileShowVariations": "أظهر سلسلة النقلات المرشحة", "mobileSomethingWentWrong": "لقد حدث خطأ ما.", + "mobileSystemColors": "ألوان النظام", + "mobileToolsTab": "أدوات", + "mobileWaitingForOpponentToJoin": "في انتظار انضمام الطرف الآخر...", + "mobileWatchTab": "شاهد", "activityActivity": "الأنشطة", "activityHostedALiveStream": "بدأ بث مباشر", "activityRankedInSwissTournament": "حائز على تصنيف #{param1} في {param2}", @@ -59,7 +64,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =0{تنافس في {count} بطولة سويسرية} =1{تنافس في {count} بطولة سويسرية} =2{تنافس في {count} بطولة سويسرية} few{تنافس في {count} بطولة سويسرية} many{تنافس في {count} بطولة سويسرية} other{تنافس في {count} بطولة سويسرية}}", "activityJoinedNbTeams": "{count, plural, =0{إنضم ل {count} فريق} =1{إنضم ل {count} فريق} =2{إنضم لفريقين {count}} few{إنضم ل {count} فرق} many{إنضم ل {count} فرقة} other{إنضم ل {count} فريقًا}}", "broadcastBroadcasts": "البثوث", + "broadcastMyBroadcasts": "بثي", "broadcastLiveBroadcasts": "بث البطولة المباشرة", + "broadcastBroadcastCalendar": "تقويم البث", + "broadcastNewBroadcast": "بث مباشر جديد", + "broadcastSubscribedBroadcasts": "البث المُشترك به", + "broadcastAboutBroadcasts": "حول البثوث", + "broadcastHowToUseLichessBroadcasts": "كيفية استخدام بث ليتشيس.", + "broadcastTheNewRoundHelp": "ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق.", + "broadcastAddRound": "إضافة جولة", + "broadcastOngoing": "الجارية", + "broadcastUpcoming": "القادمة", + "broadcastCompleted": "المكتملة", + "broadcastCompletedHelp": "يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر.", + "broadcastRoundName": "اسم الجولة", + "broadcastRoundNumber": "رقم الجولة (الشوط)", + "broadcastTournamentName": "اسم البطولة", + "broadcastTournamentDescription": "وصف موجز للبطولة", + "broadcastFullDescription": "الوصف الكامل", + "broadcastFullDescriptionHelp": "الوصف الاختياري الطويل للبث. {param1} متوفر. يجب أن لا يتجاوز طول النص {param2} حرفاً.", + "broadcastSourceSingleUrl": "رابط مصدر PGN", + "broadcastSourceUrlHelp": "URL الذي سيتحقق منه Lichess للحصول على تحديثات PGN. يجب أن يكون متاحًا للجميع على الإنترنت.", + "broadcastSourceGameIds": "حتى 64 معرف لُعْبَة ليتشيس، مفصولة بمسافات.", + "broadcastStartDateTimeZone": "موعد البداية بتوقيت البطولة المحلي: {param}", + "broadcastStartDateHelp": "اختياري، إذا كنت تعرف متى يبدأ الحدث", + "broadcastCurrentGameUrl": "رابط المباراة الحالية", + "broadcastDownloadAllRounds": "تحميل جميع المباريات", + "broadcastResetRound": "إعادة ضبط هذه الجولة", + "broadcastDeleteRound": "حذف هذه الجولة", + "broadcastDefinitivelyDeleteRound": "قم بحذف الجولة وألعابها نهائيا.", + "broadcastDeleteAllGamesOfThisRound": "احذف جميع ألعاب هذه الجولة. سوف يحتاج المصدر إلى أن يكون نشطا من أجل إعادة إنشائها.", + "broadcastEditRoundStudy": "تعديل دراسة الجولة", + "broadcastDeleteTournament": "حذف هذه المسابقة", + "broadcastDefinitivelyDeleteTournament": "قم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها.", + "broadcastShowScores": "اظهر نقاط اللاعبين بناءً على نتائج اللعبة", + "broadcastReplacePlayerTags": "اختياري: استبدل أسماء اللاعبين وتقييماتهم وألقابهم", + "broadcastFideFederations": "الاتحاد الدولي للشطرنج", + "broadcastTop10Rating": "تقييم أعلى 10", + "broadcastFidePlayers": "لاعبين FIDE", + "broadcastFidePlayerNotFound": "لم يتم العثور على لاعب الاتحاد الدولي (FIDE)", + "broadcastFideProfile": "مِلَفّ FIDE", + "broadcastFederation": "إتحاد", + "broadcastAgeThisYear": "العمر هذا العام", + "broadcastUnrated": "غير مقيم", + "broadcastRecentTournaments": "البطولات الأخيرة", + "broadcastNbBroadcasts": "{count, plural, =0{{count} بث} =1{{count} بث} =2{بثين} few{{count} بثوث} many{{count} بثوث} other{{count} بثوث}}", "challengeChallengesX": "التحديات: {param1}", "challengeChallengeToPlay": "تحدى في مباراة", "challengeChallengeDeclined": "تم رفض التحدي", @@ -183,6 +232,7 @@ "preferencesNotifyWeb": "المتصفح", "preferencesNotifyDevice": "الجهاز", "preferencesBellNotificationSound": "صوت التنبيه", + "preferencesBlindfold": "معصوب العينين", "puzzlePuzzles": "الألغاز", "puzzlePuzzleThemes": "خصائص الألغاز", "puzzleRecommended": "مقترح", @@ -378,8 +428,8 @@ "puzzleThemeXRayAttackDescription": "القطعة تهاجم أو تدافع عن مربع, من خلال قطعة عدو.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "حركات الخصم محدودة و كل الحركات تؤدي إلى تفاقم الوضع نحو الأسوء.", - "puzzleThemeHealthyMix": "خليط", - "puzzleThemeHealthyMixDescription": "القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.", + "puzzleThemeMix": "خليط", + "puzzleThemeMixDescription": "القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.", "puzzleThemePlayerGames": "مبارايات اللاعب", "puzzleThemePlayerGamesDescription": "ابحث عن ألغاز من مبارياتك أو من مباريات لاعبين آخرين.", "puzzleThemePuzzleDownloadInformation": "هذه الألغاز موجودة للعامة بإمكانك تحميلها من هنا{param}.", @@ -500,7 +550,6 @@ "replayMode": "نمط إعادة العرض", "realtimeReplay": "ذات الوقت", "byCPL": "بالاثارة", - "openStudy": "فتح دراسة", "enable": "تفعيل", "bestMoveArrow": "سهم أفضل نقلة", "showVariationArrows": "أظهر سلسلة النقلات المرشحة", @@ -510,7 +559,6 @@ "memory": "الذاكرة", "infiniteAnalysis": "تحليل لانهائي", "removesTheDepthLimit": "التحليل لأبعد عمق، وابقاء حاسوبك نشطًا", - "engineManager": "مدير المحركات", "blunder": "خطأ فادح", "mistake": "خطأ", "inaccuracy": "غير دقيق", @@ -708,7 +756,6 @@ "block": "حظر", "blocked": "محظور", "unblock": "إلغاء الحظر", - "followsYou": "يتابعك", "xStartedFollowingY": "{param1} بدأ متابعة {param2}", "more": "المزيد", "memberSince": "مسجل منذ", @@ -814,7 +861,9 @@ "cheat": "غش", "troll": "إزعاج", "other": "أخرى", - "reportDescriptionHelp": "الصق رابط المباراة (المباريات) واشرح بالتفصيل المشكلة في تصرف هذا المستحدم. لا تقل فقط \"انهم يغشون\"، ولكن اشرح لنا سبب استنتاجك. سيكون الرد أسرع إن كتبت بالإنكليزية.", + "reportCheatBoostHelp": "هذه رسالة عامية وليست مخصصة لبلاغات الغش. وهي تحاول تعليم اللاعب كيفية كتابة بلاغ مفيد لفريق لي-تشيس. و أيضا تطلب إثبات.\n\nتظهر على صفحة \"بلغ مستخدم\"\nhttps://lichess. org/report.", + "reportUsernameHelp": "اشرح ما المسيء في اسم المستخدم هذا. لا تقل فقط \"إنه مسيء/غير مناسب\"، بل أخبرنا كيف توصلت إلى هذا الاستنتاج، خاصة إذا كانت الإهانة غير واضحة، أو ليست باللغة الإنجليزية، أو كانت باللغة العامية، أو كانت إشارة تاريخية/ثقافية.", + "reportProcessedFasterInEnglish": "سيتم معالجة بلاغك بشكل أسرع إذا تمت كتابته باللغة الإنجليزية.", "error_provideOneCheatedGameLink": "برجاء تقديم رابط واحد علي الأقل لمباراة حدث فيها غش.", "by": "كتبها {param}", "importedByX": "استيراد '{param}'", @@ -1308,6 +1357,177 @@ "stormXRuns": "{count, plural, =0{لا جولات} =1{جولة واحدة} =2{جولتان} few{{count} جولات} many{{count} جولة} other{{count} جولة}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =0{لم تلعب أي جولة {param2}} =1{لعبت جولة واحدة من {param2}} =2{لعبت جولتين من {param2}} few{لعبت {count} جولات من {param2}} many{لعبت {count} جولة من {param2}} other{لعبت {count} جولة من {param2}}}", "streamerLichessStreamers": "بثوث ليشس", + "studyPrivate": "خاص", + "studyMyStudies": "دراستي", + "studyStudiesIContributeTo": "الدراسات المساهم بها", + "studyMyPublicStudies": "دراسات العامة", + "studyMyPrivateStudies": "دراساتي الخاصة", + "studyMyFavoriteStudies": "دراساتي المفضلة", + "studyWhatAreStudies": "ما هي الدراسات؟", + "studyAllStudies": "كل الدراسات", + "studyStudiesCreatedByX": "الدراسات التي أنشئها {param}", + "studyNoneYet": "لا يوجد.", + "studyHot": "ذات شعبية", + "studyDateAddedNewest": "تاريخ الإضافة (الأحدث)", + "studyDateAddedOldest": "تاريخ الإضافة (الأقدم)", + "studyRecentlyUpdated": "تم تحديثه مؤخرا", + "studyMostPopular": "الاكثر شعبية", + "studyAlphabetical": "أبجدي", + "studyAddNewChapter": "أضف فصلاً جديدا", + "studyAddMembers": "إضافة أعضاء", + "studyInviteToTheStudy": "دعوة الى دراسة", + "studyPleaseOnlyInvitePeopleYouKnow": "يرجى فقط إضافة اشخاص تعرفهم، ويريدون المشاركة في هذه الدراسة", + "studySearchByUsername": "البحث بواسطة اسم المستخدم", + "studySpectator": "مشاهد", + "studyContributor": "مساهم", + "studyKick": "طرد", + "studyLeaveTheStudy": "مغادرة الدراسة", + "studyYouAreNowAContributor": "انت الان اصبحت مساهم", + "studyYouAreNowASpectator": "انت الان اصبحت مشاهد", + "studyPgnTags": "وسم PGN", + "studyLike": "إعجاب", + "studyUnlike": "إلغاء الإعجاب", + "studyNewTag": "علامة جديدة", + "studyCommentThisPosition": "التعليق على هذا الوضع", + "studyCommentThisMove": "التعليق على هذه النقلة", + "studyAnnotateWithGlyphs": "التعليق مع الحروف الرسومية", + "studyTheChapterIsTooShortToBeAnalysed": "الفصل جداً قصير لكي يتم تحليله", + "studyOnlyContributorsCanRequestAnalysis": "فقط المساهمون في هذا الدراسة يمكنهم طلب تحليل الحاسوب", + "studyGetAFullComputerAnalysis": "احصل على تحليل حاسوب كامل للتفريع الرئيسي من قبل الخادم", + "studyMakeSureTheChapterIsComplete": "كن متأكداً ان الفصل مكتمل، يمكنك طلب تحليل الحاسوب مره واحده فحسب", + "studyAllSyncMembersRemainOnTheSamePosition": "يظل جميع ألاعضاء الذين تمت مزامنة معلوماتهم في نفس الترتيب", + "studyShareChanges": "شارك التغيبرات مع المشاهدين وإحفظهن الى الخادم", + "studyPlaying": "يلعب الان", + "studyShowEvalBar": "شرائط التقييم", + "studyFirst": "الأولى", + "studyPrevious": "السابق", + "studyNext": "التالي", + "studyLast": "الأخير", "studyShareAndExport": "مشاركة و تصدير", - "studyStart": "ابدأ" + "studyCloneStudy": "استنساخ", + "studyStudyPgn": "PGN الدراسة", + "studyDownloadAllGames": "حمل جميع الألعاب", + "studyChapterPgn": "PGN الفصل", + "studyCopyChapterPgn": "نسخ PGN", + "studyDownloadGame": "حمل لعبة", + "studyStudyUrl": "رابط الدراسة", + "studyCurrentChapterUrl": "رابط الفصل الحالي", + "studyYouCanPasteThisInTheForumToEmbed": "يمكنك لصق هذا في المنتدى لتضمينه", + "studyStartAtInitialPosition": "البدء من وضع البداية", + "studyStartAtX": "البدء من {param}", + "studyEmbedInYourWebsite": "ضمنه في موقع أو مدونة", + "studyReadMoreAboutEmbedding": "راجع المزيد عن التضمين", + "studyOnlyPublicStudiesCanBeEmbedded": "يمكن تضمين الدراسات العامة فقط!", + "studyOpen": "فتح", + "studyXBroughtToYouByY": "{param1} مقدمة من {param2}", + "studyStudyNotFound": "لم يتم العثور على الدراسة", + "studyEditChapter": "تحرير الفصل", + "studyNewChapter": "فصل جديد", + "studyImportFromChapterX": "استيراد من {param}", + "studyOrientation": "اتجاه الرقعة", + "studyAnalysisMode": "وضع التحليل", + "studyPinnedChapterComment": "التعليق المثبت على الفصل", + "studySaveChapter": "حفظ الفصل", + "studyClearAnnotations": "مسح العلامات", + "studyClearVariations": "مسح اللاينات", + "studyDeleteChapter": "حذف الفصل", + "studyDeleteThisChapter": "هل تريد حذف الفصل ؟ لايمكنك التراجع عن ذلك لاحقاً!", + "studyClearAllCommentsInThisChapter": "مسح جميع التعليقات والغلافات والأشكال المرسومة في هذا الفصل؟", + "studyRightUnderTheBoard": "تحت الرقعة مباشرة", + "studyNoPinnedComment": "بدون", + "studyNormalAnalysis": "تحليل عادي", + "studyHideNextMoves": "أخفي النقلة التالية", + "studyInteractiveLesson": "درس تفاعلي", + "studyChapterX": "الفصل {param}", + "studyEmpty": "فارغ", + "studyStartFromInitialPosition": "البدء من وضعية البداية", + "studyEditor": "المحرر", + "studyStartFromCustomPosition": "البدء من وضع مخصص", + "studyLoadAGameByUrl": "تحميل لعبة من رابط", + "studyLoadAPositionFromFen": "تحميل موقف من FEN", + "studyLoadAGameFromPgn": "استرد لعبة من PGN", + "studyAutomatic": "تلقائي", + "studyUrlOfTheGame": "رابط اللعبة", + "studyLoadAGameFromXOrY": "استيراد لعبة من {param1} او {param2}", + "studyCreateChapter": "أنشئ الفصل", + "studyCreateStudy": "أنشى الدراسة", + "studyEditStudy": "حرر الدراسة", + "studyVisibility": "الظهور", + "studyPublic": "عامة", + "studyUnlisted": "غير مدرجة", + "studyInviteOnly": "دعوة فقط", + "studyAllowCloning": "السماح بالاستنساخ", + "studyNobody": "لا أحد", + "studyOnlyMe": "أنا فقط", + "studyContributors": "المساهمون", + "studyMembers": "اعضاء", + "studyEveryone": "الجميع", + "studyEnableSync": "مكن المزامنة", + "studyYesKeepEveryoneOnTheSamePosition": "نعم: إبقاء الجميع في نفس الوضعية", + "studyNoLetPeopleBrowseFreely": "لا: دع الناس يتصفحون بحرية", + "studyPinnedStudyComment": "تعليق الدراسة المثبتة", + "studyStart": "ابدأ", + "studySave": "حفظ", + "studyClearChat": "مسح المحادثة", + "studyDeleteTheStudyChatHistory": "هل تريد حذف سجل الدردشة الدراسية؟ لا يمكن إرجاعها!", + "studyDeleteStudy": "حذف الدراسة", + "studyConfirmDeleteStudy": "حذف الدراسة بأكملها؟ لا يمكنك التراجع عن هذه الخطوة! اكتب اسم الدراسة لتأكيد عملية الحذف: {param}", + "studyWhereDoYouWantToStudyThat": "أين تريد دراسة ذلك؟", + "studyGoodMove": "نقلة جيدة", + "studyMistake": "خطأ", + "studyBrilliantMove": "نقلة رائعة", + "studyBlunder": "غلطة", + "studyInterestingMove": "نقلة مثيرة للاهتمام", + "studyDubiousMove": "نقلة مشبوهة", + "studyOnlyMove": "نقلة وحيدة", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "وضع متساوي", + "studyUnclearPosition": "وضعية غير واضح", + "studyWhiteIsSlightlyBetter": "الأبيض أفضل بقليل", + "studyBlackIsSlightlyBetter": "الأسود أفضل بقليل", + "studyWhiteIsBetter": "الأبيض أفضل", + "studyBlackIsBetter": "الأسود أفضل", + "studyWhiteIsWinning": "الأبيض يفوز", + "studyBlackIsWinning": "الأسود يفوز", + "studyNovelty": "جديد", + "studyDevelopment": "تطوير", + "studyInitiative": "مبادرة", + "studyAttack": "هجوم", + "studyCounterplay": "هجوم مضاد", + "studyTimeTrouble": "مشكلة وقت", + "studyWithCompensation": "مع تعويض", + "studyWithTheIdea": "مع فكرة", + "studyNextChapter": "الفصل التالي", + "studyPrevChapter": "الفصل السابق", + "studyStudyActions": "خيارات الدراسة", + "studyTopics": "المواضيع", + "studyMyTopics": "المواضيع الخاصة بي", + "studyPopularTopics": "المواضيع الشائعة", + "studyManageTopics": "إدارة المواضيع", + "studyBack": "رجوع", + "studyPlayAgain": "اللعب مجددا", + "studyWhatWouldYouPlay": "ماذا ستلعب في هذا الموقف؟", + "studyYouCompletedThisLesson": "تهانينا! لقد أكملت هذا الدرس.", + "studyNbChapters": "{count, plural, =0{{count} فصل} =1{{count} فصل} =2{فصلان} few{{count} فصول} many{{count} فصل} other{{count} فصول}}", + "studyNbGames": "{count, plural, =0{{count} مباراة} =1{{count} مباراة} =2{مبارتان} few{{count} مبارايات} many{{count} مباراة} other{{count} مباراة}}", + "studyNbMembers": "{count, plural, =0{{count} عضو} =1{{count} عضو} =2{{count} عضو} few{{count} عضو} many{{count} عضو} other{{count} أعضاء}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{ألصق نص PGN هنا، حتى {count} مباراة} =1{الصق نص الPGN هنا، حتى {count} لعبة واحدة} =2{ألصق نص PGN هنا، حتى {count} مباراة} few{ألصق نص PGN هنا، حتى {count} مباراة} many{ألصق نص PGN هنا، حتى {count} مباراة} other{الصق الPGN هنا، حتى {count} العاب}}", + "timeagoJustNow": "الان", + "timeagoRightNow": "حاليا", + "timeagoCompleted": "مكتمل", + "timeagoInNbSeconds": "{count, plural, =0{خلال {count} ثانية} =1{خلال ثانية} =2{خلال ثانيتين} few{خلال {count} ثوانٍ} many{خلال {count} ثانية} other{خلال {count} ثانية}}", + "timeagoInNbMinutes": "{count, plural, =0{خلال {count} دقيقة} =1{خلال دقيقة} =2{خلال {count} دقيقتين} few{خلال {count} دقائق} many{خلال {count} دقائق} other{خلال {count} دقائق}}", + "timeagoInNbHours": "{count, plural, =0{خلال {count} ساعة} =1{خلال ساعة} =2{خلال ساعتين} few{خلال {count} ساعات} many{خلال {count} ساعة} other{خلال {count} ساعة}}", + "timeagoInNbDays": "{count, plural, =0{خلال {count} يوم} =1{خلال {count} يوم} =2{خلال {count} يوم} few{خلال {count} أيام} many{خلال {count} يوم} other{خلال {count} يوم}}", + "timeagoInNbWeeks": "{count, plural, =0{خلال {count} أسبوع} =1{خلال {count} اسبوع} =2{خلال {count} اسبوع} few{خلال {count} اسبوع} many{خلال {count} اسبوع} other{خلال {count} اسبوع}}", + "timeagoInNbMonths": "{count, plural, =0{خلال {count} شهر} =1{خلال {count} شهر} =2{خلال {count} شهر} few{خلال {count} شهر} many{خلال {count} شهر} other{خلال {count} شهر}}", + "timeagoInNbYears": "{count, plural, =0{خلال {count} سنة} =1{خلال {count} سنة} =2{خلال {count} سنة} few{خلال {count} سنة} many{خلال {count} سنة} other{خلال {count} سنة}}", + "timeagoNbMinutesAgo": "{count, plural, =0{منذ {count} دقيقة/دقائق مضت} =1{منذ {count} دقيقة} =2{منذ {count} دقيقتين} few{منذ {count} دقيقة/دقائق مضت} many{منذ {count} دقيقة/دقائق مضت} other{منذ {count} دقيقة/دقائق مضت}}", + "timeagoNbHoursAgo": "{count, plural, =0{منذ {count} ساعة/ساعات مضت} =1{منذ {count} ساعة} =2{منذ {count} ساعة/ساعات مضت} few{منذ {count} ساعة/ساعات مضت} many{منذ {count} ساعة/ساعات مضت} other{منذ {count} ساعة/ساعات مضت}}", + "timeagoNbDaysAgo": "{count, plural, =0{منذ {count} يوم/أيام} =1{منذ {count} يوم} =2{منذ {count} يوم/أيام} few{منذ {count} يوم/أيام} many{منذ {count} يوم/أيام} other{منذ {count} يوم/أيام}}", + "timeagoNbWeeksAgo": "{count, plural, =0{منذ {count} أسابيع} =1{منذ {count} أسابيع} =2{منذ {count} أسابيع} few{منذ {count} أسابيع} many{منذ {count} أسابيع} other{منذ {count} أسابيع}}", + "timeagoNbMonthsAgo": "{count, plural, =0{منذ {count} شهر} =1{منذ {count} شهر} =2{منذ {count} شهر} few{منذ {count} شهر} many{منذ {count} شهر} other{منذ {count} شهر}}", + "timeagoNbYearsAgo": "{count, plural, =0{منذ {count} سنة} =1{منذ {count} سنة} =2{منذ {count} سنة} few{منذ {count} سنة} many{منذ {count} سنة} other{منذ {count} سنة}}", + "timeagoNbMinutesRemaining": "{count, plural, =0{{count}دقيقة متبقية} =1{{count}دقيقة متبقية} =2{{count}دقيقتان متبقيتان} few{{count}دقائق متبقية} many{{count}دقيقة متبقية} other{{count}دقائق متبقية}}", + "timeagoNbHoursRemaining": "{count, plural, =0{{count}ساعة متبقية} =1{{count}ساعة واحدة متبقية} =2{{count}ساعتان متبقيتان} few{{count} ساعات متبقية} many{{count}ساعة متبقية} other{{count}ساعة متبقية}}" } \ No newline at end of file diff --git a/lib/l10n/lila_az.arb b/lib/l10n/lila_az.arb index e8769b9b78..70237b703b 100644 --- a/lib/l10n/lila_az.arb +++ b/lib/l10n/lila_az.arb @@ -22,6 +22,22 @@ "activityJoinedNbTeams": "{count, plural, =1{{count} komandasına qatıldı} other{{count} komandasına qatıldı}}", "broadcastBroadcasts": "Yayım", "broadcastLiveBroadcasts": "Canlı turnir yayımları", + "broadcastNewBroadcast": "Yeni canlı yayım", + "broadcastAddRound": "Tur əlavə et", + "broadcastOngoing": "Davam edən", + "broadcastUpcoming": "Yaxınlaşan", + "broadcastCompleted": "Tamamlanan", + "broadcastRoundName": "Tur adı", + "broadcastRoundNumber": "Tur sayı", + "broadcastTournamentName": "Turnir adı", + "broadcastTournamentDescription": "Qısa turnir açıqlaması", + "broadcastFullDescription": "Tədbirin tam açıqlaması", + "broadcastFullDescriptionHelp": "Tədbirin istəyə bağlı təfsilatlı açıqlaması. {param1} seçimi mövcuddur. Mətnin uzunluğu {param2} simvoldan az olmalıdır.", + "broadcastSourceUrlHelp": "Lichess, verdiyiniz URL ilə PGN-i yeniləyəcək. Bu internetdə hamı tərəfindən əldə edilə bilən olmalıdır.", + "broadcastStartDateHelp": "İstəyə bağlı, tədbirin başlama vaxtını bilirsinizsə", + "broadcastCurrentGameUrl": "Hazırkı oyun URL-i", + "broadcastResetRound": "Bu turu sıfırla", + "broadcastDeleteRound": "Bu turu sil", "challengeChallengeToPlay": "Oyuna çağırış", "challengeChallengeDeclined": "Çağırış rədd edildi.", "challengeChallengeAccepted": "Çağırış qəbul edildi!", @@ -301,8 +317,8 @@ "puzzleThemeXRayAttackDescription": "Fiqur, bir xanaya rəqib fiquru üzərindən hücum edir və ya müdafiə edir.", "puzzleThemeZugzwang": "Suqsvanq", "puzzleThemeZugzwangDescription": "Rəqibin edə biləcəyi gediş sayı məhduddur və istənilən gediş vəziyyəti daha da pisləşdirir.", - "puzzleThemeHealthyMix": "Həftəbecər", - "puzzleThemeHealthyMixDescription": "Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.", + "puzzleThemeMix": "Həftəbecər", + "puzzleThemeMixDescription": "Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.", "searchSearch": "Axtar", "settingsSettings": "Tənzimləmələr", "settingsCloseAccount": "Hesabı bağla", @@ -416,7 +432,6 @@ "replayMode": "Təkrar rejimi", "realtimeReplay": "Gerçək zamanlı", "byCPL": "CPL üzrə", - "openStudy": "Çalışmanı aç", "enable": "Aktiv et", "bestMoveArrow": "Ən yaxşı gedişi göstər", "showVariationArrows": "Variasiya oxlarını göstərin", @@ -426,7 +441,6 @@ "memory": "RAM", "infiniteAnalysis": "Sonsuz təhlil", "removesTheDepthLimit": "Dərinlik limitini ləğv edir və kompüterinizi isti saxlayır", - "engineManager": "Mühərrik meneceri", "blunder": "Kobud Səhv", "mistake": "Səhv", "inaccuracy": "Qeyri-dəqiqlik", @@ -620,7 +634,6 @@ "block": "Blok", "blocked": "Bloklanıb", "unblock": "Blokdan çıxart", - "followsYou": "Səni izləyir", "xStartedFollowingY": "{param1} {param2} adlı oyunçunu izləməyə başladı", "more": "Daha çox", "memberSince": "Üzvlük tarixi", @@ -716,7 +729,6 @@ "cheat": "Hiylə", "troll": "Trol", "other": "Digər", - "reportDescriptionHelp": "Oyunun və ya oyunların linkini yapışdırın və bu istifadəçinin davranışında nəyin səhv olduğunu izah edin. Yalnız \"hiylə edirlər\" deməyin, necə bu nəticəyə gəldiyinizi bizə deyin. İngilis dilində yazıldığı təqdirdə hesabat daha sürətli işlənəcəkdir.", "error_provideOneCheatedGameLink": "Lütfən ən azı bir hiyləli oyun linki daxil edin.", "by": "{param} tərəfindən", "thisTopicIsNowClosed": "Mövzu bağlandı.", @@ -1106,6 +1118,136 @@ "stormXRuns": "{count, plural, =1{1 cəhd} other{{count} cəhd}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Bir dəfə {param2} oynadı} other{{count} dəfə {param2} oynadı}}", "streamerLichessStreamers": "Lichess yayımçıları", + "studyPrivate": "Özəl", + "studyMyStudies": "Çalışmalarım", + "studyStudiesIContributeTo": "Töhfə verdiyim çalışmalar", + "studyMyPublicStudies": "Hərkəsə açıq çalışmalarım", + "studyMyPrivateStudies": "Özəl çalışmalarım", + "studyMyFavoriteStudies": "Sevimli çalışmalarım", + "studyWhatAreStudies": "Çalışmalar nədir?", + "studyAllStudies": "Bütün çalışmalar", + "studyStudiesCreatedByX": "{param} tərəfindən yaradılan çalışmalar", + "studyNoneYet": "Hələ ki, yoxdur.", + "studyHot": "Məşhur", + "studyDateAddedNewest": "Əlavə edilmə tarixi (yenidən köhnəyə)", + "studyDateAddedOldest": "Əlavə edilmə tarixi (köhnədən yeniyə)", + "studyRecentlyUpdated": "Ən son yenilənən", + "studyMostPopular": "Ən məşhur", + "studyAlphabetical": "Əlifbaya görə", + "studyAddNewChapter": "Yeni bir fəsil əlavə et", + "studyAddMembers": "Üzv əlavə et", + "studyInviteToTheStudy": "Çalışmaya dəvət et", + "studyPleaseOnlyInvitePeopleYouKnow": "Zəhmət olmasa yalnız tanıdığınız və bu çalışmaya aktiv olaraq qoşulmaq istəyən insanları dəvət edin.", + "studySearchByUsername": "İstifadəçi adına görə axtar", + "studySpectator": "Tamaşaçı", + "studyContributor": "Töhfə verən", + "studyKick": "Qov", + "studyLeaveTheStudy": "Çalışmanı tərk et", + "studyYouAreNowAContributor": "İndi iştirakçısınız", + "studyYouAreNowASpectator": "İndi tamaşaçısınız", + "studyPgnTags": "PGN etiketləri", + "studyLike": "Bəyən", + "studyNewTag": "Yeni etiket", + "studyCommentThisPosition": "Bu pozisiyaya rəy bildirin", + "studyCommentThisMove": "Bu gedişə rəy bildirin", + "studyAnnotateWithGlyphs": "Simvol ilə izah et", + "studyTheChapterIsTooShortToBeAnalysed": "Fəsil təhlil edilməsi üçün çox qısadır.", + "studyOnlyContributorsCanRequestAnalysis": "Yalnız çalışma iştirakçıları kompüter təhlili tələb edə bilər.", + "studyGetAFullComputerAnalysis": "Ana variant üçün serverdən hərtərəfli kompüter təhlilini alın.", + "studyMakeSureTheChapterIsComplete": "Fəslin tamamlandığına əmin olun. Yalnız bir dəfə təhlil tələbi edə bilərsiniz.", + "studyAllSyncMembersRemainOnTheSamePosition": "EYNİLƏŞDİRİLMİŞ bütün üzvlər eyni pozisiyada qalır", + "studyShareChanges": "Dəyişiklikləri tamaşaçılarla paylaşın və onları serverdə saxlayın", + "studyPlaying": "Oynanılan", + "studyFirst": "İlk", + "studyPrevious": "Əvvəlki", + "studyNext": "Növbəti", + "studyLast": "Son", "studyShareAndExport": "Paylaş və yüklə", - "studyStart": "Başlat" + "studyCloneStudy": "Klonla", + "studyStudyPgn": "Çalışma PGN-i", + "studyDownloadAllGames": "Bütün oyunları endir", + "studyChapterPgn": "Fəsil PGN-i", + "studyDownloadGame": "Oyunu endir", + "studyStudyUrl": "Çalışma URL-i", + "studyCurrentChapterUrl": "Cari fəsil URL-ii", + "studyYouCanPasteThisInTheForumToEmbed": "Pərçimləmək üçün bunu forumda paylaşa bilərsiniz", + "studyStartAtInitialPosition": "Başlanğıc pozisiyada başlasın", + "studyStartAtX": "buradan başla: {param}", + "studyEmbedInYourWebsite": "Veb sayt və ya bloqunuzda pərçimləyin", + "studyReadMoreAboutEmbedding": "Pərçimləmə haqqında daha ətraflı", + "studyOnlyPublicStudiesCanBeEmbedded": "Yalnız hərkəsə açıq çalışmalar pərçimlənə bilər!", + "studyOpen": "Aç", + "studyXBroughtToYouByY": "{param2} sizə {param1} tərəfindən gətirildi", + "studyStudyNotFound": "Çalışma tapılmadı", + "studyEditChapter": "Fəslə düzəliş et", + "studyNewChapter": "Yeni fəsil", + "studyOrientation": "İstiqamət", + "studyAnalysisMode": "Təhlil rejimi", + "studyPinnedChapterComment": "Sancaqlanmış fəsil rəyləri", + "studySaveChapter": "Fəsli yadda saxla", + "studyClearAnnotations": "İzahları təmizlə", + "studyDeleteChapter": "Fəsli sil", + "studyDeleteThisChapter": "Bu fəsil silinsin? Bunun geri dönüşü yoxdur!", + "studyClearAllCommentsInThisChapter": "Bu fəsildəki bütün rəylər, simvollar və çəkilmiş formalar təmizlənsin?", + "studyRightUnderTheBoard": "Lövhənin altında", + "studyNoPinnedComment": "Görünməsin", + "studyNormalAnalysis": "Normal təhlil", + "studyHideNextMoves": "Növbəti gedişləri gizlət", + "studyInteractiveLesson": "İnteraktiv dərs", + "studyChapterX": "{param}. Fəsil", + "studyEmpty": "Boş", + "studyStartFromInitialPosition": "Başlanğıc pozisiyadan başlasın", + "studyEditor": "Redaktor", + "studyStartFromCustomPosition": "Özəl pozisiyadan başlasın", + "studyLoadAGameByUrl": "URL ilə oyun yüklə", + "studyLoadAPositionFromFen": "FEN ilə pozisiya yüklə", + "studyLoadAGameFromPgn": "PGN ilə oyun yüklə", + "studyAutomatic": "Avtomatik", + "studyUrlOfTheGame": "Oyun URL-i", + "studyLoadAGameFromXOrY": "{param1} və ya {param2} ilə oyun yükləyin", + "studyCreateChapter": "Fəsil yarat", + "studyCreateStudy": "Çalışma yarat", + "studyEditStudy": "Çalışmaya düzəliş et", + "studyVisibility": "Görünmə", + "studyPublic": "Hərkəsə açıq", + "studyUnlisted": "Siyahıya alınmamış", + "studyInviteOnly": "Yalnız dəvətlə", + "studyAllowCloning": "Klonlamağa icazə ver", + "studyNobody": "Heç kim", + "studyOnlyMe": "Yalnız mən", + "studyContributors": "Töhfə verənlər", + "studyMembers": "Üzvlər", + "studyEveryone": "Hamı", + "studyEnableSync": "Eyniləşdirməni aktivləşdir", + "studyYesKeepEveryoneOnTheSamePosition": "Bəli: hər kəsi eyni pozisiyada saxla", + "studyNoLetPeopleBrowseFreely": "Xeyr: sərbəst gəzməyə icazə ver", + "studyPinnedStudyComment": "Sancaqlanmış çalışma rəyləri", + "studyStart": "Başlat", + "studySave": "Saxla", + "studyClearChat": "Söhbəti təmizlə", + "studyDeleteTheStudyChatHistory": "Çalışmanın söhbət tarixçəsi silinsin? Bunun geri dönüşü yoxdur!", + "studyDeleteStudy": "Çalışmanı sil", + "studyWhereDoYouWantToStudyThat": "Harada çalışmaq istəyirsən?", + "studyGoodMove": "Yaxşı gediş", + "studyMistake": "Səhv", + "studyBrilliantMove": "Brilyant gediş", + "studyBlunder": "Kobud səhv", + "studyInterestingMove": "Maraqlı gediş", + "studyDubiousMove": "Şübhəli gediş", + "studyOnlyMove": "Tək gediş", + "studyZugzwang": "Suqsvanq", + "studyEqualPosition": "Bərabər mövqe", + "studyUnclearPosition": "Qeyri-müəyyən mövqe", + "studyWhiteIsSlightlyBetter": "Ağlar biraz öndədir", + "studyBlackIsSlightlyBetter": "Qaralar biraz öndədir", + "studyWhiteIsBetter": "Ağlar üstündür", + "studyBlackIsBetter": "Qaralar üstündür", + "studyWhiteIsWinning": "Ağlar qalib gəlir", + "studyBlackIsWinning": "Qaralar qalib gəlir", + "studyNbChapters": "{count, plural, =1{{count} Fəsil} other{{count} Fəsil}}", + "studyNbGames": "{count, plural, =1{{count} Oyun} other{{count} Oyun}}", + "studyNbMembers": "{count, plural, =1{{count} Üzv} other{{count} Üzv}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər} other{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər}}", + "timeagoJustNow": "elə indi", + "timeagoRightNow": "indicə" } \ No newline at end of file diff --git a/lib/l10n/lila_be.arb b/lib/l10n/lila_be.arb index ef86112a2f..18dd49a2cf 100644 --- a/lib/l10n/lila_be.arb +++ b/lib/l10n/lila_be.arb @@ -1,4 +1,14 @@ { + "mobileAreYouSure": "Вы ўпэўнены?", + "mobileClearButton": "Ачысціць", + "mobileHomeTab": "Галоўная", + "mobileNoSearchResults": "Няма вынікаў", + "mobileOkButton": "Добра", + "mobilePlayersMatchingSearchTerm": "Гульцы з «{param}»", + "mobilePuzzlesTab": "Задачы", + "mobileRecentSearches": "Нядаўнія пошукі", + "mobileSettingsImmersiveMode": "Поўнаэкранны рэжым", + "mobileSettingsTab": "Налады", "activityActivity": "Актыўнасць", "activityHostedALiveStream": "Правялі прамую трансляцыю", "activityRankedInSwissTournament": "Скончыў на {param1} месцы ў {param2}", @@ -21,7 +31,35 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Паўдзельнічаў(-ла) у {count} турніры па швейцарскай сістэме} few{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме} many{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме} other{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме}}", "activityJoinedNbTeams": "{count, plural, =1{Далучыўся да {count} каманды} few{Далучыўся да {count} каманд} many{Далучыўся да {count} каманд} other{Далучыўся да {count} каманд}}", "broadcastBroadcasts": "Трансляцыі", + "broadcastMyBroadcasts": "Мае трансляцыі", "broadcastLiveBroadcasts": "Прамыя трансляцыі турніраў", + "broadcastNewBroadcast": "Новая прамая трансляцыя", + "broadcastAboutBroadcasts": "Пра трансляцыіі", + "broadcastHowToUseLichessBroadcasts": "Як карыстацца трансляцыямі Lichess.", + "broadcastAddRound": "Дадаць тур", + "broadcastOngoing": "Бягучыя", + "broadcastUpcoming": "Надыходзячыя", + "broadcastCompleted": "Завершаныя", + "broadcastRoundName": "Назва туру", + "broadcastRoundNumber": "Нумар туру", + "broadcastTournamentName": "Назва турніру", + "broadcastTournamentDescription": "Сціслае апісанне турніру", + "broadcastFullDescription": "Поўнае апісанне турніру", + "broadcastFullDescriptionHelp": "Неабавязковая дасканалае апісанне турніру. Даступны {param1}. Даўжыня павінна быць менш за {param2} сімвалаў.", + "broadcastSourceUrlHelp": "Спасылка, з якой Lichess паспрабуе атрымоўваць абнаўленні PGN. Яны павінна быць даступнай для кожнай ва Інтэрнэце.", + "broadcastStartDateHelp": "Па жаданні, калі вы ведаеце пачатак падзеі", + "broadcastCurrentGameUrl": "Спасылка на бягучую гульню", + "broadcastDownloadAllRounds": "Спампаваць усе туры", + "broadcastResetRound": "Скасаваць гэты тур", + "broadcastDeleteRound": "Выдаліць гэты тур", + "broadcastDefinitivelyDeleteRound": "Канчаткова выдаліць ​​тур і ўсе яго гульні.", + "broadcastDeleteAllGamesOfThisRound": "Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай.", + "broadcastEditRoundStudy": "Рэдагаваць навучанне туру", + "broadcastDeleteTournament": "Выдаліць гэты турнір", + "broadcastDefinitivelyDeleteTournament": "Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні.", + "broadcastFidePlayers": "Гульцы FIDE", + "broadcastFideProfile": "Профіль FIDE", + "broadcastFederation": "Федэрацыя", "challengeChallengesX": "Выклікаў: {param1}", "challengeChallengeToPlay": "Выклікаць на гульню", "challengeChallengeDeclined": "Выклік адхілены", @@ -172,6 +210,7 @@ "puzzleKeepGoing": "Працягвайце…", "puzzlePuzzleSuccess": "Поспех!", "puzzlePuzzleComplete": "Задача вырашана!", + "puzzlePuzzlesByOpenings": "Задачы за дэбютамі", "puzzleNotTheMove": "Гэта не той ход!", "puzzleTrySomethingElse": "Паспрабуйце нешта іншае.", "puzzleRatingX": "Рэйтынг: {param}", @@ -334,8 +373,8 @@ "puzzleThemeXRayAttackDescription": "Фігара нападае або бараніць поле праз варожую фігуру.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Супернік абмежаваны ў хадах і ўсе магчымыя хады пагаршаюць яго пазіцыю.", - "puzzleThemeHealthyMix": "Здаровая сумесь", - "puzzleThemeHealthyMixDescription": "Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.", + "puzzleThemeMix": "Здаровая сумесь", + "puzzleThemeMixDescription": "Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.", "puzzleThemePlayerGames": "З партый гульца", "puzzleThemePlayerGamesDescription": "Праглядзіце задачы ўзятыя з вашых гульняў, ці з партый іншага гульца.", "puzzleThemePuzzleDownloadInformation": "Гэта публічныя задачы, іх магчыма спампаваць з {param}.", @@ -402,7 +441,7 @@ "analysis": "Дошка для аналіза", "depthX": "Глыбіня {param}", "usingServerAnalysis": "Выкарыстоўваецца серверны аналіз", - "loadingEngine": "Загружаем шахматную праграму...", + "loadingEngine": "Загружаем рухавічок...", "calculatingMoves": "Пралічваем хады...", "engineFailed": "Памылка пры загрузцы шахматнай праграмы", "cloudAnalysis": "Воблачны аналіз", @@ -413,6 +452,7 @@ "promoteVariation": "Прасунуць варыянт", "makeMainLine": "Зрабіць асноўным варыянтам", "deleteFromHere": "Выдаліць з гэтага месца", + "collapseVariations": "Ачысціць варыянты", "forceVariation": "Прасунуць варыянт", "copyVariationPgn": "Скап'яваць варыянт у фармаце PGN", "move": "Ход", @@ -452,7 +492,6 @@ "replayMode": "Рэжым паўтору", "realtimeReplay": "У рэальным часе", "byCPL": "Цікавае", - "openStudy": "Адкрыць навучанне", "enable": "Уключыць", "bestMoveArrow": "Паказваць стрэлкай найлепшы ход", "evaluationGauge": "Шкала ацэнкі", @@ -654,7 +693,6 @@ "block": "Заблакаваць", "blocked": "Заблакаваны", "unblock": "Разблакіраваць", - "followsYou": "Падпісаны на вас", "xStartedFollowingY": "{param1} падпісаўся на {param2}", "more": "Яшчэ", "memberSince": "Далучыўся", @@ -712,6 +750,7 @@ "ifNoneLeaveEmpty": "Калі няма, пакіньце пустым", "profile": "Профіль", "editProfile": "Рэдагаваць профіль", + "realName": "Сапраўднае імя", "biography": "Біяграфія", "countryRegion": "Краіна або рэгіён", "thankYou": "Дзякуй!", @@ -728,12 +767,14 @@ "automaticallyProceedToNextGameAfterMoving": "Аўтаматычна пераходзіць да наступнай гульні пасля ходу", "autoSwitch": "Аўтапераключэнне", "puzzles": "Задачы", + "onlineBots": "Анлайн боты", "name": "Назва", "description": "Апісанне", "descPrivate": "Прыватнае апісанне", "descPrivateHelp": "Дапамагчы зрабіць \"прыватнае апісанне\".", "no": "Не", "yes": "Так", + "website": "Вэб-сайт", "help": "Дапамога:", "createANewTopic": "Стварыць новую тэму", "topics": "Тэмы", @@ -752,7 +793,6 @@ "cheat": "Несумленная гульня", "troll": "Троль", "other": "Іншае", - "reportDescriptionHelp": "Пакіньце ніжэй спасылку на гульню (ці гульні) і патлумачце, што вас непакоіць у паводзінах гэтага карыстальніка. Не пішыце нешта кшталту «ён чмут!» – патлумачце, як вы прыйшлі да гэтага выніку. Мы хутчэй разбярэмся ў сітуацыі, калі вы напішаце нам па-англійску.", "error_provideOneCheatedGameLink": "Калі ласка, дадайце спасылку хаця б на адну гульню, дзе былі парушаны правілы.", "by": "аўтар: {param}", "importedByX": "Імпартваў/-ла {param}", @@ -785,6 +825,7 @@ "slow": "Павольная", "insideTheBoard": "Унутры дошкі", "outsideTheBoard": "Па-за дошкай", + "allSquaresOfTheBoard": "Усе клеткі на дошцы", "onSlowGames": "У павольных гульнях", "always": "Заўседы", "never": "Ніколі", @@ -948,6 +989,11 @@ "transparent": "Празрысты", "deviceTheme": "Тэма прылады", "backgroundImageUrl": "Спасылка на фон:", + "board": "Дошка", + "size": "Размер", + "opacity": "Празрыстасць", + "brightness": "Яркасць", + "hue": "Адценне", "pieceSet": "Набор фігур", "embedInYourWebsite": "Убудаваць у свой сайт", "usernameAlreadyUsed": "Гэтае імя карыстальніка ўжо занятае. Калі ласка, паспрабуйце іншае.", @@ -1214,6 +1260,177 @@ "stormXRuns": "{count, plural, =1{1 спроба} few{{count} спробы} many{{count} спробаў} other{{count} спробаў}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Выкарастана адна з {param2} спроб} few{Выкарастана {count} з {param2} спроб} many{Выкарастана {count} з {param2} спробаў} other{Выкарастана {count} з {param2} спробаў}}", "streamerLichessStreamers": "Стрымеры на Lichess", + "studyPrivate": "Прыватны", + "studyMyStudies": "Мае навучанні", + "studyStudiesIContributeTo": "Навучанні, якія я рэдагую", + "studyMyPublicStudies": "Мае публічныя навучанні", + "studyMyPrivateStudies": "Мае прыватные навучанні", + "studyMyFavoriteStudies": "Мае ўлюбленые навучанні", + "studyWhatAreStudies": "Што такое навучанні?", + "studyAllStudies": "Усе навучанні", + "studyStudiesCreatedByX": "Навучанні, створаныя {param}", + "studyNoneYet": "Пакуль нічога няма.", + "studyHot": "Гарачыя", + "studyDateAddedNewest": "Дата дадання (навейшыя)", + "studyDateAddedOldest": "Дата дадання (старэйшыя)", + "studyRecentlyUpdated": "Нядаўна абноўленыя", + "studyMostPopular": "Найбольш папулярныя", + "studyAlphabetical": "Па алфавіце", + "studyAddNewChapter": "Дадаць новы раздзел", + "studyAddMembers": "Дадаць удзельнікаў", + "studyInviteToTheStudy": "Закліцца да навучання", + "studyPleaseOnlyInvitePeopleYouKnow": "Калі ласка, заклікайце толькі людзей, якіх вы ведаеце, та тых хто актыўна хоча далучыцца да навучання.", + "studySearchByUsername": "Шукаць па імені карыстальніка", + "studySpectator": "Глядач", + "studyContributor": "Рэдактар", + "studyKick": "Выдаліць", + "studyLeaveTheStudy": "Пакінуць навучанне", + "studyYouAreNowAContributor": "Вы цяпер рэдактар", + "studyYouAreNowASpectator": "Вы цяпер глядач", + "studyPgnTags": "Тэгі PGN", + "studyLike": "Упадабаць", + "studyUnlike": "Разпадабаць", + "studyNewTag": "Новы тэг", + "studyCommentThisPosition": "Каментаваць пазіцыю", + "studyCommentThisMove": "Каментаваць гэты ход", + "studyAnnotateWithGlyphs": "Дадаць знакавую анатацыю", + "studyTheChapterIsTooShortToBeAnalysed": "Раздел занадта кароткі для аналізу.", + "studyOnlyContributorsCanRequestAnalysis": "Толькі рэдактары навучання могуць запрасіць камп'ютарны аналіз.", + "studyGetAFullComputerAnalysis": "Атрымайце поўны серверны кампутарны аналіз галоўнай лініі.", + "studyMakeSureTheChapterIsComplete": "Пераканайцеся, што раздзел гатоў. Вы можаце запрасіць аналіз толькі адзін раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Усе сінхранізаваныя ўдзельнікі застаюцца на аднолькавай пазіцыі", + "studyShareChanges": "Падзяліцца зменамі з гледачамі та захаваць іх на серверы", + "studyPlaying": "Гуляецца", + "studyShowEvalBar": "Шкалы ацэнкі", + "studyFirst": "На першую", + "studyPrevious": "Папярэдняя", + "studyNext": "Наступная", + "studyLast": "На апошнюю", "studyShareAndExport": "Падзяліцца & экспартаваць", - "studyStart": "Пачаць" + "studyCloneStudy": "Кланаваць", + "studyStudyPgn": "PGN навучання", + "studyDownloadAllGames": "Спампаваць усе гульні", + "studyChapterPgn": "PGN раздзелу", + "studyCopyChapterPgn": "Скапіраваць PGN", + "studyDownloadGame": "Спампаваць гульню", + "studyStudyUrl": "URL навучання", + "studyCurrentChapterUrl": "URL бягучага раздзелу", + "studyYouCanPasteThisInTheForumToEmbed": "Вы можаце ўставіць гэта на форум, каб убудаваць", + "studyStartAtInitialPosition": "Пачынаць у пачатковай пазіцыі", + "studyStartAtX": "Пачынаць з {param}", + "studyEmbedInYourWebsite": "Убудаваць у свой сайт або блог", + "studyReadMoreAboutEmbedding": "Пачытаць больш пра ўбудаванне", + "studyOnlyPublicStudiesCanBeEmbedded": "Толькі публічныя навучанні могуць быць убудаваны!", + "studyOpen": "Адкрыць", + "studyXBroughtToYouByY": "{param2} зрабіў для вас {param1}", + "studyStudyNotFound": "Навучанне не знойдзена", + "studyEditChapter": "Рэдагаваць раздзел", + "studyNewChapter": "Новы раздзел", + "studyImportFromChapterX": "Імпартаваць з {param}", + "studyOrientation": "Арыентацыя дошкі", + "studyAnalysisMode": "Рэжым аналізу", + "studyPinnedChapterComment": "Замацаваны каментар раздзелу", + "studySaveChapter": "Захаваць раздзел", + "studyClearAnnotations": "Ачысціць анатацыі", + "studyClearVariations": "Ачысціць варыянты", + "studyDeleteChapter": "Выдаліць раздзел", + "studyDeleteThisChapter": "Выдаліць гэты раздел? Гэта нельга будзе адмяніць!", + "studyClearAllCommentsInThisChapter": "Выдаліць усе каментары, знакавыя анатацыі і намаляваныя фігуры ў гэтым раздзеле?", + "studyRightUnderTheBoard": "Адразу пад дошкай", + "studyNoPinnedComment": "Ніякіх", + "studyNormalAnalysis": "Звычайны аналіз", + "studyHideNextMoves": "Схаваць наступныя хады", + "studyInteractiveLesson": "Інтэрактыўны занятак", + "studyChapterX": "Раздзел {param}", + "studyEmpty": "Пуста", + "studyStartFromInitialPosition": "Пачынаць з пачатковай пазіцыі", + "studyEditor": "Рэдактар", + "studyStartFromCustomPosition": "Пачынаць з абранай пазіцыі", + "studyLoadAGameByUrl": "Загрузіць гульні па URLs", + "studyLoadAPositionFromFen": "Загрузіць пазіцыю з FEN", + "studyLoadAGameFromPgn": "Загрузіць гульні з PGN", + "studyAutomatic": "Аўтаматычна", + "studyUrlOfTheGame": "URL гульняў, адзін на радок", + "studyLoadAGameFromXOrY": "Загрузіць партыі з {param1} або {param2}", + "studyCreateChapter": "Стварыць раздзел", + "studyCreateStudy": "Стварыць навучанне", + "studyEditStudy": "Рэдактаваць навучанне", + "studyVisibility": "Бачнасць", + "studyPublic": "Публічны", + "studyUnlisted": "Нябачны", + "studyInviteOnly": "Толькі па запрашэннях", + "studyAllowCloning": "Дазволіць кланаванне", + "studyNobody": "Ніхто", + "studyOnlyMe": "Толькі я", + "studyContributors": "Рэдактары", + "studyMembers": "Удзельнікі", + "studyEveryone": "Кожны", + "studyEnableSync": "Уключыць сінхранізацыю", + "studyYesKeepEveryoneOnTheSamePosition": "Так: трымаць усіх на аднолькавай пазіцыі", + "studyNoLetPeopleBrowseFreely": "Не: хай людзі вольна праглядаюць пазіцыі", + "studyPinnedStudyComment": "Замацаваць каментар да занятку", + "studyStart": "Пачаць", + "studySave": "Захаваць", + "studyClearChat": "Ачысціць чат", + "studyDeleteTheStudyChatHistory": "Выдаліць гісторыю чата навучання цалкам? Гэта нельга будзе адмяніць!", + "studyDeleteStudy": "Выдаліць навучанне", + "studyConfirmDeleteStudy": "Выдаліць навучанне поўнасцю? Гэта нельга будзе адмяніць! Увядзіце назву навучання каб падцвердзіць: {param}", + "studyWhereDoYouWantToStudyThat": "Дзе вы жадаеце навучацца?", + "studyGoodMove": "Добры ход", + "studyMistake": "Памылка", + "studyBrilliantMove": "Бліскучы ход", + "studyBlunder": "Позех", + "studyInterestingMove": "Цікавы ход", + "studyDubiousMove": "Сумнеўны ход", + "studyOnlyMove": "Адзіны ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Раўная пазіцыя", + "studyUnclearPosition": "Незразумелая пазіцыя", + "studyWhiteIsSlightlyBetter": "У белых трошкі лепш", + "studyBlackIsSlightlyBetter": "У чорных трошкі лепш", + "studyWhiteIsBetter": "У белых лепш", + "studyBlackIsBetter": "У чорных лепш", + "studyWhiteIsWinning": "Белыя перамагаюць", + "studyBlackIsWinning": "Чорныя перамагаюць", + "studyNovelty": "Новаўвядзенне", + "studyDevelopment": "Развіццё", + "studyInitiative": "Ініцыятыва", + "studyAttack": "Напад", + "studyCounterplay": "Контргульня", + "studyTimeTrouble": "Цэйтнот", + "studyWithCompensation": "З кампенсацыяй", + "studyWithTheIdea": "З ідэяй", + "studyNextChapter": "Наступны раздзел", + "studyPrevChapter": "Папярэдні раздзел", + "studyStudyActions": "Дзеянні ў навучанні", + "studyTopics": "Тэмы", + "studyMyTopics": "Мае тэмы", + "studyPopularTopics": "Папулярныя тэмы", + "studyManageTopics": "Кіраваць тэмамі", + "studyBack": "Назад", + "studyPlayAgain": "Гуляць зноў", + "studyWhatWouldYouPlay": "Як бы вы пахадзілі ў гэтай пазіцыі?", + "studyYouCompletedThisLesson": "Віншуем! Вы прайшлі гэты ўрок.", + "studyNbChapters": "{count, plural, =1{{count} раздзел} few{{count} раздзелы} many{{count} раздзелаў} other{{count} раздзелаў}}", + "studyNbGames": "{count, plural, =1{{count} партыя} few{{count} партыі} many{{count} партый} other{{count} партый}}", + "studyNbMembers": "{count, plural, =1{{count} удзельнік} few{{count} удзельніка} many{{count} удзельнікаў} other{{count} удзельнікаў}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Устаўце сюды ваш PGN тэкст, не больш за {count} гульню} few{Устаўце сюды ваш PGN тэкст, не больш за {count} гульні} many{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў} other{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў}}", + "timeagoJustNow": "зараз", + "timeagoRightNow": "прама зараз", + "timeagoCompleted": "завершана", + "timeagoInNbSeconds": "{count, plural, =1{праз {count} секунду} few{праз {count} секунды} many{праз {count} секунд} other{праз {count} секунд}}", + "timeagoInNbMinutes": "{count, plural, =1{праз {count} хвіліну} few{праз {count} хвіліны} many{праз {count} хвілін} other{праз {count} хвілін}}", + "timeagoInNbHours": "{count, plural, =1{праз {count} гадзіну} few{праз {count} гадзіны} many{праз {count} гадзін} other{праз {count} гадзін}}", + "timeagoInNbDays": "{count, plural, =1{праз {count} дзень} few{праз {count} дні} many{праз {count} дзён} other{праз {count} дзён}}", + "timeagoInNbWeeks": "{count, plural, =1{праз {count} тыдзень} few{праз {count} тыдні} many{праз {count} тыдняў} other{праз {count} тыдняў}}", + "timeagoInNbMonths": "{count, plural, =1{праз {count} месяц} few{праз {count} месяцы} many{праз {count} месяцаў} other{праз {count} месяцаў}}", + "timeagoInNbYears": "{count, plural, =1{праз {count} год} few{праз {count} гады} many{праз {count} гадоў} other{праз {count} гадоў}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} хвіліну таму} few{{count} хвіліны таму} many{{count} хвілін таму} other{{count} хвілін таму}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} гадзіну таму} few{{count} гадзіны таму} many{{count} гадзін таму} other{{count} гадзін таму}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} дзень таму} few{{count} дні таму} many{{count} дзён таму} other{{count} дзён таму}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} тыдзень таму} few{{count} тыдні таму} many{{count} тыдняў таму} other{{count} тыдняў таму}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} месяц таму} few{{count} месяцы таму} many{{count} месяцаў таму} other{{count} месяцаў таму}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} год таму} few{{count} гады таму} many{{count} гадоў таму} other{{count} гадоў таму}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Засталася {count} хвіліна} few{Засталося {count} хвіліны} many{Засталося {count} хвілін} other{Засталося {count} хвіліны}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Засталася {count} гадзіна} few{Засталося {count} гадзіны} many{Засталося {count} гадзін} other{Засталося {count} гадзіны}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bg.arb b/lib/l10n/lila_bg.arb index 18d2310883..830aa781cb 100644 --- a/lib/l10n/lila_bg.arb +++ b/lib/l10n/lila_bg.arb @@ -1,28 +1,33 @@ { + "mobileAllGames": "Всички игри", + "mobileAreYouSure": "Сигурни ли сте?", + "mobileClearButton": "Изчисти", + "mobileFeedbackButton": "Отзиви", + "mobileGreeting": "Здравейте, {param}", + "mobileGreetingWithoutName": "Здравейте", + "mobileHideVariation": "Скрий вариацията", "mobileHomeTab": "Начало", - "mobilePuzzlesTab": "Задачи", - "mobileToolsTab": "Анализ", - "mobileWatchTab": "Гледай", - "mobileSettingsTab": "Настройки", "mobileMustBeLoggedIn": "За да видите тази страница, трябва да влезете в профила си.", - "mobileSystemColors": "Системни цветове", - "mobileFeedbackButton": "Отзиви", + "mobileNoSearchResults": "Няма резултати", "mobileOkButton": "ОК", + "mobilePuzzleStormSubtitle": "Решете колкото можете повече задачи за 3 минути.", + "mobilePuzzleThemesSubtitle": "Решавайте задачи от любимите Ви дебюти или изберете друга тема.", + "mobilePuzzlesTab": "Задачи", + "mobileRecentSearches": "Последни търсения", "mobileSettingsHapticFeedback": "Вибрация при докосване", "mobileSettingsImmersiveMode": "Режим \"Цял екран\"", - "mobileAllGames": "Всички игри", - "mobileRecentSearches": "Последни търсения", - "mobileClearButton": "Изчисти", - "mobileNoSearchResults": "Няма резултати", - "mobileAreYouSure": "Сигурни ли сте?", - "mobileSharePuzzle": "Сподели тази задача", - "mobileShareGameURL": "Сподели URL на играта", + "mobileSettingsTab": "Настройки", "mobileShareGamePGN": "Сподели PGN", + "mobileShareGameURL": "Сподели URL на играта", "mobileSharePositionAsFEN": "Сподели позицията във формат FEN", - "mobileShowVariations": "Покажи вариациите", - "mobileHideVariation": "Скрий вариацията", + "mobileSharePuzzle": "Сподели тази задача", "mobileShowComments": "Покажи коментарите", + "mobileShowResult": "Покажи резултат", + "mobileShowVariations": "Покажи вариациите", "mobileSomethingWentWrong": "Възникна грешка.", + "mobileSystemColors": "Системни цветове", + "mobileToolsTab": "Анализ", + "mobileWatchTab": "Гледай", "activityActivity": "Дейност", "activityHostedALiveStream": "Стартира предаване на живо", "activityRankedInSwissTournament": "Рейтинг #{param1} от {param2}", @@ -35,6 +40,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Изигра {count} ход} other{Изигра {count} хода}}", "activityInNbCorrespondenceGames": "{count, plural, =1{в {count} кореспондентска игра} other{в {count} кореспондентски игри}}", "activityCompletedNbGames": "{count, plural, =1{Завърши {count} кореспондентна игра} other{Завършил {count} кореспондентни игри}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Изиграна {count} {param2} кореспондентска игра} other{Изиграни {count} {param2} кореспондентски игри}}", "activityFollowedNbPlayers": "{count, plural, =1{Последва {count} играч} other{Следва {count} играчи}}", "activityGainedNbFollowers": "{count, plural, =1{Прибави {count} нов последовател} other{Прибави {count} нови последователи}}", "activityHostedNbSimuls": "{count, plural, =1{Бе домакин на {count} шахматен сеанс} other{Домакин на {count} шахматни сеанса}}", @@ -45,7 +51,44 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Участва в {count} турнир по швейцарската система} other{Участва в {count} турнира по швейцарската система}}", "activityJoinedNbTeams": "{count, plural, =1{Присъедини се в {count} отбор} other{Присъедини се в {count} отбори}}", "broadcastBroadcasts": "Излъчване", + "broadcastMyBroadcasts": "Моите излъчвания", "broadcastLiveBroadcasts": "Излъчвания на турнир на живо", + "broadcastBroadcastCalendar": "Календар на излъчванията", + "broadcastNewBroadcast": "Нови предавания на живо", + "broadcastSubscribedBroadcasts": "Излчвания които следя", + "broadcastAddRound": "Добави рунд", + "broadcastOngoing": "Текущи", + "broadcastUpcoming": "Предстоящи", + "broadcastCompleted": "Завършени", + "broadcastRoundName": "Име на рунда", + "broadcastRoundNumber": "Номер на рунда", + "broadcastTournamentName": "Име на турнира", + "broadcastTournamentDescription": "Кратко описание на турнира", + "broadcastFullDescription": "Пълно описание на събитието", + "broadcastFullDescriptionHelp": "Незадължително дълго описание на излъчването. {param1} е налично. Дължината трябва да по-малка от {param2} знака.", + "broadcastSourceUrlHelp": "Уебадресът, който Lichess ще проверява, за да получи осъвременявания на PGN. Той трябва да е публично достъпен от интернет.", + "broadcastStartDateHelp": "По избор, ако знаете, кога започва събитието", + "broadcastCurrentGameUrl": "URL на настоящата партия", + "broadcastDownloadAllRounds": "Изтегли всички рундове", + "broadcastResetRound": "Нулирай този рунд", + "broadcastDeleteRound": "Изтрий този рунд", + "broadcastDefinitivelyDeleteRound": "Окончателно изтрийте този рунд и всичките му игри.", + "broadcastDeleteAllGamesOfThisRound": "Изтрийте този рунд и всичките му игри. Източникът трябва да е активен за да можете да ги възстановите.", + "broadcastDeleteTournament": "Изтрий този турнир", + "broadcastDefinitivelyDeleteTournament": "Окончателно изтрий целия турнир, всичките му рундове и игри.", + "broadcastReplacePlayerTags": "По избор: промени имената на играчите, рейтингите и титлите", + "broadcastFideFederations": "ФИДЕ федерации", + "broadcastFideProfile": "ФИДЕ профил", + "broadcastFederation": "Федерация", + "broadcastOpenLichess": "Отвори в Lichess", + "broadcastTeams": "Отбори", + "broadcastBoards": "Дъски", + "broadcastOverview": "Общ преглед", + "broadcastOfficialWebsite": "Официален уебсайт", + "broadcastStandings": "Класиране", + "broadcastGamesThisTournament": "Игри в този турнир", + "broadcastScore": "Резултат", + "broadcastNbBroadcasts": "{count, plural, =1{{count} излъчване} other{{count} излъчвания}}", "challengeChallengesX": "Предизвикателства: {param1}", "challengeChallengeToPlay": "Предизвикайте на партия", "challengeChallengeDeclined": "Предизвикателството е отказано", @@ -364,8 +407,8 @@ "puzzleThemeXRayAttackDescription": "Фигура атакува или защитава поле зад противникова фигура.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Опонентът има малко възможни ходове и всеки един от тях води до влошаване на положението му.", - "puzzleThemeHealthyMix": "От всичко по малко", - "puzzleThemeHealthyMixDescription": "По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.", + "puzzleThemeMix": "От всичко по малко", + "puzzleThemeMixDescription": "По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.", "puzzleThemePlayerGames": "Партии на играча", "puzzleThemePlayerGamesDescription": "Разгледайте пъзели генерирани от вашите игри, или игрите на други играчи.", "puzzleThemePuzzleDownloadInformation": "Тези пъзели са публични и могат да бъдат изтеглени от {param}.", @@ -486,7 +529,6 @@ "replayMode": "Режим на повторение", "realtimeReplay": "В реално време", "byCPL": "По CPL", - "openStudy": "Проучване", "enable": "Включване", "bestMoveArrow": "Показване на най-добър ход", "showVariationArrows": "Показване на стрелки за вариациите", @@ -496,7 +538,6 @@ "memory": "Памет", "infiniteAnalysis": "Неограничен анализ", "removesTheDepthLimit": "Анализът ще е безкраен, а компютърът ви - топъл", - "engineManager": "Мениджър на двигателя", "blunder": "Груба грешка", "mistake": "Грешка", "inaccuracy": "Неточност", @@ -578,6 +619,7 @@ "rank": "Класация", "rankX": "Класация: {param}", "gamesPlayed": "Изиграни игри", + "ok": "ОК", "cancel": "Отказ", "whiteTimeOut": "Времето на белите изтече", "blackTimeOut": "Времето на черните изтече", @@ -692,7 +734,6 @@ "block": "Блокирай", "blocked": "Блокирани", "unblock": "Отблокирай", - "followsYou": "Следва ви", "xStartedFollowingY": "{param1} започна да следва {param2}", "more": "Още", "memberSince": "Член от", @@ -795,7 +836,6 @@ "cheat": "Измама", "troll": "Вредител", "other": "Друго", - "reportDescriptionHelp": "Поставете линк към играта и обяснете какъв е проблемът с поведението на този потребител. Не казвайте единствено, че мами, но ни кажете как сте стигнали до този извод. Вашият доклад ще бъде обработен по-бързо, ако е написан на английски.", "error_provideOneCheatedGameLink": "Моля дай поне един линк до измамна игра.", "by": "от {param}", "importedByX": "Внесени от {param}", @@ -1184,6 +1224,7 @@ "instructions": "Инструкции", "showMeEverything": "Покажи ми всичко", "lichessPatronInfo": "Lichess е благотворителна организация и работи с напълно безплатен софтуер и отворен код. Всички разходи за опериране, разработка и съдържание са финансирани единствено от дарения от потребителите ни.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Опонентът напусна играта. Можете да заявите победа след {count} секунди.} other{Опонентът напусна играта. Можете да заявите победа след {count} секунди.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} полуход} other{Мат в {count} полухода}}", "nbBlunders": "{count, plural, =1{{count} груба грешка} other{{count} груби грешки}}", @@ -1280,6 +1321,176 @@ "stormXRuns": "{count, plural, =1{1 опит} other{{count} опита}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Изигран един опит от {param2}} other{Изиграни {count} опита от {param2}}}", "streamerLichessStreamers": "Lichess стриймъри", + "studyPrivate": "Лични", + "studyMyStudies": "Моите казуси", + "studyStudiesIContributeTo": "Казуси, към които допринасям", + "studyMyPublicStudies": "Моите публични казуси", + "studyMyPrivateStudies": "Моите лични казуси", + "studyMyFavoriteStudies": "Моите любими казуси", + "studyWhatAreStudies": "Какво представляват казусите?", + "studyAllStudies": "Всички казуси", + "studyStudiesCreatedByX": "Казуси от {param}", + "studyNoneYet": "Все още няма.", + "studyHot": "Популярни", + "studyDateAddedNewest": "Дата на добавяне (най-нови)", + "studyDateAddedOldest": "Дата на добавяне (най-стари)", + "studyRecentlyUpdated": "Скоро обновени", + "studyMostPopular": "Най-популярни", + "studyAlphabetical": "Азбучно", + "studyAddNewChapter": "Добавяне на нов раздел", + "studyAddMembers": "Добави членове", + "studyInviteToTheStudy": "Покани към казуса", + "studyPleaseOnlyInvitePeopleYouKnow": "Моля канете само хора, които познавате и които биха искали да се присъединят.", + "studySearchByUsername": "Търсене по потребителско име", + "studySpectator": "Зрител", + "studyContributor": "Сътрудник", + "studyKick": "Изритване", + "studyLeaveTheStudy": "Напусни казуса", + "studyYouAreNowAContributor": "Вие сте сътрудник", + "studyYouAreNowASpectator": "Вие сте зрител", + "studyPgnTags": "PGN тагове", + "studyLike": "Харесай", + "studyUnlike": "Не харесвам", + "studyNewTag": "Нов таг", + "studyCommentThisPosition": "Коментирай позицията", + "studyCommentThisMove": "Коментирай хода", + "studyAnnotateWithGlyphs": "Анотация със специални символи", + "studyTheChapterIsTooShortToBeAnalysed": "Тази глава е твърде къса и не може да бъде анализирана.", + "studyOnlyContributorsCanRequestAnalysis": "Само сътрудници към казуса могат да пускат компютърен анализ.", + "studyGetAFullComputerAnalysis": "Вземи пълен сървърен анализ на основна линия.", + "studyMakeSureTheChapterIsComplete": "Уверете се, че главата е завършена. Можете да пуснете анализ само веднъж.", + "studyAllSyncMembersRemainOnTheSamePosition": "Всички синхронизирани членове остават на същата позиция", + "studyShareChanges": "Споделете промените със зрителите и ги запазете на сървъра", + "studyPlaying": "Играе се", + "studyFirst": "Първа", + "studyPrevious": "Предишна", + "studyNext": "Следваща", + "studyLast": "Последна", "studyShareAndExport": "Сподели", - "studyStart": "Начало" + "studyCloneStudy": "Клонирай", + "studyStudyPgn": "PGN на казуса", + "studyDownloadAllGames": "Изтегли всички партии", + "studyChapterPgn": "PGN на главата", + "studyCopyChapterPgn": "Копирай PGN", + "studyDownloadGame": "Изтегли партия", + "studyStudyUrl": "URL на казуса", + "studyCurrentChapterUrl": "URL на настоящата глава", + "studyYouCanPasteThisInTheForumToEmbed": "Можете да поставите това във форум и ще бъде вградено", + "studyStartAtInitialPosition": "Започни от начална позиция", + "studyStartAtX": "Започни от {param}", + "studyEmbedInYourWebsite": "Вгради в твоя сайт или блог", + "studyReadMoreAboutEmbedding": "Прочети повече за вграждането", + "studyOnlyPublicStudiesCanBeEmbedded": "Само публични казуси могат да бъдат вграждани!", + "studyOpen": "Отвори", + "studyXBroughtToYouByY": "{param1}, предоставени от {param2}", + "studyStudyNotFound": "Казусът не бе открит", + "studyEditChapter": "Промени глава", + "studyNewChapter": "Нова глава", + "studyImportFromChapterX": "Импортиране от {param}", + "studyOrientation": "Ориентация", + "studyAnalysisMode": "Режим на анализ", + "studyPinnedChapterComment": "Коментар на главата", + "studySaveChapter": "Запази глава", + "studyClearAnnotations": "Изтрий анотациите", + "studyClearVariations": "Изчисти вариациите", + "studyDeleteChapter": "Изтрий глава", + "studyDeleteThisChapter": "Изтриване на главата? Това е необратимо!", + "studyClearAllCommentsInThisChapter": "Изтрий всички коментари, специални символи и нарисувани форми в главата?", + "studyRightUnderTheBoard": "Точно под дъската", + "studyNoPinnedComment": "Никакви", + "studyNormalAnalysis": "Нормален анализ", + "studyHideNextMoves": "Скриване на следващите ходове", + "studyInteractiveLesson": "Интерактивен урок", + "studyChapterX": "Глава: {param}", + "studyEmpty": "Празна", + "studyStartFromInitialPosition": "Започни от начална позиция", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Започни от избрана позиция", + "studyLoadAGameByUrl": "Зареди партии от URL", + "studyLoadAPositionFromFen": "Зареди позиция от FEN", + "studyLoadAGameFromPgn": "Зареди партии от PGN", + "studyAutomatic": "Автоматичен", + "studyUrlOfTheGame": "URL на партиите, по една на линия", + "studyLoadAGameFromXOrY": "Зареди партии от {param1} или {param2}", + "studyCreateChapter": "Създай", + "studyCreateStudy": "Създай казус", + "studyEditStudy": "Редактирай казус", + "studyVisibility": "Видимост", + "studyPublic": "Публични", + "studyUnlisted": "Несподелени", + "studyInviteOnly": "Само с покани", + "studyAllowCloning": "Позволи клониране", + "studyNobody": "Никой", + "studyOnlyMe": "Само за мен", + "studyContributors": "Сътрудници", + "studyMembers": "Членове", + "studyEveryone": "Всички", + "studyEnableSync": "Разреши синхронизиране", + "studyYesKeepEveryoneOnTheSamePosition": "Да: дръж всички на същата позиция", + "studyNoLetPeopleBrowseFreely": "Не: позволи свободно разглеждане", + "studyPinnedStudyComment": "Коментар на казуса", + "studyStart": "Начало", + "studySave": "Запази", + "studyClearChat": "Изтрий чат съобщенията", + "studyDeleteTheStudyChatHistory": "Изтриване на чат историята? Това е необратимо!", + "studyDeleteStudy": "Изтрий казуса", + "studyConfirmDeleteStudy": "Изтриване на целия казус? Това е необратимо! Въведете името на казуса за да потвърдите: {param}", + "studyWhereDoYouWantToStudyThat": "Къде да бъде проучено това?", + "studyGoodMove": "Добър ход", + "studyMistake": "Грешка", + "studyBrilliantMove": "Отличен ход", + "studyBlunder": "Груба грешка", + "studyInterestingMove": "Интересен ход", + "studyDubiousMove": "Съмнителен ход", + "studyOnlyMove": "Единствен ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Равна позиция", + "studyUnclearPosition": "Неясна позиция", + "studyWhiteIsSlightlyBetter": "Белите са малко по-добре", + "studyBlackIsSlightlyBetter": "Черните са малко по-добре", + "studyWhiteIsBetter": "Белите са по-добре", + "studyBlackIsBetter": "Черните са по-добре", + "studyWhiteIsWinning": "Белите печелят", + "studyBlackIsWinning": "Черните печелят", + "studyNovelty": "Нововъведeние", + "studyDevelopment": "Развитие", + "studyInitiative": "Инициатива", + "studyAttack": "Атака", + "studyCounterplay": "Контра атака", + "studyTimeTrouble": "Проблем с времето", + "studyWithCompensation": "С компенсация", + "studyWithTheIdea": "С идеята", + "studyNextChapter": "Следваща глава", + "studyPrevChapter": "Предишна глава", + "studyStudyActions": "Опции за учене", + "studyTopics": "Теми", + "studyMyTopics": "Моите теми", + "studyPopularTopics": "Популярни теми", + "studyManageTopics": "Управление на темите", + "studyBack": "Обратно", + "studyPlayAgain": "Играйте отново", + "studyWhatWouldYouPlay": "Какво бихте играли в тази позиция?", + "studyYouCompletedThisLesson": "Поздравления! Вие завършихте този урок.", + "studyNbChapters": "{count, plural, =1{{count} Глава} other{{count} Глави}}", + "studyNbGames": "{count, plural, =1{{count} Игра} other{{count} Игри}}", + "studyNbMembers": "{count, plural, =1{{count} Член} other{{count} Членове}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Постави твоя PGN текст тук, до {count} партия} other{Постави твоя PGN текст тук, до {count} партии}}", + "timeagoJustNow": "току що", + "timeagoRightNow": "точно сега", + "timeagoCompleted": "завършено", + "timeagoInNbSeconds": "{count, plural, =1{след {count} секунда} other{след {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{след {count} минута} other{след {count} минути}}", + "timeagoInNbHours": "{count, plural, =1{след {count} час} other{след {count} часа}}", + "timeagoInNbDays": "{count, plural, =1{след {count} ден} other{след {count} дни}}", + "timeagoInNbWeeks": "{count, plural, =1{след {count} седмица} other{след {count} седмици}}", + "timeagoInNbMonths": "{count, plural, =1{след {count} месец} other{след {count} месеца}}", + "timeagoInNbYears": "{count, plural, =1{след {count} година} other{след {count} години}}", + "timeagoNbMinutesAgo": "{count, plural, =1{преди {count} минута} other{преди {count} минути}}", + "timeagoNbHoursAgo": "{count, plural, =1{преди {count} час} other{Преди {count} часа}}", + "timeagoNbDaysAgo": "{count, plural, =1{преди {count} ден} other{Преди {count} дни}}", + "timeagoNbWeeksAgo": "{count, plural, =1{преди {count} седмица} other{преди {count} седмици}}", + "timeagoNbMonthsAgo": "{count, plural, =1{преди {count} месец} other{преди {count} месеца}}", + "timeagoNbYearsAgo": "{count, plural, =1{преди {count} година} other{преди {count} години}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{остава {count} минутa} other{остават {count} минути}}", + "timeagoNbHoursRemaining": "{count, plural, =1{остава {count} час} other{остават {count} часа}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bn.arb b/lib/l10n/lila_bn.arb index 2773f19bca..f31b663c60 100644 --- a/lib/l10n/lila_bn.arb +++ b/lib/l10n/lila_bn.arb @@ -22,6 +22,13 @@ "activityJoinedNbTeams": "{count, plural, =1{যুক্ত হয়েছে {count} দল} other{যুক্ত হয়েছে {count} দল}}", "broadcastBroadcasts": "সম্প্রচার", "broadcastLiveBroadcasts": "সরাসরি টুর্নামেন্ট সম্প্রচার", + "broadcastNewBroadcast": "নতুন সরাসরি সম্প্রচার", + "broadcastOngoing": "চলমান", + "broadcastUpcoming": "আসন্ন", + "broadcastCompleted": "সমাপ্ত", + "broadcastRoundNumber": "গোল নম্বর", + "broadcastFullDescription": "ইভেন্টের সম্পূর্ণ বিবরণ", + "broadcastSourceUrlHelp": "ইউআরএল যা লাইসেন্সেস পিজিএন আপডেট পেতে চেক করবে। এটি অবশ্যই ইন্টারনেট থেকে সর্বজনীনভাবে অ্যাক্সেসযোগ্য।.", "challengeChallengesX": "প্রতিদ্বন্দ্বীরা:{param1}", "challengeChallengeToPlay": "খেলার জন্য চ্যালেঞ্জ করুন", "challengeChallengeDeclined": "চ্যালেঞ্জ প্রত্যাখ্যান করা হয়েছে", @@ -285,8 +292,8 @@ "puzzleThemeXRayAttackDescription": "একটি গুটি যখন প্রতিপক্ষের গুটির ভিতর দিয়ে কোনো ঘরকে রক্ষা বা আক্রমন করে।", "puzzleThemeZugzwang": "জুগজওয়াং(Zugzwang)", "puzzleThemeZugzwangDescription": "প্রতিপক্ষের সীমিত চাল আছে, এবং সব চাল তাদের অবস্থান আরো খারাপ করবে।", - "puzzleThemeHealthyMix": "পরিমিত মিশ্রণ", - "puzzleThemeHealthyMixDescription": "সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।", + "puzzleThemeMix": "পরিমিত মিশ্রণ", + "puzzleThemeMixDescription": "সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।", "puzzleThemePlayerGames": "খেলোয়ারদের খেলা হতে", "puzzleThemePlayerGamesDescription": "খেলোয়ারদের খেলা থেকে বাছাই করে তৈরি করা ধাঁধা।", "puzzleThemePuzzleDownloadInformation": "এই ধাঁধা গুলো সবার জন্য উন্মুক্ত এবং {param} থেকে নামিয়ে নেয়া যাবে।", @@ -365,6 +372,7 @@ "promoteVariation": "বিজ্ঞাপন", "makeMainLine": "প্রাধান সীমা করা", "deleteFromHere": "এখান থেকে মুছুন", + "collapseVariations": "ভেরিয়েশন সঙ্কুচিত করুন", "forceVariation": "জোর করে পরিবর্তন", "move": "চাল", "variantLoss": "ভিন্ন ক্ষতি", @@ -402,7 +410,6 @@ "replayMode": "উত্তর ধরন", "realtimeReplay": "সঠিকসময়", "byCPL": "CPL দ্বারা", - "openStudy": "মুক্ত অধ্যয়ন", "enable": "সচল", "bestMoveArrow": "সরানোর উত্তম চিহ্ন", "showVariationArrows": "বৈচিত্র্য তীর দেখান", @@ -412,7 +419,6 @@ "memory": "মেমরি", "infiniteAnalysis": "অনন্ত বিশ্লেষণ", "removesTheDepthLimit": "গভীরতার সীমা অপসারণ করুন এবং আপনার কম্পিউটারকে গরম রাখুন", - "engineManager": "ইঞ্জিন ম্যানেজার", "blunder": "গুরুতর ভুল", "mistake": "ভুল", "inaccuracy": "অশুদ্ধি", @@ -438,6 +444,7 @@ "latestForumPosts": "সর্বশেষ ফোরাম বার্তা", "players": "খেলোয়াড়", "friends": "বন্ধুরা", + "otherPlayers": "অন্যান্য খেলোয়াড়", "discussions": "বার্তাগুলি", "today": "আজ", "yesterday": "গতকাল", @@ -608,7 +615,6 @@ "block": "বাধা দিন", "blocked": "বাধাগ্রস্ত", "unblock": "বাধা উঠিয়ে নিন", - "followsYou": "আপনাকে অনুসরণ করছে", "xStartedFollowingY": "{param1} অনুসরণ করা শুরু করেছেন {param2}", "more": "আরও", "memberSince": "সদস্য রয়েছেন", @@ -693,6 +699,7 @@ "descPrivateHelp": "যে লেখা শুধু দলের সদস্য দেখতে পাবে.", "no": "না", "yes": "হ্যা", + "mobile": "মোবাইল", "help": "সাহায্য", "createANewTopic": "নতুন বিষয় তৈরি করুন", "topics": "বিষয়", @@ -711,7 +718,6 @@ "cheat": "চিটিং করছে", "troll": "ব্যঙ্গ করছে", "other": "অন্য কোনো কারণ", - "reportDescriptionHelp": "এখানে সেই খেলাটির link দেন এবং বলুন ওই ব্যক্তি ব্যবহারে কি অসুবিধা ছিল ?", "error_provideOneCheatedGameLink": "অনুগ্রহ করে একটা চিটেড গেমের লিংক দিন।", "by": "{param} এর দ্বারা", "importedByX": "খেলাটি এনেছেন {param}", @@ -1169,5 +1175,112 @@ "stormClickToReload": "পুনঃলোড করতে ক্লিক করুন", "stormXRuns": "{count, plural, =1{১ দউর} other{{count} দউর}}", "streamerLichessStreamers": "লিছেসস স্ত্রেয়ামের", - "studyStart": "শুরু করুন" + "studyPrivate": "ব্যাক্তিগত", + "studyMyStudies": "আমার অধ্যায়ন", + "studyStudiesIContributeTo": "যেসকল অধ্যায়নে আমার অবদান রয়েছে", + "studyMyPublicStudies": "জনসাধারনকৃত আমার অধ্যায়নগুলো", + "studyMyPrivateStudies": "আমার ব্যাক্তিগত অধ্যায়ন", + "studyMyFavoriteStudies": "আমার পছন্দের অধ্যায়ন", + "studyWhatAreStudies": "অধ্যায়ন কি?", + "studyAllStudies": "সকল অধ্যায়নগুলি", + "studyStudiesCreatedByX": "অধ্যায়ন তৈরি করেছেন {param}", + "studyNoneYet": "আপাতত নেই।", + "studyHot": "গরমাগরম", + "studyDateAddedNewest": "তৈরির তারিখ (সবচেয়ে নতুন)", + "studyDateAddedOldest": "তৈরির তারিখ (সবচেয়ে পুরনো)", + "studyRecentlyUpdated": "সাম্প্রতিক হালনাগাদকৃত", + "studyMostPopular": "সবচেয়ে জনপ্রিয়", + "studyAlphabetical": "বর্ণানুক্রমিক", + "studyAddNewChapter": "নতুন অধ্যায় যোগ করুন", + "studyAddMembers": "সদস্য যোগ করুন", + "studyInviteToTheStudy": "স্টাডিতে আমন্ত্রণ জানান", + "studyPleaseOnlyInvitePeopleYouKnow": "দয়া করে যাদের আপনি জানেন তাদের এবং যারা সক্রিয়ভাবে যোগদান করতে চায়, কেবল তাদেরকেই আমন্ত্রন জানান।", + "studySearchByUsername": "ইউজারনেম দ্বারা খুঁজুন", + "studySpectator": "দর্শক", + "studyContributor": "অবদানকারী", + "studyKick": "লাথি দিয়ে বের করুন", + "studyYouAreNowASpectator": "আপনি এখন দর্শক", + "studyPgnTags": "PGN ট্যাগ", + "studyLike": "পছন্দ করা", + "studyUnlike": "পছন্দ নয়", + "studyNewTag": "নতুন ট্যাগ", + "studyTheChapterIsTooShortToBeAnalysed": "এনালাইসিস করার জন্য চ্যাপ্টারটা খুব ছোট", + "studyOnlyContributorsCanRequestAnalysis": "শুধুমাত্র স্টাডি'টার কন্ট্রিবিউটররাই কম্পিউটার এনালাইসিস এর রিকোয়েস্ট করতে পারবে।", + "studyPlaying": "খেলছে", + "studyFirst": "সর্ব প্রথম", + "studyPrevious": "আগের ধাপ", + "studyNext": "পরের ধাপ", + "studyLast": "সর্বশেষ", + "studyStudyPgn": "অধ্যায়ন PGN আকারে", + "studyDownloadAllGames": "ডাউনলোড করুন সকল গেম", + "studyOpen": "ওপেন", + "studyStartFromCustomPosition": "নির্দিষ্ট অবস্থান থেকে শুরু করুন", + "studyLoadAGameByUrl": "URL থেকে খেলা লোড করুন", + "studyLoadAPositionFromFen": "FEN থেকে একটি অবস্থান লোড করুন", + "studyLoadAGameFromPgn": "PGN থেকে খেলা লোড করুন", + "studyAutomatic": "স্বয়ংক্রিয়", + "studyUrlOfTheGame": "খেলাগুলোর URL, লাইনপ্রতি একটি", + "studyLoadAGameFromXOrY": "{param1} অথবা {param2} থেকে খেলাসমূহ লোড করুন", + "studyCreateChapter": "অধ্যায় তৈরি করুন", + "studyCreateStudy": "স্টাডি তৈরি করুন", + "studyEditStudy": "স্টাডি সম্পাদনা করুন", + "studyVisibility": "দৃশ্যমানতা", + "studyPublic": "পাবলিক", + "studyUnlisted": "প্রাইভেট", + "studyInviteOnly": "কেবল আমন্ত্রনভিত্তিক", + "studyAllowCloning": "ক্লোন করার অনুমতি দিন", + "studyNobody": "কেউ না", + "studyOnlyMe": "শুধু আমি", + "studyContributors": "অবদানকারীরা", + "studyMembers": "সদস্যবৃন্দ", + "studyEveryone": "সবাই", + "studyEnableSync": "সাইনক চালু করুন", + "studyYesKeepEveryoneOnTheSamePosition": "সবাইকে একই অবস্থানে রাখুন", + "studyNoLetPeopleBrowseFreely": "না: মানুষকে মুক্তভাবে ব্রাউজ করতে দিন", + "studyPinnedStudyComment": "পিন করা স্টাডি মন্তব্য", + "studyStart": "শুরু করুন", + "studySave": "সংরক্ষন করুন", + "studyClearChat": "চ্যাট পরিষ্কার করুন", + "studyDeleteTheStudyChatHistory": "স্টাডি চ্যাটের ইতিহাস মুছে ফেলবেন? এটা কিন্তু ফিরে আসবে না!", + "studyDeleteStudy": "স্টাডি মুছে ফেলুন", + "studyWhereDoYouWantToStudyThat": "আপনি কোথায় এটা চর্চা করবেন?", + "studyGoodMove": "ভালো চাল", + "studyMistake": "ভূল চাল", + "studyBrilliantMove": "অসাধারণ চাল", + "studyBlunder": "ব্লান্ডার", + "studyInterestingMove": "আগ্রহোদ্দীপক চাল", + "studyDubiousMove": "অনিশ্চিত চাল", + "studyOnlyMove": "একমাত্র সম্ভাব্য চাল", + "studyZugzwang": "যুগযোয়াং", + "studyEqualPosition": "সমান অবস্থান", + "studyUnclearPosition": "অনিশ্চিত অবস্থান", + "studyWhiteIsSlightlyBetter": "সাদা একটু বেশি ভালো", + "studyBlackIsSlightlyBetter": "কালো একটু বেশি ভালো", + "studyWhiteIsBetter": "সাদা ভালো", + "studyBlackIsBetter": "কালো ভালো", + "studyWhiteIsWinning": "সাদা জিতছে", + "studyBlackIsWinning": "কালো জিতছে", + "studyNovelty": "নোভেল্টি", + "studyNbChapters": "{count, plural, =1{{count}টি অধ্যায়} other{{count}টি অধ্যায়}}", + "studyNbGames": "{count, plural, =1{{count}টি খেলা} other{{count}টি খেলা}}", + "studyNbMembers": "{count, plural, =1{{count} জন সদস্য} other{{count} জন সদস্য}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত} other{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত}}", + "timeagoJustNow": "এখনই", + "timeagoRightNow": "এই মুহূর্তে", + "timeagoCompleted": "সম্পন্ন হয়েছে", + "timeagoInNbSeconds": "{count, plural, =1{{count} সেকেন্ডের মধ্যে} other{{count} সেকেন্ডের মধ্যে}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} মিনিটের মধ্যে} other{{count} মিনিটের মধ্যে}}", + "timeagoInNbHours": "{count, plural, =1{{count} ঘন্টার মধ্যে} other{{count} ঘন্টার মধ্যে}}", + "timeagoInNbDays": "{count, plural, =1{{count} দিনের মধ্যে} other{{count} দিনের মধ্যে}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} সপ্তাহের মধ্যে} other{{count} সপ্তাহের মধ্যে}}", + "timeagoInNbMonths": "{count, plural, =1{{count} মাসের মধ্যে} other{{count} মাসের মধ্যে}}", + "timeagoInNbYears": "{count, plural, =1{{count} বছরের মধ্যে} other{{count} বছরের মধ্যে}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} মিনিট আগে} other{{count} মিনিট আগে}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ঘণ্টা আগে} other{{count} ঘন্টা আগে}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} দিন আগে} other{{count} দিন আগে}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} সপ্তাহ আগে} other{{count} সপ্তাহ আগে}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} মাস আগে} other{{count} মাস আগে}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} বছর আগে} other{{count} বছর আগে}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} মিনিট বাকি} other{{count} মিনিট বাকি}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ঘন্টা বাকি} other{{count} ঘন্টা বাকি}}" } \ No newline at end of file diff --git a/lib/l10n/lila_br.arb b/lib/l10n/lila_br.arb index a3f3cbed35..e8fa88e269 100644 --- a/lib/l10n/lila_br.arb +++ b/lib/l10n/lila_br.arb @@ -22,6 +22,17 @@ "activityJoinedNbTeams": "{count, plural, =1{Ezel eus {count} skipailh} =2{Ezel eus {count} skipailh} few{Ezel eus {count} skipailh} many{Ezel eus {count} skipailh} other{Ezel eus {count} skipailh}}", "broadcastBroadcasts": "War-eeun", "broadcastLiveBroadcasts": "Tournamantoù skignet war-eeun", + "broadcastNewBroadcast": "Skignañ war-eeun nevez", + "broadcastOngoing": "O ren", + "broadcastUpcoming": "A-benn nebeut", + "broadcastCompleted": "Tremenet", + "broadcastRoundNumber": "Niverenn ar batalm", + "broadcastFullDescription": "Deskrivadur an abadenn a-bezh", + "broadcastFullDescriptionHelp": "Deskrivadur hir ar skignañ war-eeun ma fell deoc'h.{param1} zo dijabl. Ne vo ket hiroc'h evit {param2} sin.", + "broadcastSourceUrlHelp": "An URL a ray Lichess ganti evit kaout hizivadurioù ar PGN. Ret eo dezhi bezañ digor d'an holl war Internet.", + "broadcastStartDateHelp": "Diret eo, ma ouzit pegoulz e krogo", + "broadcastDeleteTournament": "Dilemel an tournamant-mañ", + "broadcastDefinitivelyDeleteTournament": "Dilemel an tournamant da viken, an holl grogadoù ha pep tra penn-da-benn.", "challengeChallengesX": "Daeoù: {param1}", "challengeChallengeToPlay": "Daeañ ar c'hoarier-mañ", "challengeChallengeDeclined": "Dae nac’het", @@ -174,8 +185,8 @@ "puzzleThemeShort": "Poelladenn verr", "puzzleThemeVeryLong": "Poelladenn hir-tre", "puzzleThemeZugzwang": "Zugzwang", - "puzzleThemeHealthyMix": "A bep seurt", - "puzzleThemeHealthyMixDescription": "A bep seurt. N'ouzit ket petra gortoz hag e mod-se e voc'h prest evit pep tra! Heñvel ouzh ar c'hrogadoù gwir.", + "puzzleThemeMix": "A bep seurt", + "puzzleThemeMixDescription": "A bep seurt. N'ouzit ket petra gortoz hag e mod-se e voc'h prest evit pep tra! Heñvel ouzh ar c'hrogadoù gwir.", "searchSearch": "Klask", "settingsSettings": "Arventennoù", "settingsCloseAccount": "Serriñ ar gont", @@ -283,7 +294,6 @@ "replayMode": "Mod adwelet", "realtimeReplay": "Amzer wirion", "byCPL": "Dre CPL", - "openStudy": "Digeriñ ar studi", "enable": "Enaouiñ", "bestMoveArrow": "Bir ar gwellañ fiñvadenn", "evaluationGauge": "Jaoj priziañ", @@ -291,7 +301,6 @@ "cpus": "CPUs", "memory": "Memor", "infiniteAnalysis": "Dielfennañ didermen", - "engineManager": "Merañ an urzhiataer", "blunder": "Bourd", "mistake": "Fazi", "inaccuracy": "Diresisted", @@ -480,7 +489,6 @@ "block": "Stankañ", "blocked": "Stanket", "unblock": "Distankañ", - "followsYou": "Ho heuilh", "xStartedFollowingY": "{param1} zo krog da heuliañ {param2}", "more": "Muioc'h", "memberSince": "Ezel abaoe an/ar", @@ -575,7 +583,6 @@ "cheat": "Trucherezh", "troll": "Troll", "other": "All", - "reportDescriptionHelp": "Pegit liamm ar c'hrogad(où) ha displegit ar pezh a ya a-dreuz gant emzalc'h oc'h enebour. Lâret \"o truchañ emañ\" ne vo ket trawalc'h, ret eo displegañ mat. Buanoc'h e pledimp ganti ma skrivit e saozneg.", "error_provideOneCheatedGameLink": "Roit d'an nebeutañ ul liamm hag a gas d'ur c'hrogad trucherezh ennañ.", "by": "gant {param}", "importedByX": "Enportzhiet gant {param}", @@ -1010,6 +1017,135 @@ "stormAllTime": "A-viskoazh", "stormXRuns": "{count, plural, =1{1 frapad} =2{{count} frapad} few{{count} frapad} many{{count} frapad} other{{count} frapad}}", "streamerLichessStreamers": "Streamerien Lichess", + "studyPrivate": "Prevez", + "studyMyStudies": "Ma studiadennoù", + "studyStudiesIContributeTo": "Studiadennoù am eus kemeret perzh enne", + "studyMyPublicStudies": "Ma studiadennoù foran", + "studyMyPrivateStudies": "Ma studiadennoù prevez", + "studyMyFavoriteStudies": "Ma studiadennoù muiañ-karet", + "studyWhatAreStudies": "Petra eo ar studiadennoù?", + "studyAllStudies": "An holl studiadennoù", + "studyStudiesCreatedByX": "Studiadennoù krouet gant {param}", + "studyNoneYet": "Hini ebet evit poent.", + "studyHot": "Deus ar c'hiz", + "studyDateAddedNewest": "Deiziad ouzhpennet (nevesañ)", + "studyDateAddedOldest": "Deiziad ouzhpennet (koshañ)", + "studyRecentlyUpdated": "Hizivaet a-nevez", + "studyMostPopular": "Muiañ karet", + "studyAddNewChapter": "Ouzhpennañ ur pennad", + "studyAddMembers": "Ouzhpennañ izili", + "studyInviteToTheStudy": "Pediñ d'ar studiadenn", + "studyPleaseOnlyInvitePeopleYouKnow": "Na bedit nemet tud a anavezit hag o deus c'hoant da gemer perzh da vat en ho studiadenn.", + "studySearchByUsername": "Klask dre anv implijer", + "studySpectator": "Arvester", + "studyContributor": "Perzhiad", + "studyKick": "Forbannañ", + "studyLeaveTheStudy": "Kuitaat ar studiadenn", + "studyYouAreNowAContributor": "Perzhiad oc'h bremañ", + "studyYouAreNowASpectator": "Un arvester oc'h bremañ", + "studyPgnTags": "Tikedennoù PGN", + "studyLike": "Plijet", + "studyNewTag": "Tikedenn nevez", + "studyCommentThisPosition": "Lâret ur ger diwar-benn al lakadur-mañ", + "studyCommentThisMove": "Ober un evezhiadenn diwar-benn ar fiñvadenn-mañ", + "studyAnnotateWithGlyphs": "Notennaouiñ gant arouezioù", + "studyTheChapterIsTooShortToBeAnalysed": "Re verr eo ar pennad evit bezañ dielfennet.", + "studyOnlyContributorsCanRequestAnalysis": "N'eus nemet perzhidi ar studiadenn a c'hall goulenn un dielfennañ urzhiataer.", + "studyGetAFullComputerAnalysis": "Kaout un dielfennañ klok eus ar bennlinenn graet gant un urzhiataer.", + "studyMakeSureTheChapterIsComplete": "Bezit sur eo klok ar pennad. Ne c'hallit goulenn un dielfennañ nemet ur wech.", + "studyAllSyncMembersRemainOnTheSamePosition": "Er memes lec'hiadur e chom holl izili ar SYNC", + "studyShareChanges": "Rannañ cheñchamantoù gant an arvesterien ha saveteiñ anezhe war ar servor", + "studyPlaying": "O c'hoari", + "studyFirst": "Kentañ", + "studyPrevious": "War-gil", + "studyNext": "War-lec'h", + "studyLast": "Diwezhañ", "studyShareAndExport": "Skignañ & ezporzhiañ", - "studyStart": "Kregiñ" + "studyCloneStudy": "Eilañ", + "studyStudyPgn": "PGN ar studi", + "studyDownloadAllGames": "Pellgargañ an holl grogadoù", + "studyChapterPgn": "PGN ar pennad", + "studyDownloadGame": "Pellgargañ ur c'hrogad", + "studyStudyUrl": "Studiañ URL", + "studyCurrentChapterUrl": "URL ar pennad evit poent", + "studyYouCanPasteThisInTheForumToEmbed": "Gallout a rit pegañ se er forom evit ensoc'hañ", + "studyStartAtInitialPosition": "Kregiñ el lec'hiadur kentañ", + "studyStartAtX": "Kregiñ e {param}", + "studyEmbedInYourWebsite": "Enframmañ en ho lec'hienn pe blog", + "studyReadMoreAboutEmbedding": "Goût hiroc'h diwar-benn an ensoc'hañ", + "studyOnlyPublicStudiesCanBeEmbedded": "Ar studiadennoù foran a c'hall bezañ ensoc'het!", + "studyOpen": "Digeriñ", + "studyXBroughtToYouByY": "{param1}, zo kaset deoc'h gant {param2}", + "studyStudyNotFound": "N'eo ket bet kavet ar studiadenn", + "studyEditChapter": "Aozañ ar pennad", + "studyNewChapter": "Pennad nevez", + "studyOrientation": "Tuadur", + "studyAnalysisMode": "Doare dielfennañ", + "studyPinnedChapterComment": "Ali war ar pennad spilhet", + "studySaveChapter": "Saveteiñ pennad", + "studyClearAnnotations": "Diverkañ an notennoù", + "studyDeleteChapter": "Dilemel pennad", + "studyDeleteThisChapter": "Dilemel ar pennad-mañ? Hep distro e vo!", + "studyClearAllCommentsInThisChapter": "Diverkañ an holl evezhiadennoù ha notennoù er pennad?", + "studyRightUnderTheBoard": "Dindan an dablez", + "studyNoPinnedComment": "Hini ebet", + "studyNormalAnalysis": "Dielfennañ normal", + "studyHideNextMoves": "Kuzhat ar fiñvadennoù da heul", + "studyInteractiveLesson": "Kentel etreoberiat", + "studyChapterX": "Pennad {param}", + "studyEmpty": "Goullo", + "studyStartFromInitialPosition": "Kregiñ el lec'hiadur kentañ", + "studyEditor": "Aozer", + "studyStartFromCustomPosition": "Kregiñ adalek ul lakadur aozet", + "studyLoadAGameByUrl": "Kargañ ur c'hrogad dre URL", + "studyLoadAPositionFromFen": "Kargañ ul lakadur dre FEN", + "studyLoadAGameFromPgn": "Kargañ ul lakadur dre PGN", + "studyAutomatic": "Emgefre", + "studyUrlOfTheGame": "URL ar c'hrogad", + "studyLoadAGameFromXOrY": "Kargañ ur c'hrogad eus {param1} pe {param2}", + "studyCreateChapter": "Krouiñ pennad", + "studyCreateStudy": "Krouiñ ur studiadenn", + "studyEditStudy": "Aozañ studiadenn", + "studyVisibility": "Gwelusted", + "studyPublic": "Foran", + "studyUnlisted": "N'eo ket bet listennet", + "studyInviteOnly": "Kouvidi hepken", + "studyAllowCloning": "Aotreañ ar c'hlonañ", + "studyNobody": "Den ebet", + "studyOnlyMe": "Me hepken", + "studyContributors": "Perzhidi", + "studyMembers": "Izili", + "studyEveryone": "An holl dud", + "studyEnableSync": "Gweredekaat sync", + "studyYesKeepEveryoneOnTheSamePosition": "Ya: laoskit an traoù evel m'emaint", + "studyNoLetPeopleBrowseFreely": "Nann: laoskit an dud merdeiñ trankilik", + "studyPinnedStudyComment": "Ali war ar studiadenn spilhet", + "studyStart": "Kregiñ", + "studySave": "Saveteiñ", + "studyClearChat": "Diverkañ ar flapañ", + "studyDeleteTheStudyChatHistory": "Dilemel an istor-flapañ? Hep distro e vo!", + "studyDeleteStudy": "Dilemel ar studiadenn", + "studyWhereDoYouWantToStudyThat": "Pelec'h ho peus c'hoant da studiañ se?", + "studyGoodMove": "Fiñvadenn vat", + "studyMistake": "Fazi", + "studyBlunder": "Bourd", + "studyNbChapters": "{count, plural, =1{{count} pennad} =2{{count} pennad} few{{count} pennad} many{{count} pennad} other{{count} pennad}}", + "studyNbGames": "{count, plural, =1{{count} C'hoariadenn} =2{{count} C'hoariadenn} few{{count} C'hoariadenn} many{{count} C'hoariadenn} other{{count} C'hoariadenn}}", + "studyNbMembers": "{count, plural, =1{{count} Ezel} =2{{count} Ezel} few{{count} Ezel} many{{count} Ezel} other{{count} Ezel}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pegit testenn ho PGN amañ, betek {count} krogad} =2{Pegit testenn ho PGN amañ, betek {count} grogad} few{Pegit testenn ho PGN amañ, betek {count} krogadoù} many{Pegit testenn ho PGN amañ, betek {count} krogadoù} other{Pegit testenn ho PGN amañ, betek {count} krogadoù}}", + "timeagoJustNow": "bremañ", + "timeagoRightNow": "bremañ", + "timeagoInNbSeconds": "{count, plural, =1{a-benn {count} eilenn} =2{a-benn {count} eilenn} few{a-benn {count} eilenn} many{a-benn {count} eilenn} other{a-benn {count} eilenn}}", + "timeagoInNbMinutes": "{count, plural, =1{a-benn {count} vunutenn} =2{a-benn {count} vunutenn} few{a-benn {count} munutenn} many{a-benn {count} munutenn} other{a-benn {count} munutenn}}", + "timeagoInNbHours": "{count, plural, =1{a-benn {count} eur} =2{a-benn {count} eur} few{a-benn {count} eur} many{a-benn {count} eur} other{a-benn {count} eur}}", + "timeagoInNbDays": "{count, plural, =1{a-benn {count} deiz} =2{a-benn {count} zeiz} few{a-benn {count} deiz} many{a-benn {count} deiz} other{a-benn {count} deiz}}", + "timeagoInNbWeeks": "{count, plural, =1{a-benn {count} sizhun} =2{a-benn {count} sizhun} few{a-benn {count} sizhun} many{a-benn {count} sizhun} other{a-benn {count} sizhun}}", + "timeagoInNbMonths": "{count, plural, =1{a-benn {count} miz} =2{a-benn {count} viz} few{a-benn {count} miz} many{a-benn {count} miz} other{a-benn {count} miz}}", + "timeagoInNbYears": "{count, plural, =1{a-benn {count} bloaz} =2{a-benn {count} vloaz} few{a-benn {count} bloaz} many{a-benn {count} bloaz} other{a-benn {count} bloaz}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} vunutenn zo} =2{{count} vunutenn zo} few{{count} munutenn zo} many{{count} munutenn zo} other{{count} munutenn zo}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} eur zo} =2{{count} eur zo} few{{count} eur zo} many{{count} eur zo} other{{count} eur zo}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} deiz zo} =2{{count} zeiz zo} few{{count} deiz zo} many{{count} deiz zo} other{{count} deiz zo}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} sizhun zo} =2{{count} sizhun zo} few{{count} sizhun zo} many{{count} sizhun zo} other{{count} sizhun zo}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} miz zo} =2{{count} viz zo} few{{count} miz zo} many{{count} miz zo} other{{count} miz zo}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} bloaz zo} =2{{count} vloaz zo} few{{count} bloaz zo} many{{count} bloaz zo} other{{count} bloaz zo}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bs.arb b/lib/l10n/lila_bs.arb index d3dcea6446..e61b14484f 100644 --- a/lib/l10n/lila_bs.arb +++ b/lib/l10n/lila_bs.arb @@ -21,7 +21,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Takmičio se u {count} turnirima po švicarskom sistemu} few{Takmičili se u {count} turnirima po švicarskom sistemu} other{Takmičili se u {count} turnirima po švicarskom sistemu}}", "activityJoinedNbTeams": "{count, plural, =1{Pridružio/la se {count} timu} few{Pridružio/la se {count} tima} other{Pridružio/la se {count} timova}}", "broadcastBroadcasts": "Emitovanja", + "broadcastMyBroadcasts": "Moja emitiranja", "broadcastLiveBroadcasts": "Prenos turnira uživo", + "broadcastNewBroadcast": "Novo emitovanje uživo", + "broadcastAddRound": "Dodajte kolo", + "broadcastOngoing": "U toku", + "broadcastUpcoming": "Nadolazeći", + "broadcastCompleted": "Završeno", + "broadcastRoundName": "Ime kola", + "broadcastRoundNumber": "Zaokružen broj", + "broadcastTournamentName": "Naziv turnira", + "broadcastTournamentDescription": "Kratak opis turnira", + "broadcastFullDescription": "Potpuni opis događaja", + "broadcastFullDescriptionHelp": "Neobavezni dugi opis događaja koji se emituje. {param1} je dostupan. Dužina mora biti manja od {param2} slova.", + "broadcastSourceUrlHelp": "Link koji će Lichess koristiti kako bi redovno ažurirao PGN. Mora biti javno dostupan na internetu.", + "broadcastStartDateHelp": "Neobavezno, ukoliko znate kada počinje događaj", + "broadcastCurrentGameUrl": "Link za trenutnu partiju", + "broadcastDownloadAllRounds": "Skinite sve runde", + "broadcastResetRound": "Ponovo postavite ovo kolo", + "broadcastDeleteRound": "Izbrišite ovo kolo", + "broadcastDefinitivelyDeleteRound": "Definitivno izbrišite ovo kolo i partije u njemu.", + "broadcastDeleteAllGamesOfThisRound": "Izbrišite sve partije iz ovog kola. Izvor mora biti aktivan da biste ih mogli ponovo kreirati.", + "broadcastEditRoundStudy": "Podesite studiju kola", + "broadcastDeleteTournament": "Izbrišite ovaj turnir", + "broadcastDefinitivelyDeleteTournament": "Definitivno izbrišite cijeli turnir, sva kola i sve partije.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} emitovanje} few{{count} emitovanja} other{{count} emitovanja}}", "challengeChallengesX": "Izazovi: {param1}", "challengeChallengeToPlay": "Izazov na partiju", "challengeChallengeDeclined": "Izazov odbijen", @@ -339,8 +363,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ili brani polje kroz protivničku figuru.", "puzzleThemeZugzwang": "Iznudica", "puzzleThemeZugzwangDescription": "Protivnik ima ograničen broj poteza, a svaki od njih pogoršava mu poziciju.", - "puzzleThemeHealthyMix": "Zdrava mješavina", - "puzzleThemeHealthyMixDescription": "Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.", + "puzzleThemeMix": "Zdrava mješavina", + "puzzleThemeMixDescription": "Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.", "puzzleThemePlayerGames": "Igračke igre", "puzzleThemePlayerGamesDescription": "Potražite zagonetke stvorene iz vaših igara ili iz igara drugog igrača.", "puzzleThemePuzzleDownloadInformation": "Ove zagonetke su u javnoj domeni i mogu se preuzeti sa {param}.", @@ -457,7 +481,6 @@ "replayMode": "Repriza partije", "realtimeReplay": "U stvarnom vremenu", "byCPL": "Po SDP", - "openStudy": "Otvori studiju", "enable": "Omogući", "bestMoveArrow": "Strelica za najbolji potez", "showVariationArrows": "Prikaži strelice za varijante", @@ -467,7 +490,6 @@ "memory": "Memorija", "infiniteAnalysis": "Neprekidna analiza", "removesTheDepthLimit": "Uklanja granicu do koje računar može analizirati, i održava toplinu računara", - "engineManager": "Upravitelj šahovskog programa", "blunder": "Grubi previd", "mistake": "Greška", "inaccuracy": "Nepreciznost", @@ -661,7 +683,6 @@ "block": "Blokirajte", "blocked": "Blokiran", "unblock": "Odblokiraj", - "followsYou": "Prati vas", "xStartedFollowingY": "{param1} je počeo pratiti {param2}", "more": "Više", "memberSince": "Član od", @@ -758,7 +779,6 @@ "cheat": "Varanje", "troll": "Provokacija", "other": "Ostalo", - "reportDescriptionHelp": "Zalijepite link na partiju ili partije u pitanju i objasnite što nije bilo u redu sa ponašanjem korisnika. Nemojte samo reći \"varao je\", nego objasnite kako ste došli do tog zaključka. Vaša prijava će biti brže obrađena ukoliko je napišete na engleskom jeziku.", "error_provideOneCheatedGameLink": "Molimo navedite barem jedan link na partiju u kojoj je igrač varao.", "by": "od {param}", "importedByX": "Uvezao {param}", @@ -1227,6 +1247,174 @@ "stormXRuns": "{count, plural, =1{Jedna runda} few{{count} rundi} other{{count} rundi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odigrao jednu rundu od {param2}} few{Odigrao {count} rundi od {param2}} other{Odigrao {count} rundi {param2}}}", "streamerLichessStreamers": "Lichess emiteri", + "studyPrivate": "Privatna", + "studyMyStudies": "Moje studije", + "studyStudiesIContributeTo": "Studije kojima doprinosim", + "studyMyPublicStudies": "Moje javne studije", + "studyMyPrivateStudies": "Moje privatne studije", + "studyMyFavoriteStudies": "Moje omiljene studije", + "studyWhatAreStudies": "Šta su studije?", + "studyAllStudies": "Sve studije", + "studyStudiesCreatedByX": "Studije koje je kreirao/la {param}", + "studyNoneYet": "Još nijedna.", + "studyHot": "U trendu", + "studyDateAddedNewest": "Datum dodavanja (najnovije)", + "studyDateAddedOldest": "Datum dodavanja (najstarije)", + "studyRecentlyUpdated": "Nedavno ažurirane", + "studyMostPopular": "Najpopularnije", + "studyAlphabetical": "Abecedno", + "studyAddNewChapter": "Dodajte novo poglavlje", + "studyAddMembers": "Dodajte članove", + "studyInviteToTheStudy": "Pozovite na studiju", + "studyPleaseOnlyInvitePeopleYouKnow": "Molimo Vas da pozovete samo ljude koje znate i koji su zainteresovani da aktivno učustvuju u ovoj studiji.", + "studySearchByUsername": "Pretraga prema korisničkom imenu", + "studySpectator": "Posmatrač", + "studyContributor": "Saradnik", + "studyKick": "Izbaci", + "studyLeaveTheStudy": "Napustite studiju", + "studyYouAreNowAContributor": "Sada ste saradnik", + "studyYouAreNowASpectator": "Sada ste posmatrač", + "studyPgnTags": "PGN oznake", + "studyLike": "Sviđa mi se", + "studyUnlike": "Ne sviđa mi se", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentirajte ovu poziciju", + "studyCommentThisMove": "Komentirajte ovaj potez", + "studyAnnotateWithGlyphs": "Obilježite poteze simbolima", + "studyTheChapterIsTooShortToBeAnalysed": "Poglavlje je prekratko za analizu.", + "studyOnlyContributorsCanRequestAnalysis": "Samo saradnici u studiji mogu zahtijevati računarsku analizu.", + "studyGetAFullComputerAnalysis": "Dobijte potpunu serversku analizu glavne varijacije.", + "studyMakeSureTheChapterIsComplete": "Budite sigurni da je poglavlje gotovo. Računarsku analizu možete zahtjevati samo jednom.", + "studyAllSyncMembersRemainOnTheSamePosition": "Svi sinhronizovani članovi ostaju na istoj poziciji", + "studyShareChanges": "Podijelite promjene sa posmatračima i sačuvajte ih na server", + "studyPlaying": "U toku", + "studyShowEvalBar": "Evaluacijske trake", + "studyFirst": "Prva strana", + "studyPrevious": "Prethodna strana", + "studyNext": "Sljedeća strana", + "studyLast": "Posljednja strana", "studyShareAndExport": "Podijelite i izvezite", - "studyStart": "Pokreni" + "studyCloneStudy": "Klonirajte", + "studyStudyPgn": "Studirajte PGN", + "studyDownloadAllGames": "Skinite sve partije", + "studyChapterPgn": "PGN poglavlja", + "studyCopyChapterPgn": "Kopirajte PGN", + "studyDownloadGame": "Skini partiju", + "studyStudyUrl": "Link studije", + "studyCurrentChapterUrl": "Link trenutnog poglavlja", + "studyYouCanPasteThisInTheForumToEmbed": "Možete ovo zalijepiti na forumu ili Vašem blogu na Lichessu kako biste ugradili poglavlje", + "studyStartAtInitialPosition": "Krenite sa inicijalnom pozicijom", + "studyStartAtX": "Krenite sa {param}", + "studyEmbedInYourWebsite": "Ugradite na Vaš sajt", + "studyReadMoreAboutEmbedding": "Pročitajte više o ugrađivanju", + "studyOnlyPublicStudiesCanBeEmbedded": "Samo javne studije mogu biti ugrađene!", + "studyOpen": "Otvorite", + "studyXBroughtToYouByY": "{param1} vam je donio {param2}", + "studyStudyNotFound": "Studija nije pronađena", + "studyEditChapter": "Uredite poglavlje", + "studyNewChapter": "Novo poglavlje", + "studyImportFromChapterX": "Uvezite iz {param}", + "studyOrientation": "Orijentacija", + "studyAnalysisMode": "Tip analize", + "studyPinnedChapterComment": "Stalni komentar poglavlja", + "studySaveChapter": "Sačuvajte poglavlje", + "studyClearAnnotations": "Izbrišite bilješke", + "studyClearVariations": "Ukloni varijante", + "studyDeleteChapter": "Izbrišite poglavlje", + "studyDeleteThisChapter": "Da li želite izbrisati ovo poglavlje? Nakon ove akcije, poglavlje se ne može vratiti!", + "studyClearAllCommentsInThisChapter": "Da li želite izbrisati sve komentare, simbole i nacrtane oblike u ovom poglavlju?", + "studyRightUnderTheBoard": "Odmah ispod ploče", + "studyNoPinnedComment": "Nijedan", + "studyNormalAnalysis": "Normalna analiza", + "studyHideNextMoves": "Sakrijte sljedeće poteze", + "studyInteractiveLesson": "Interaktivna lekcija", + "studyChapterX": "Poglavlje {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Krenite sa inicijalnom pozicijom", + "studyEditor": "Uređivač", + "studyStartFromCustomPosition": "Krenite sa željenom pozicijom", + "studyLoadAGameByUrl": "Učitajte partiju pomoću linka", + "studyLoadAPositionFromFen": "Učitajte partiju pomoću FEN koda", + "studyLoadAGameFromPgn": "Učitajte partiju pomoću PGN formata", + "studyAutomatic": "Automatska", + "studyUrlOfTheGame": "Link partije", + "studyLoadAGameFromXOrY": "Učitajte partiju sa {param1} ili {param2}", + "studyCreateChapter": "Kreirajte poglavlje", + "studyCreateStudy": "Kreirajte studiju", + "studyEditStudy": "Uredite studiju", + "studyVisibility": "Vidljivost", + "studyPublic": "Javna", + "studyUnlisted": "Neizlistane", + "studyInviteOnly": "Samo po pozivu", + "studyAllowCloning": "Dozvolite kloniranje", + "studyNobody": "Niko", + "studyOnlyMe": "Samo ja", + "studyContributors": "Saradnici", + "studyMembers": "Članovi", + "studyEveryone": "Svi", + "studyEnableSync": "Omogućite sinhronizaciju", + "studyYesKeepEveryoneOnTheSamePosition": "Da: zadržite sve na istoj poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: Dozvolite ljudima da slobodno pregledaju", + "studyPinnedStudyComment": "Stalni komentar studije", + "studyStart": "Pokreni", + "studySave": "Sačuvaj", + "studyClearChat": "Izbrišite dopisivanje", + "studyDeleteTheStudyChatHistory": "Da li želite izbrisati svo dopisivanje vezano za ovu studiju? Nakon ove akcije, obrisani tekst se ne može vratiti!", + "studyDeleteStudy": "Izbrišite studiju", + "studyConfirmDeleteStudy": "Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: {param}", + "studyWhereDoYouWantToStudyThat": "Gdje želite da ovu poziciju prostudirate?", + "studyGoodMove": "Dobar potez", + "studyMistake": "Greška", + "studyBrilliantMove": "Briljantan potez", + "studyBlunder": "Grubi previd", + "studyInterestingMove": "Zanimljiv potez", + "studyDubiousMove": "Sumnjiv potez", + "studyOnlyMove": "Jedini potez", + "studyZugzwang": "Iznudica", + "studyEqualPosition": "Jednaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Bijeli je u blagoj prednosti", + "studyBlackIsSlightlyBetter": "Crni je u blagoj prednosti", + "studyWhiteIsBetter": "Bijeli je bolji", + "studyBlackIsBetter": "Crni je bolji", + "studyWhiteIsWinning": "Bijeli dobija", + "studyBlackIsWinning": "Crni dobija", + "studyNovelty": "Nov potez", + "studyDevelopment": "Razvoj", + "studyInitiative": "Inicijativa", + "studyAttack": "Napad", + "studyCounterplay": "Protivnapad", + "studyTimeTrouble": "Cajtnot", + "studyWithCompensation": "S kompenzacijom", + "studyWithTheIdea": "S idejom", + "studyNextChapter": "Sljedeće poglavlje", + "studyPrevChapter": "Prethodno poglavlje", + "studyStudyActions": "Opcije za studiju", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Popularne teme", + "studyManageTopics": "Upravljajte temama", + "studyBack": "Nazad", + "studyPlayAgain": "Igrajte ponovo", + "studyWhatWouldYouPlay": "Šta biste odigrali u ovoj poziciji?", + "studyYouCompletedThisLesson": "Čestitamo! Kompletirali ste ovu lekciju.", + "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", + "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partija}}", + "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Člana} other{{count} Članova}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} partije} few{Ovdje zalijepite svoj PGN tekst, do {count} partije} other{Ovdje zalijepite svoj PGN tekst, do {count} partija}}", + "timeagoJustNow": "upravo sada", + "timeagoRightNow": "upravo sada", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundu} few{za {count} sekunde} other{za {count} sekundi}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minute} other{za {count} minuta}}", + "timeagoInNbHours": "{count, plural, =1{za {count} sat} few{za {count} sata} other{za {count} sati}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dan} few{za {count} dana} other{za {count} dana}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} sedmicu} few{za {count} sedmice} other{za {count} sedmica}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} mjesec} few{za {count} mjeseca} other{za {count} mjeseci}}", + "timeagoInNbYears": "{count, plural, =1{za {count} godinu} few{za {count} godine} other{za {count} godina}}", + "timeagoNbMinutesAgo": "{count, plural, =1{prije {count} minutu} few{prije {count} minute} other{prije {count} minuta}}", + "timeagoNbHoursAgo": "{count, plural, =1{prije {count} sat} few{prije {count} sata} other{prije {count} sati}}", + "timeagoNbDaysAgo": "{count, plural, =1{prije {count} dan} few{prije {count} dana} other{prije {count} dana}}", + "timeagoNbWeeksAgo": "{count, plural, =1{prije {count} sedmicu} few{prije {count} sedmice} other{prije {count} sedmica}}", + "timeagoNbMonthsAgo": "{count, plural, =1{prije {count} mjesec} few{prije {count} mjeseca} other{prije {count} mjeseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{prije {count} godinu} few{prije {count} godine} other{prije {count} godina}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ca.arb b/lib/l10n/lila_ca.arb index 214dc06cbd..6687a5cefd 100644 --- a/lib/l10n/lila_ca.arb +++ b/lib/l10n/lila_ca.arb @@ -1,46 +1,46 @@ { + "mobileAllGames": "Totes les partides", + "mobileAreYouSure": "Estàs segur?", + "mobileBlindfoldMode": "A la cega", + "mobileCancelTakebackOffer": "Anul·la la petició per desfer la jugada", + "mobileClearButton": "Neteja", + "mobileCorrespondenceClearSavedMove": "Elimina la jugada guardada", + "mobileCustomGameJoinAGame": "Unir-se a una partida", + "mobileFeedbackButton": "Suggeriments", + "mobileGreeting": "Hola, {param}", + "mobileGreetingWithoutName": "Hola", + "mobileHideVariation": "Amaga les variacions", "mobileHomeTab": "Inici", - "mobilePuzzlesTab": "Problemes", - "mobileToolsTab": "Eines", - "mobileWatchTab": "Visualitza", - "mobileSettingsTab": "Configuració", + "mobileLiveStreamers": "Retransmissors en directe", "mobileMustBeLoggedIn": "Has d'estar connectat per veure aquesta pàgina.", - "mobileSystemColors": "Colors del sistema", - "mobileFeedbackButton": "Suggeriments", - "mobileOkButton": "D'acord", - "mobileSettingsHapticFeedback": "Resposta hàptica", - "mobileSettingsImmersiveMode": "Mode immersiu", + "mobileNoSearchResults": "Sense resultats", "mobileNotFollowingAnyUser": "No estàs seguint a cap usuari.", - "mobileAllGames": "Totes les partides", - "mobileRecentSearches": "Cerques recents", - "mobileClearButton": "Neteja", + "mobileOkButton": "D'acord", "mobilePlayersMatchingSearchTerm": "Jugadors amb \"{param}\"", - "mobileNoSearchResults": "Sense resultats", - "mobileAreYouSure": "Estàs segur?", - "mobilePuzzleStreakAbortWarning": "Perdreu la vostra ratxa i la vostra puntuació es guardarà.", + "mobilePuzzleStormConfirmEndRun": "Voleu acabar aquesta ronda?", + "mobilePuzzleStormFilterNothingToShow": "Res a mostrar, si us plau canvieu els filtres", "mobilePuzzleStormNothingToShow": "Res a mostrar. Fes algunes rondes al Puzzle Storm.", - "mobileSharePuzzle": "Comparteix aquest problema", - "mobileShareGameURL": "Comparteix l'enllaç a la partida", + "mobilePuzzleStormSubtitle": "Resoleu el màxim nombre de problemes en 3 minuts.", + "mobilePuzzleStreakAbortWarning": "Perdreu la vostra ratxa i la vostra puntuació es guardarà.", + "mobilePuzzleThemesSubtitle": "Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.", + "mobilePuzzlesTab": "Problemes", + "mobileRecentSearches": "Cerques recents", + "mobileSettingsHapticFeedback": "Resposta hàptica", + "mobileSettingsImmersiveMode": "Mode immersiu", + "mobileSettingsTab": "Configuració", "mobileShareGamePGN": "Comparteix PGN", + "mobileShareGameURL": "Comparteix l'enllaç a la partida", "mobileSharePositionAsFEN": "Comparteix la posició com a FEN", - "mobileShowVariations": "Mostra les variacions", - "mobileHideVariation": "Amaga les variacions", + "mobileSharePuzzle": "Comparteix aquest problema", "mobileShowComments": "Mostra comentaris", - "mobilePuzzleStormConfirmEndRun": "Voleu acabar aquesta ronda?", - "mobilePuzzleStormFilterNothingToShow": "Res a mostrar, si us plau canvieu els filtres", - "mobileCancelTakebackOffer": "Anul·la la petició per desfer la jugada", - "mobileCancelDrawOffer": "Anul·la la petició de taules", - "mobileWaitingForOpponentToJoin": "Esperant que s'uneixi l'adversari...", - "mobileBlindfoldMode": "A la cega", - "mobileLiveStreamers": "Retransmissors en directe", - "mobileCustomGameJoinAGame": "Unir-se a una partida", - "mobileCorrespondenceClearSavedMove": "Elimina la jugada guardada", - "mobileSomethingWentWrong": "Alguna cosa ha anat malament.", "mobileShowResult": "Mostra el resultat", - "mobilePuzzleThemesSubtitle": "Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.", - "mobilePuzzleStormSubtitle": "Resoleu el màxim nombre de problemes en 3 minuts.", - "mobileGreeting": "Hola, {param}", - "mobileGreetingWithoutName": "Hola", + "mobileShowVariations": "Mostra les variacions", + "mobileSomethingWentWrong": "Alguna cosa ha anat malament.", + "mobileSystemColors": "Colors del sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Eines", + "mobileWaitingForOpponentToJoin": "Esperant que s'uneixi l'adversari...", + "mobileWatchTab": "Visualitza", "activityActivity": "Activitat", "activityHostedALiveStream": "Has fet una retransmissió en directe", "activityRankedInSwissTournament": "Classificat #{param1} en {param2}", @@ -63,7 +63,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha jugat en {count} tornejos suïssos} other{Ha jugat en {count} tornejos suïssos}}", "activityJoinedNbTeams": "{count, plural, =1{Membre de {count} equip} other{T'has unit a {count} equips}}", "broadcastBroadcasts": "Retransmissions", + "broadcastMyBroadcasts": "Les meves retransmissions", "broadcastLiveBroadcasts": "Retransmissions de tornejos en directe", + "broadcastBroadcastCalendar": "Calendari de retransmissions", + "broadcastNewBroadcast": "Nova retransmissió en directe", + "broadcastSubscribedBroadcasts": "Emissions que segueixo", + "broadcastAboutBroadcasts": "Sobre les retransmissions", + "broadcastHowToUseLichessBroadcasts": "Com utilitzar les retransmissions de Lichess.", + "broadcastTheNewRoundHelp": "La nova ronda tindrà els mateixos membres i contribuïdors que l'anterior.", + "broadcastAddRound": "Afegir una ronda", + "broadcastOngoing": "En curs", + "broadcastUpcoming": "Properes", + "broadcastCompleted": "Acabada", + "broadcastCompletedHelp": "Lichess detecta el final de la ronda en funció de les partides de l'origen. Utilitzeu aquesta opció si no hi ha origen.", + "broadcastRoundName": "Nom de ronda", + "broadcastRoundNumber": "Ronda número", + "broadcastTournamentName": "Nom del torneig", + "broadcastTournamentDescription": "Breu descripció del torneig", + "broadcastFullDescription": "Descripció total de l'esdeveniment", + "broadcastFullDescriptionHelp": "Opció de llarga descripció de l'esdeveniment. {param1} és disponible. Ha de tenir menys de {param2} lletres.", + "broadcastSourceSingleUrl": "URL origen del PGN", + "broadcastSourceUrlHelp": "URL que Lichess comprovarà per a obtenir actualitzacions PGN. Ha de ser públicament accessible des d'Internet.", + "broadcastSourceGameIds": "Fins a 64 identificadors de partides de Lichess, separades per espais.", + "broadcastStartDateTimeZone": "Dia d'inici a la zona horari del torneig: {param}", + "broadcastStartDateHelp": "Opcional, si saps quan comença l'esdeveniment", + "broadcastCurrentGameUrl": "URL actual de joc", + "broadcastDownloadAllRounds": "Baixa totes les rondes", + "broadcastResetRound": "Restablir aquesta ronda", + "broadcastDeleteRound": "Eliminar aquesta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivament la ronda i les seves partides.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar totes les partides d'aquesta ronda. L'origen ha d'estar actiu per a recrear-les.", + "broadcastEditRoundStudy": "Edita l'estudi de la ronda", + "broadcastDeleteTournament": "Elimina aquest torneig", + "broadcastDefinitivelyDeleteTournament": "Elimina el torneig de forma definitiva, amb totes les seves rondes i les seves partides.", + "broadcastShowScores": "Mostra les puntuacions dels jugadors en funció dels resultats de les partides", + "broadcastReplacePlayerTags": "Opcional: Reemplaça noms dels jugadors, puntuacions i títols", + "broadcastFideFederations": "Federacions FIDE", + "broadcastTop10Rating": "Top 10 Ràting", + "broadcastFidePlayers": "Jugadors FIDE", + "broadcastFidePlayerNotFound": "No s'ha trobat el jugador FIDE", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federació", + "broadcastAgeThisYear": "Edat aquest any", + "broadcastUnrated": "Sense avaluació", + "broadcastRecentTournaments": "Tornejos recents", + "broadcastOpenLichess": "Obre a Lichess", + "broadcastTeams": "Equips", + "broadcastBoards": "Taulers", + "broadcastOverview": "Visió general", + "broadcastSubscribeTitle": "Subscriviu-vos per ser notificats quan comença cada ronda. Podeu activar/desactivara la campana o modificar les notificacions push a les preferències del vostre compte.", + "broadcastUploadImage": "Puja una imatge del torneig", + "broadcastNoBoardsYet": "Encara no hi ha taulers. Apareixeran en el moment que es carreguin les partides.", + "broadcastBoardsCanBeLoaded": "Els taulers es poden carregar per codi o a través de {param}", + "broadcastStartsAfter": "Començar a les {param}", + "broadcastStartVerySoon": "La retransmissió començarà aviat.", + "broadcastNotYetStarted": "La retransmissió encara no ha començat.", + "broadcastOfficialWebsite": "Lloc web oficial", + "broadcastStandings": "Classificació", + "broadcastOfficialStandings": "Classificació oficial", + "broadcastIframeHelp": "Més opcions a la {param}", + "broadcastWebmastersPage": "pàgina d'administració", + "broadcastPgnSourceHelp": "Un origen públic en PGN públic en temps real d'aquesta ronda. També oferim un {param} per una sincronització més ràpida i eficient.", + "broadcastEmbedThisBroadcast": "Incrusta aquesta retransmissió al vostre lloc web", + "broadcastEmbedThisRound": "Incrusta {param} al vostre lloc web", + "broadcastRatingDiff": "Diferència puntuació", + "broadcastGamesThisTournament": "Partides en aquest torneig", + "broadcastScore": "Puntuació", + "broadcastAllTeams": "Tots els equips", + "broadcastTournamentFormat": "Format del torneig", + "broadcastTournamentLocation": "Ubicació del torneig", + "broadcastTopPlayers": "Millors jugadors", + "broadcastTimezone": "Zona horària", + "broadcastFideRatingCategory": "Categoria puntuació FIDE", + "broadcastOptionalDetails": "Detalls opcionals", + "broadcastPastBroadcasts": "Retransmissions finalitzades", + "broadcastAllBroadcastsByMonth": "Veure totes les retransmissions per més", + "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmissió} other{{count} retransmissions}}", "challengeChallengesX": "Desafiaments: {param1}", "challengeChallengeToPlay": "Desafia a una partida", "challengeChallengeDeclined": "Desafiament rebutjat", @@ -187,6 +262,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositiu", "preferencesBellNotificationSound": "So de notificació", + "preferencesBlindfold": "A la cega", "puzzlePuzzles": "Problemes", "puzzlePuzzleThemes": "Temàtiques de problemes", "puzzleRecommended": "Recomanat", @@ -382,8 +458,8 @@ "puzzleThemeXRayAttackDescription": "Una peça ataca o defensa una casella, a través d'una peça rival.", "puzzleThemeZugzwang": "Atzucac", "puzzleThemeZugzwangDescription": "El rival té els moviments limitats i cada jugada empitjora la seva posició.", - "puzzleThemeHealthyMix": "Una mica de cada", - "puzzleThemeHealthyMixDescription": "Una mica de tot. No sabràs el que t'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.", + "puzzleThemeMix": "Una mica de cada", + "puzzleThemeMixDescription": "Una mica de tot. No sabràs el que t'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.", "puzzleThemePlayerGames": "Partides de jugadors", "puzzleThemePlayerGamesDescription": "Problemes generats a partir de les teves partides o de les partides d'altres jugadors.", "puzzleThemePuzzleDownloadInformation": "Aquests problemes són de domini públic i es poden descarregar des de {param}.", @@ -504,7 +580,6 @@ "replayMode": "Mode de reproducció", "realtimeReplay": "En temps real", "byCPL": "Per CPL", - "openStudy": "Obrir estudi", "enable": "Habilitar", "bestMoveArrow": "Fletxa de la millor jugada", "showVariationArrows": "Mostrar fletxes de les variants", @@ -514,7 +589,6 @@ "memory": "Memòria", "infiniteAnalysis": "Anàlisi il·limitada", "removesTheDepthLimit": "Treu el límit de profunditat i escalfa el teu ordinador", - "engineManager": "Gestió del mòdul", "blunder": "Errada greu", "mistake": "Errada", "inaccuracy": "Imprecisió", @@ -712,7 +786,6 @@ "block": "Bloqueja", "blocked": "Bloquejat", "unblock": "Desbloqueja", - "followsYou": "T'està seguint", "xStartedFollowingY": "{param1} ha començat a seguir {param2}", "more": "Més", "memberSince": "Membre des del", @@ -818,7 +891,9 @@ "cheat": "Trampós", "troll": "Troll", "other": "Altres", - "reportDescriptionHelp": "Enganxa l'enllaç de la partida (o partides) i explica el comportament negatiu d'aquest usuari. No et limitis a dir que \"fa trampes\", i explica com has arribat a aquesta conclusió. El teu informe serà processat més ràpidament si l'escrius en anglès.", + "reportCheatBoostHelp": "Enganxa l'enllaç de la partida (o partides) i explica quin és el problema amb el comportament d'aquest usuari. No et limitis a dir que \"fa trampes\", explica'ns com has arribat a aquesta conclusió.", + "reportUsernameHelp": "Explica quin és el comportament ofensiu d'aquest usuari. No et limitis a dir simplement \"és ofensiu/inapropiat\", explica'ns com has arribat a aquesta conclusió, especialment si l'insult està ofuscat, en un idioma diferent de l'anglès, és un barbarisme o és una referència històrica o cultural.", + "reportProcessedFasterInEnglish": "El teu informe serà tractat més ràpidament si està escrit en anglès.", "error_provideOneCheatedGameLink": "Si us plau, proporcioneu com a mínim un enllaç a un joc on s'han fet trampes.", "by": "per {param}", "importedByX": "Importat per {param}", @@ -1216,6 +1291,7 @@ "showMeEverything": "Mostrar tot", "lichessPatronInfo": "Lichess és una entitat sense ànim de lucre i un programari totalment lliure i de codi obert.\nLes despeses de funcionament, desenvolupament i continguts es financen exclusivament amb donacions d'usuaris.", "nothingToSeeHere": "Res a veure per aquí de moment.", + "stats": "Estadístiques", "opponentLeftCounter": "{count, plural, =1{El teu contrincant ha abandonat la partida. Pots reclamar la victòria en {count} segon.} other{El teu contrincant ha abandonat la partida. Pots reclamar la victòria en {count} segons.}}", "mateInXHalfMoves": "{count, plural, =1{Mat en {count} mig-moviment} other{Mat en {count} jugades}}", "nbBlunders": "{count, plural, =1{{count} errada greu} other{{count} errades greus}}", @@ -1312,6 +1388,178 @@ "stormXRuns": "{count, plural, =1{Un intent} other{{count} intents}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ha jugat una ronda de {param2}} other{Ha jugat {count} rondes de {param2}}}", "streamerLichessStreamers": "Retransmissors de Lichess", + "studyPrivate": "Privat", + "studyMyStudies": "Els meus estudis", + "studyStudiesIContributeTo": "Estudis on jo hi contribueixo", + "studyMyPublicStudies": "Els meus estudis públics", + "studyMyPrivateStudies": "Els meus estudis privats", + "studyMyFavoriteStudies": "Els meus estudis favorits", + "studyWhatAreStudies": "Què són els estudis?", + "studyAllStudies": "Tots els estudis", + "studyStudiesCreatedByX": "Estudis creats per {param}", + "studyNoneYet": "Res encara.", + "studyHot": "Candent", + "studyDateAddedNewest": "Data d’inclusió (més nous)", + "studyDateAddedOldest": "Data d’inclusió (més antics)", + "studyRecentlyUpdated": "Actualitzat darrerament", + "studyMostPopular": "Més popular", + "studyAlphabetical": "Alfabètic", + "studyAddNewChapter": "Afegir un nou capítol", + "studyAddMembers": "Afegeix membres", + "studyInviteToTheStudy": "Convida a l’estudi", + "studyPleaseOnlyInvitePeopleYouKnow": "Si us plau, convida gent que coneixes, i que vólen unir-se activament a l’estudi.", + "studySearchByUsername": "Cerca per nom d'usuari", + "studySpectator": "Espectador", + "studyContributor": "Contribuïdor", + "studyKick": "Expulsa", + "studyLeaveTheStudy": "Deixar l’estudi", + "studyYouAreNowAContributor": "Ara ets un contribuïdor", + "studyYouAreNowASpectator": "Actualment ets un espectador", + "studyPgnTags": "Etiquetes PGN", + "studyLike": "M’agrada", + "studyUnlike": "Ja no m'agrada", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar en aquesta posició", + "studyCommentThisMove": "Comentar en aquest moviment", + "studyAnnotateWithGlyphs": "Anotar amb signes", + "studyTheChapterIsTooShortToBeAnalysed": "El capítol és massa curt per ser analitzat.", + "studyOnlyContributorsCanRequestAnalysis": "Només els contribuïdors de l’estudi poden demanar un anàlisis computeritzat.", + "studyGetAFullComputerAnalysis": "Obté un anàlisi complert desde el servidor de la línia principal.", + "studyMakeSureTheChapterIsComplete": "Segura’t que el capítol és complert. Només pots requerir l’anàlisi una sola vegada.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tots els membres sincronitzats es mantenen a la mateixa posició", + "studyShareChanges": "Comparteix els canvis amb els espectadors i guarda’ls al servidor", + "studyPlaying": "Jugant", + "studyShowEvalBar": "Barres d'avaluació", + "studyFirst": "Primer", + "studyPrevious": "Anterior", + "studyNext": "Següent", + "studyLast": "Últim", "studyShareAndExport": "Comparteix i exporta", - "studyStart": "Inici" + "studyCloneStudy": "Clona", + "studyStudyPgn": "PGN de l’estudi", + "studyDownloadAllGames": "Descarrega tots els jocs", + "studyChapterPgn": "PGN del capítol", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descarrega partida", + "studyStudyUrl": "URL de l’estudi", + "studyCurrentChapterUrl": "URL del capítol actual", + "studyYouCanPasteThisInTheForumToEmbed": "Pots enganxar això en el forum per insertar", + "studyStartAtInitialPosition": "Comnçar a la posició inicial", + "studyStartAtX": "Començar a {param}", + "studyEmbedInYourWebsite": "Inserta en la teva web o blog", + "studyReadMoreAboutEmbedding": "Llegeix més sobre insertar", + "studyOnlyPublicStudiesCanBeEmbedded": "Només els estudis públics poden ser inserits!", + "studyOpen": "Obrir", + "studyXBroughtToYouByY": "{param1}, presentat per {param2}", + "studyStudyNotFound": "Estudi no trobat", + "studyEditChapter": "Editar capítol", + "studyNewChapter": "Nou capítol", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientaciò", + "studyAnalysisMode": "Mode d'anàlisi", + "studyPinnedChapterComment": "Comentari del capítol fixat", + "studySaveChapter": "Guarda el capítol", + "studyClearAnnotations": "Netejar anotacions", + "studyClearVariations": "Netejar variacions", + "studyDeleteChapter": "Eliminar capítol", + "studyDeleteThisChapter": "Eliminar aquest capítol? No hi ha volta enrera!", + "studyClearAllCommentsInThisChapter": "Esborrar tots els comentaris, signes i marques en aquest capítol?", + "studyRightUnderTheBoard": "Just a sota el tauler", + "studyNoPinnedComment": "Cap", + "studyNormalAnalysis": "Análisis normal", + "studyHideNextMoves": "Oculta els següents moviments", + "studyInteractiveLesson": "Lliçó interactiva", + "studyChapterX": "Capítol {param}", + "studyEmpty": "Buit", + "studyStartFromInitialPosition": "Començar a la posició inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Començar a una posició personalitzada", + "studyLoadAGameByUrl": "Carregar una partida desde una URL", + "studyLoadAPositionFromFen": "Carregar una posició via codi FEN", + "studyLoadAGameFromPgn": "Carregar una partida PGN", + "studyAutomatic": "Automàtic", + "studyUrlOfTheGame": "URL del joc", + "studyLoadAGameFromXOrY": "Carregar una partida desde {param1} o {param2}", + "studyCreateChapter": "Crear capítol", + "studyCreateStudy": "Crear estudi", + "studyEditStudy": "Editar estudi", + "studyVisibility": "Visibilitat", + "studyPublic": "Públic", + "studyUnlisted": "No llistats", + "studyInviteOnly": "Només per invitació", + "studyAllowCloning": "Permitir clonat", + "studyNobody": "Ningú", + "studyOnlyMe": "Només jo", + "studyContributors": "Col·laboradors", + "studyMembers": "Membres", + "studyEveryone": "Tothom", + "studyEnableSync": "Habilita la sincronització", + "studyYesKeepEveryoneOnTheSamePosition": "Sí: tothom veu la mateixa posició", + "studyNoLetPeopleBrowseFreely": "No: permetre que la gent navegui lliurement", + "studyPinnedStudyComment": "Comentar estudi fixat", + "studyStart": "Inici", + "studySave": "Desa", + "studyClearChat": "Neteja el Chat", + "studyDeleteTheStudyChatHistory": "Eliminar el xat de l’estudi? No hi ha volta enrera!", + "studyDeleteStudy": "Eliminar estudi", + "studyConfirmDeleteStudy": "Esteu segurs que voleu eliminar el estudi? Tingues en compte que no es pot desfer. Per a confirmar-ho escriu el nom del estudi: {param}", + "studyWhereDoYouWantToStudyThat": "A on vols estudiar-ho?", + "studyGoodMove": "Bona jugada", + "studyMistake": "Errada", + "studyBrilliantMove": "Jugada brillant", + "studyBlunder": "Error greu", + "studyInterestingMove": "Jugada interessant", + "studyDubiousMove": "Jugada dubtosa", + "studyOnlyMove": "Única jugada", + "studyZugzwang": "Zugzwang (atzucac)", + "studyEqualPosition": "Posició igualada", + "studyUnclearPosition": "Posició poc clara", + "studyWhiteIsSlightlyBetter": "El blanc està lleugerament millor", + "studyBlackIsSlightlyBetter": "El negre està lleugerament millor", + "studyWhiteIsBetter": "El blanc està millor", + "studyBlackIsBetter": "El negre està millor", + "studyWhiteIsWinning": "El blanc està guanyant", + "studyBlackIsWinning": "El negre està guanyant", + "studyNovelty": "Novetat", + "studyDevelopment": "Desenvolupament", + "studyInitiative": "Iniciativa", + "studyAttack": "Atac", + "studyCounterplay": "Contra atac", + "studyTimeTrouble": "Problema de temps", + "studyWithCompensation": "Amb compensació", + "studyWithTheIdea": "Amb la idea", + "studyNextChapter": "Capítol següent", + "studyPrevChapter": "Capítol Anterior", + "studyStudyActions": "Acions de l'estudi", + "studyTopics": "Temes", + "studyMyTopics": "Els meus temes", + "studyPopularTopics": "Temes populars", + "studyManageTopics": "Gestiona els temes", + "studyBack": "Enrere", + "studyPlayAgain": "Torna a jugar", + "studyWhatWouldYouPlay": "Que jugaríeu en aquesta posició?", + "studyYouCompletedThisLesson": "Enhorabona, heu completat aquesta lliçó.", + "studyPerPage": "{param} per pàgina", + "studyNbChapters": "{count, plural, =1{{count} Capítol} other{{count} Capítols}}", + "studyNbGames": "{count, plural, =1{{count} Joc} other{{count} Jocs}}", + "studyNbMembers": "{count, plural, =1{{count} Membre} other{{count} Membres}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Enganxa el teu PGN aquí, fins a {count} partida} other{Enganxa el teu PGN aquí, fins a {count} partides}}", + "timeagoJustNow": "ara mateix", + "timeagoRightNow": "ara mateix", + "timeagoCompleted": "completat", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segon} other{en {count} segons}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minut} other{en {count} minuts}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} hores}}", + "timeagoInNbDays": "{count, plural, =1{en {count} dia} other{en {count} dies}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} setmana} other{en {count} setmanes}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} mesos}}", + "timeagoInNbYears": "{count, plural, =1{en {count} any} other{en {count} anys}}", + "timeagoNbMinutesAgo": "{count, plural, =1{fa {count} minut} other{fa {count} minuts}}", + "timeagoNbHoursAgo": "{count, plural, =1{fa {count} hora} other{fa {count} hores}}", + "timeagoNbDaysAgo": "{count, plural, =1{fa {count} dia} other{fa {count} dies}}", + "timeagoNbWeeksAgo": "{count, plural, =1{fa {count} setmana} other{fa {count} setmanes}}", + "timeagoNbMonthsAgo": "{count, plural, =1{fa {count} mes} other{fa {count} mesos}}", + "timeagoNbYearsAgo": "{count, plural, =1{fa {count} any} other{fa {count} anys}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Queda {count} minut} other{Queden {count} minuts}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Queda {count} hora} other{Queden {count} hores}}" } \ No newline at end of file diff --git a/lib/l10n/lila_cs.arb b/lib/l10n/lila_cs.arb index 7add3d2f1f..2448c873ee 100644 --- a/lib/l10n/lila_cs.arb +++ b/lib/l10n/lila_cs.arb @@ -1,4 +1,31 @@ { + "mobileAreYouSure": "Jste si jistý?", + "mobileBlindfoldMode": "Páska přes oči", + "mobileCancelTakebackOffer": "Zrušit nabídnutí vrácení tahu", + "mobileClearButton": "Vymazat", + "mobileCorrespondenceClearSavedMove": "Vymazat uložené tahy", + "mobileCustomGameJoinAGame": "Připojit se ke hře", + "mobileGreeting": "Ahoj, {param}", + "mobileGreetingWithoutName": "Ahoj", + "mobileHideVariation": "Schovej variace", + "mobileLiveStreamers": "Živé vysílání", + "mobileNoSearchResults": "Žádné výsledky", + "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobilePuzzleStormConfirmEndRun": "Chceš ukončit tento běh?", + "mobilePuzzleStormFilterNothingToShow": "Nic k zobrazení, prosím změn filtry", + "mobilePuzzleStormNothingToShow": "Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.", + "mobilePuzzleStormSubtitle": "Vyřeš co nejvíce úloh co dokážeš za 3 minuty.", + "mobilePuzzleStreakAbortWarning": "Ztratíte aktuální sérii a vaše skóre bude uloženo.", + "mobilePuzzleThemesSubtitle": "Hraj úlohy z tvých oblíbených zahájení, nebo si vyber styl.", + "mobileShareGamePGN": "Sdílet PGN", + "mobileShareGameURL": "Sdílet URL hry", + "mobileSharePositionAsFEN": "Sdílet pozici jako FEN", + "mobileSharePuzzle": "Sdílej tuto úlohu", + "mobileShowComments": "Zobraz komentáře", + "mobileShowResult": "Zobrazit výsledky", + "mobileShowVariations": "Zobraz variace", + "mobileSomethingWentWrong": "Něco se pokazilo.", + "mobileWaitingForOpponentToJoin": "Čeká se na připojení protihráče...", "activityActivity": "Aktivita", "activityHostedALiveStream": "Hostoval živý stream", "activityRankedInSwissTournament": "{param1}. místo v turnaji {param2}", @@ -11,6 +38,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Hrán {count} tah} few{Hrány {count} tahy} many{Hráno {count} tahů} other{Hráno {count} tahů}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} korespondenční partii} few{v {count} korespondenčních partiích} many{v {count} korespondenčních partiích} other{v {count} korespondenčních partiích}}", "activityCompletedNbGames": "{count, plural, =1{Dokončena {count} korespondenční partie} few{Dokončeny {count} korespondenční partie} many{Dokončeno {count} korespondenčních partií} other{Dokončeno {count} korespondenčních partií}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Dokončena {count} {param2} korespondenční partie} few{Dokončeny {count} {param2} korespondenční partie} many{Dokončeno {count} {param2} korespondenčních partii} other{Dokončeno {count} {param2} korespondenčních partii}}", "activityFollowedNbPlayers": "{count, plural, =1{Začali jste sledovat {count} hráče} few{Začali jste sledovat {count} hráče} many{Začali jste sledovat {count} hráčů} other{Začali jste sledovat {count} hráčů}}", "activityGainedNbFollowers": "{count, plural, =1{Získán {count} nový následovník} few{Získáni {count} noví následovníci} many{Získáno {count} nových následovníků} other{Získáno {count} nových následovníků}}", "activityHostedNbSimuls": "{count, plural, =1{Hostili jste {count} simultánku} few{Hostili jste {count} simultánky} many{Hostili jste {count} simultánek} other{Hostili jste {count} simultánek}}", @@ -21,7 +49,65 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Účast v {count} švýcarském turnaji} few{Účast ve {count} švýcarských turnajích} many{Účast v {count} švýcarských turnajích} other{Účast v {count} švýcarských turnajích}}", "activityJoinedNbTeams": "{count, plural, =1{Přidal se k {count} týmu} few{Přidal se k {count} týmům} many{Přidal se k {count} týmům} other{Přidal se k {count} týmům}}", "broadcastBroadcasts": "Přenosy", + "broadcastMyBroadcasts": "Moje vysílání", "broadcastLiveBroadcasts": "Živé přenosy turnajů", + "broadcastBroadcastCalendar": "Kalendář přenosů", + "broadcastNewBroadcast": "Nový živý přenos", + "broadcastSubscribedBroadcasts": "Odebírané přenosy", + "broadcastAboutBroadcasts": "O vysílání", + "broadcastHowToUseLichessBroadcasts": "Jak používat Lichess vysílání.", + "broadcastTheNewRoundHelp": "Nové kolo bude mít stejné členy a přispěvatele jako to předchozí.", + "broadcastAddRound": "Přidat kolo", + "broadcastOngoing": "Probíhající", + "broadcastUpcoming": "Chystané", + "broadcastCompleted": "Dokončené", + "broadcastCompletedHelp": "Lichess detekuje dokončení kola na základě zdrojových her. Tento přepínač použijte, pokud není k dispozici žádný zdroj.", + "broadcastRoundName": "Číslo kola", + "broadcastRoundNumber": "Číslo kola", + "broadcastTournamentName": "Název turnaje", + "broadcastTournamentDescription": "Stručný popis turnaje", + "broadcastFullDescription": "Úplný popis události", + "broadcastFullDescriptionHelp": "Volitelný dlouhý popis přenosu. {param1} je k dispozici. Délka musí být menší než {param2} znaků.", + "broadcastSourceSingleUrl": "PGN Zdrojová URL adresa", + "broadcastSourceUrlHelp": "URL adresa, kterou bude Lichess kontrolovat pro získání PGN aktualizací. Musí být veřejně přístupná z internetu.", + "broadcastSourceGameIds": "Až 64 ID Lichess her, oddělených mezerama.", + "broadcastStartDateTimeZone": "Datum zahájení v lokálním čase turnaje: {param}", + "broadcastStartDateHelp": "Nepovinné, pokud víte, kdy událost začíná", + "broadcastCurrentGameUrl": "URL adresa právě probíhající partie", + "broadcastDownloadAllRounds": "Stáhnout hry ze všech kol", + "broadcastResetRound": "Resetovat toto kolo", + "broadcastDeleteRound": "Smazat toto kolo", + "broadcastDefinitivelyDeleteRound": "Definitivně smazat kolo a jeho hry.", + "broadcastDeleteAllGamesOfThisRound": "Smazat všechny hry v tomto kole. Zdroj musí být aktivní aby bylo možno je znovu vytvořit.", + "broadcastEditRoundStudy": "Upravit studie kola", + "broadcastDeleteTournament": "Smazat tento turnaj", + "broadcastDefinitivelyDeleteTournament": "Opravdu smazat celý turnaj, všechna kola a hry.", + "broadcastShowScores": "Zobraz skóre hráču dle herních výsledků", + "broadcastReplacePlayerTags": "Volitelné: nahraď jména hráčů, rating a tituly", + "broadcastFideFederations": "FIDE federace", + "broadcastTop10Rating": "Rating top 10", + "broadcastFidePlayers": "FIDE hráči", + "broadcastFidePlayerNotFound": "FIDE hráč nenalezen", + "broadcastFideProfile": "FIDE profil", + "broadcastFederation": "Federace", + "broadcastAgeThisYear": "Věk tento rok", + "broadcastUnrated": "Nehodnocen", + "broadcastRecentTournaments": "Nedávné tournamenty", + "broadcastOpenLichess": "Otevřít v Lichess", + "broadcastTeams": "Týmy", + "broadcastBoards": "Šachovnice", + "broadcastOverview": "Přehled", + "broadcastUploadImage": "Nahrát obrázek turnaje", + "broadcastNoBoardsYet": "Zatím žádné šachovnice. Ty se zobrazí se po nahrání partií.", + "broadcastStartsAfter": "Začíná po {param}", + "broadcastStartVerySoon": "Vysílání začne velmi brzy.", + "broadcastNotYetStarted": "Vysílání ještě nezačalo.", + "broadcastOfficialWebsite": "Oficiální stránka", + "broadcastStandings": "Pořadí", + "broadcastOfficialStandings": "Oficiální pořadí", + "broadcastIframeHelp": "Více možností na {param}", + "broadcastScore": "Skóre", + "broadcastNbBroadcasts": "{count, plural, =1{{count} vysílání} few{{count} vysílání} many{{count} vysílání} other{{count} vysílání}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvat k partii", "challengeChallengeDeclined": "Výzva odmítnuta", @@ -145,6 +231,7 @@ "preferencesNotifyWeb": "Prohlížeč", "preferencesNotifyDevice": "Zařízení", "preferencesBellNotificationSound": "Typ zvukového upozornění", + "preferencesBlindfold": "Páska přes oči", "puzzlePuzzles": "Úlohy", "puzzlePuzzleThemes": "Motivy úloh", "puzzleRecommended": "Doporučené", @@ -340,8 +427,8 @@ "puzzleThemeXRayAttackDescription": "Figura útočí nebo chrání pole skrze nepřátelskou figuru.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Soupeř musí zahrát jakýkoliv tah, přičemž všechny zhoršují jeho pozici a zlepšují naší pozici.", - "puzzleThemeHealthyMix": "Mix úloh", - "puzzleThemeHealthyMixDescription": "Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.", + "puzzleThemeMix": "Mix úloh", + "puzzleThemeMixDescription": "Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.", "puzzleThemePlayerGames": "Z vašich her", "puzzleThemePlayerGamesDescription": "Vyhledejte úlohy vygenerované z vašich her, nebo z her jiného hráče.", "puzzleThemePuzzleDownloadInformation": "Tyto hádanky jsou ve veřejné doméně a lze je stáhnout z {param}.", @@ -420,6 +507,8 @@ "promoteVariation": "Povýšit variantu", "makeMainLine": "Povýšit na hlavní variantu", "deleteFromHere": "Smazat odsud", + "collapseVariations": "Schovat variace", + "expandVariations": "Zobrazit variace", "forceVariation": "Zobrazit jako variantu", "copyVariationPgn": "Zkopírovat PGN varianty", "move": "Tah", @@ -460,7 +549,6 @@ "replayMode": "Mód přehrávání", "realtimeReplay": "Jako při hře", "byCPL": "Dle CPL", - "openStudy": "Otevřít studii", "enable": "Povolit analýzu", "bestMoveArrow": "Šipka pro nejlepší tah", "showVariationArrows": "Zobrazit šipky variant", @@ -470,7 +558,6 @@ "memory": "Paměť", "infiniteAnalysis": "Nekonečná analýza", "removesTheDepthLimit": "Zapne nekonečnou analýzu a odstraní omezení hloubky propočtu", - "engineManager": "Správce enginu", "blunder": "Hrubá chyba", "mistake": "Chyba", "inaccuracy": "Nepřesnost", @@ -496,6 +583,7 @@ "latestForumPosts": "Poslední příspěvky", "players": "Hráči", "friends": "Přátelé", + "otherPlayers": "ostatní hráči", "discussions": "Konverzace", "today": "Dnes", "yesterday": "Včera", @@ -551,6 +639,7 @@ "rank": "Pořadí", "rankX": "Pořadí: {param}", "gamesPlayed": "Odehraných partií", + "ok": "OK", "cancel": "Zrušit", "whiteTimeOut": "Bílému došel čas", "blackTimeOut": "Černému došel čas", @@ -667,7 +756,6 @@ "block": "Blokovat", "blocked": "Blokován", "unblock": "Odblokovat", - "followsYou": "Vás sleduje", "xStartedFollowingY": "{param1} začal sledovat {param2}", "more": "Více", "memberSince": "Členem od", @@ -726,8 +814,10 @@ "ifNoneLeaveEmpty": "Pokud nemáte, nechte pole volné", "profile": "Profil", "editProfile": "Upravit profil", + "realName": "Skutečné jméno", "setFlair": "Nastav si svou ikonu za jménem", - "flair": "Upravitelná ikona", + "flair": "Ikona", + "youCanHideFlair": "Existuje nastavení které schová všechny uživatelské ikony za jménem po celém webu.", "biography": "O mně", "countryRegion": "Země nebo region", "thankYou": "Děkujeme!", @@ -744,12 +834,15 @@ "automaticallyProceedToNextGameAfterMoving": "Automaticky přejdi k další hře po tahu", "autoSwitch": "Přepnout automaticky", "puzzles": "Puzzle", + "onlineBots": "Online roboti", "name": "Jméno", "description": "Popis", "descPrivate": "Soukromý popis", "descPrivateHelp": "Text, který uvidí pouze členové týmu, kteří poté uvidí jenom tento text.", "no": "Ne", "yes": "Ano", + "website": "Web", + "mobile": "Mobil", "help": "Nápověda:", "createANewTopic": "Vytvořit nové téma", "topics": "Témata", @@ -768,7 +861,9 @@ "cheat": "Podvod", "troll": "Troll", "other": "Jiné", - "reportDescriptionHelp": "Vložte link na hru(y) a popište, co je špatně na chování tohoto hráče. (Pokud možno anglicky.)", + "reportCheatBoostHelp": "Zde vlož odkaz na hru(hry) a napiš co dělal tento uživatel. Nepiš pouze \"on podváděl\", ale napiš proč si myslíš že podváděl.", + "reportUsernameHelp": "Vysvětli co je urážlivého na jeho u6ivatelském jménu. Nepiš pouze \"Je urážlivé/nevhodné\", ale řekni i důvod proč to tak je, zejména pokud je urážka zatemněná, nebo je v jiném jazyce než v angličtině, nebo je ve slangu či jde o historickou nebokulturní referenci.", + "reportProcessedFasterInEnglish": "Nahlášení bude rychlejší pokud bude v angličtině.", "error_provideOneCheatedGameLink": "Prosím, uveďte alespoň jeden link na partii, ve které se podvádělo.", "by": "od {param}", "importedByX": "Importováno uživatelem {param}", @@ -801,6 +896,7 @@ "slow": "Pomalé", "insideTheBoard": "Na šachovnici", "outsideTheBoard": "Mimo šachovnici", + "allSquaresOfTheBoard": "Všechny pole na šachovnici", "onSlowGames": "Při pomalých hrách", "always": "Vždy", "never": "Nikdy", @@ -862,8 +958,10 @@ "simultaneousExhibitions": "Simultánky", "host": "Host", "hostColorX": "Barva zakladatele: {param}", + "yourPendingSimuls": "Tvoje simulace ve frontě", "createdSimuls": "Nově vytvořené simultánky", "hostANewSimul": "Vytvoř novou simultánku", + "signUpToHostOrJoinASimul": "Zaregistruj se abys mohl založit nebo se připojit k simulaci", "noSimulFound": "Simultánka nenalezena", "noSimulExplanation": "Tato simultánka neexistuje", "returnToSimulHomepage": "Vrať se na domovskou sránku simultánek", @@ -888,6 +986,7 @@ "keyboardShortcuts": "Klávesové zkratky", "keyMoveBackwardOrForward": "O tah vpřed/vzad", "keyGoToStartOrEnd": "běžte na začátek/konec", + "keyCycleSelectedVariation": "Projdi zkrze vybranou variaci", "keyShowOrHideComments": "zobrazte/skryjte komentáře", "keyEnterOrExitVariation": "Zobraz variantu", "keyRequestComputerAnalysis": "Vyžádejte si počítačovou analýzu, poučte se ze svých chyb", @@ -895,8 +994,12 @@ "keyNextBlunder": "Další hrubá chyba", "keyNextMistake": "Další chyba", "keyNextInaccuracy": "Další nepřesnost", + "keyPreviousBranch": "Předchozí větev", + "keyNextBranch": "Další větev", "toggleVariationArrows": "Přepnout šipky variant", + "cyclePreviousOrNextVariation": "Projdi předchozí/následující variantu", "toggleGlyphAnnotations": "Přepnout poznámky glyfů", + "togglePositionAnnotations": "Přepni zvýraznění pozice", "variationArrowsInfo": "Šipky variant umožňují navigaci bez použití seznamu tahů.", "playSelectedMove": "zahrát vybraný tah", "newTournament": "Nový turnaj", @@ -973,6 +1076,12 @@ "transparent": "Průhledné", "deviceTheme": "Motiv podle zařízení", "backgroundImageUrl": "URL zdroj obrázku na pozadí:", + "board": "Šachovnice", + "size": "Velikost", + "opacity": "Průhlednost", + "brightness": "Jas", + "hue": "Hue", + "boardReset": "Vrátit barvy na původní nastavení", "pieceSet": "Vzhled figur", "embedInYourWebsite": "Vložit na web", "usernameAlreadyUsed": "Toto uživatelské jméno již existuje. Zvol, prosím, jiné.", @@ -1055,6 +1164,7 @@ "agreementPolicy": "Souhlasím, že se budu řídit všemi pravidly Lichessu.", "searchOrStartNewDiscussion": "Začněte nebo vyhledejte konverzaci", "edit": "Upravit", + "bullet": "Bullet", "blitz": "Bleskové šachy", "rapid": "Rapid", "classical": "Klasické", @@ -1150,6 +1260,8 @@ "instructions": "Pokyny", "showMeEverything": "Ukaž mi všechno", "lichessPatronInfo": "Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem.\nVeškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů.", + "nothingToSeeHere": "Momentálně zde není nic k vidění.", + "stats": "Statistiky", "opponentLeftCounter": "{count, plural, =1{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundu.} few{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundy.} many{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.} other{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Mat v {count} půltahu} few{Mat v {count} půltazích} many{Mat v {count} půltazích} other{Mat v {count} půltazích}}", "nbBlunders": "{count, plural, =1{{count} hrubá chyba} few{{count} hrubé chyby} many{{count} hrubých chyb} other{{count} hrubých chyb}}", @@ -1246,6 +1358,178 @@ "stormXRuns": "{count, plural, =1{1 pokus} few{{count} pokusy} many{{count} pokusů} other{{count} pokusů}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odehrán jeden {param2}} few{Odehrány {count} pokusy {param2}} many{Odehráno {count} her {param2}} other{Odehráno {count} běhů z {param2}}}", "streamerLichessStreamers": "Lichess streameři", + "studyPrivate": "Soukromé", + "studyMyStudies": "Moje studie", + "studyStudiesIContributeTo": "Studie, ke kterým přispívám", + "studyMyPublicStudies": "Moje veřejné studie", + "studyMyPrivateStudies": "Moje soukromé studie", + "studyMyFavoriteStudies": "Moje oblíbené studie", + "studyWhatAreStudies": "Co jsou studie?", + "studyAllStudies": "Všechny studie", + "studyStudiesCreatedByX": "Studie vytvořené hráčem {param}", + "studyNoneYet": "Zatím nic.", + "studyHot": "Oblíbené", + "studyDateAddedNewest": "Datum přidání (nejnovější)", + "studyDateAddedOldest": "Datum přidání (nejstarší)", + "studyRecentlyUpdated": "Nedávno aktualizované", + "studyMostPopular": "Nejoblíbenější", + "studyAlphabetical": "Abecedně", + "studyAddNewChapter": "Přidat novou kapitolu", + "studyAddMembers": "Přidat uživatele", + "studyInviteToTheStudy": "Pozvat do studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosím zvěte pouze lidi, které znáte a kteří se chtějí aktivně připojit k této studii.", + "studySearchByUsername": "Hledat podle uživatelského jména", + "studySpectator": "Divák", + "studyContributor": "Přispívající", + "studyKick": "Vyhodit", + "studyLeaveTheStudy": "Opustit studii", + "studyYouAreNowAContributor": "Nyní jste přispívající", + "studyYouAreNowASpectator": "Nyní jste divák", + "studyPgnTags": "PGN tagy", + "studyLike": "To se mi líbí", + "studyUnlike": "Už se mi nelíbí", + "studyNewTag": "Nový štítek", + "studyCommentThisPosition": "Komentář k tomuto příspěvku", + "studyCommentThisMove": "Komentář k tomuto tahu", + "studyAnnotateWithGlyphs": "Popsat glyfy", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitola je moc krátká na to, aby mohla být zanalyzována.", + "studyOnlyContributorsCanRequestAnalysis": "Pouze přispěvatelé mohou požádat o počítačovou analýzu.", + "studyGetAFullComputerAnalysis": "Získejte plnou počítačovou analýzu hlavní varianty.", + "studyMakeSureTheChapterIsComplete": "Ujistěte se, že je kapitola úplná. O analýzu můžete požádat pouze jednou.", + "studyAllSyncMembersRemainOnTheSamePosition": "Všichni SYNCHRONIZOVANÍ členové zůstávají na stejné pozici", + "studyShareChanges": "Sdílet změny s diváky a uložit je na server", + "studyPlaying": "Probíhající", + "studyShowEvalBar": "Lišta hodnotící pozici", + "studyFirst": "První", + "studyPrevious": "Předchozí", + "studyNext": "Další", + "studyLast": "Poslední", "studyShareAndExport": "Sdílení a export", - "studyStart": "Začít" + "studyCloneStudy": "Klonovat", + "studyStudyPgn": "PGN studie", + "studyDownloadAllGames": "Stáhnout všechny hry", + "studyChapterPgn": "PGN kapitoly", + "studyCopyChapterPgn": "Kopírovat PGN", + "studyDownloadGame": "Stáhnout hru", + "studyStudyUrl": "URL studie", + "studyCurrentChapterUrl": "URL aktuální kapitoly", + "studyYouCanPasteThisInTheForumToEmbed": "Tento odkaz můžete vložit např. do diskusního fóra", + "studyStartAtInitialPosition": "Začít ve výchozí pozici", + "studyStartAtX": "Začít u tahu {param}", + "studyEmbedInYourWebsite": "Vložte vaší stránku nebo blog", + "studyReadMoreAboutEmbedding": "Přečtěte si více o vkládání", + "studyOnlyPublicStudiesCanBeEmbedded": "Lze vložit pouze veřejné studie!", + "studyOpen": "Otevřít", + "studyXBroughtToYouByY": "{param1} vám přináší {param2}", + "studyStudyNotFound": "Studie nenalezena", + "studyEditChapter": "Upravit kapitolu", + "studyNewChapter": "Nová kapitola", + "studyImportFromChapterX": "Importovat z {param}", + "studyOrientation": "Orientace", + "studyAnalysisMode": "Režim rozboru", + "studyPinnedChapterComment": "Připnutý komentář u kapitoly", + "studySaveChapter": "Uložit kapitolu", + "studyClearAnnotations": "Vymazat anotace", + "studyClearVariations": "Vymazat varianty", + "studyDeleteChapter": "Odstranit kapitolu", + "studyDeleteThisChapter": "Opravdu chcete odstranit tuto kapitolu? Kapitola bude navždy ztracena!", + "studyClearAllCommentsInThisChapter": "Vymazat všechny komentáře a výtvory v této kapitole?", + "studyRightUnderTheBoard": "Přímo pod šachovnicí", + "studyNoPinnedComment": "Žádný", + "studyNormalAnalysis": "Normální rozbor", + "studyHideNextMoves": "Skrýt následující tahy", + "studyInteractiveLesson": "Interaktivní lekce", + "studyChapterX": "Kapitola: {param}", + "studyEmpty": "Prázdné", + "studyStartFromInitialPosition": "Začít z původní pozice", + "studyEditor": "Tvůrce", + "studyStartFromCustomPosition": "Začít od vlastní pozice", + "studyLoadAGameByUrl": "Načíst hru podle URL", + "studyLoadAPositionFromFen": "Načíst polohu z FEN", + "studyLoadAGameFromPgn": "Načíst hru z PGN", + "studyAutomatic": "Automatický", + "studyUrlOfTheGame": "URL hry", + "studyLoadAGameFromXOrY": "Načíst hru z {param1} nebo {param2}", + "studyCreateChapter": "Vytvořit kapitolu", + "studyCreateStudy": "Vytvořit studii", + "studyEditStudy": "Upravit studii", + "studyVisibility": "Viditelnost", + "studyPublic": "Veřejná", + "studyUnlisted": "Neveřejná", + "studyInviteOnly": "Pouze na pozvání", + "studyAllowCloning": "Povolit klonování", + "studyNobody": "Nikdo", + "studyOnlyMe": "Pouze já", + "studyContributors": "Přispěvatelé", + "studyMembers": "Členové", + "studyEveryone": "Kdokoli", + "studyEnableSync": "Povolit synchronizaci", + "studyYesKeepEveryoneOnTheSamePosition": "Ano, všichni zůstávají na stejné pozici", + "studyNoLetPeopleBrowseFreely": "Ne, umožnit volné procházení", + "studyPinnedStudyComment": "Připnutý komentář studie", + "studyStart": "Začít", + "studySave": "Uložit", + "studyClearChat": "Vyčistit chat", + "studyDeleteTheStudyChatHistory": "Opravdu chcete vymazat historii chatu? Operaci nelze vrátit!", + "studyDeleteStudy": "Smazat studii", + "studyConfirmDeleteStudy": "Opravdu chcete smazat celou studii? Akci nelze vrátit zpět. Zadejte název studie pro potvrzení: {param}", + "studyWhereDoYouWantToStudyThat": "Kde chcete tuto pozici studovat?", + "studyGoodMove": "Dobrý tah", + "studyMistake": "Chyba", + "studyBrilliantMove": "Výborný tah", + "studyBlunder": "Hrubá chyba", + "studyInterestingMove": "Zajímavý tah", + "studyDubiousMove": "Pochybný tah", + "studyOnlyMove": "Jediný tah", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Rovná pozice", + "studyUnclearPosition": "Nejasná pozice", + "studyWhiteIsSlightlyBetter": "Bílý stojí o něco lépe", + "studyBlackIsSlightlyBetter": "Černý stojí o něco lépe", + "studyWhiteIsBetter": "Bílý stojí lépe", + "studyBlackIsBetter": "Černý stojí lépe", + "studyWhiteIsWinning": "Bílý má rozhodující výhodu", + "studyBlackIsWinning": "Černý má rozhodující výhodu", + "studyNovelty": "Novinka", + "studyDevelopment": "Vývin", + "studyInitiative": "S iniciativou", + "studyAttack": "S útokem", + "studyCounterplay": "S protihrou", + "studyTimeTrouble": "Časová tíseň", + "studyWithCompensation": "S kompenzací", + "studyWithTheIdea": "S ideou", + "studyNextChapter": "Další kapitola", + "studyPrevChapter": "Předchozí kapitola", + "studyStudyActions": "Akce pro studii", + "studyTopics": "Témata", + "studyMyTopics": "Moje témata", + "studyPopularTopics": "Oblíbená témata", + "studyManageTopics": "Správa témat", + "studyBack": "Zpět", + "studyPlayAgain": "Hrát znovu", + "studyWhatWouldYouPlay": "Co byste v této pozici hráli?", + "studyYouCompletedThisLesson": "Blahopřejeme! Dokončili jste tuto lekci.", + "studyPerPage": "{param} na stránku", + "studyNbChapters": "{count, plural, =1{{count} kapitola} few{{count} kapitoly} many{{count} kapitol} other{{count} kapitol}}", + "studyNbGames": "{count, plural, =1{{count} hra} few{{count} hry} many{{count} her} other{{count} her}}", + "studyNbMembers": "{count, plural, =1{{count} člen} few{{count} členi} many{{count} členů} other{{count} členů}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Vložte obsah vašeho PGN souboru (až {count} hra)} few{Vložte obsah vašeho PGN souboru (až {count} hry)} many{Vložte obsah vašeho PGN souboru (až {count} her)} other{Vložte obsah vašeho PGN souboru (až {count} her)}}", + "timeagoJustNow": "právě teď", + "timeagoRightNow": "právě teď", + "timeagoCompleted": "dokončeno", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundu} few{za {count} sekundy} many{za {count} sekund} other{za {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minuty} many{za {count} minut} other{za {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{za {count} hodinu} few{za {count} hodiny} many{za {count} hodin} other{za {count} hodin}}", + "timeagoInNbDays": "{count, plural, =1{za {count} den} few{za {count} dny} many{za {count} dnů} other{za {count} dnů}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} týden} few{za {count} týdny} many{za {count} týdnů} other{za {count} týdnů}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} měsíc} few{za {count} měsíce} many{za {count} měsíců} other{za {count} měsíců}}", + "timeagoInNbYears": "{count, plural, =1{za {count} rok} few{za {count} roky} many{za {count} let} other{za {count} let}}", + "timeagoNbMinutesAgo": "{count, plural, =1{před {count} minutou} few{před {count} minutami} many{před {count} minutami} other{před {count} minutami}}", + "timeagoNbHoursAgo": "{count, plural, =1{před {count} hodinou} few{před {count} hodinami} many{před {count} hodinami} other{před {count} hodinami}}", + "timeagoNbDaysAgo": "{count, plural, =1{před {count} dnem} few{před {count} dny} many{před {count} dny} other{před {count} dny}}", + "timeagoNbWeeksAgo": "{count, plural, =1{před {count} týdnem} few{před {count} týdny} many{před {count} týdny} other{před {count} týdny}}", + "timeagoNbMonthsAgo": "{count, plural, =1{před {count} měsícem} few{před {count} měsíci} many{před {count} měsíci} other{před {count} měsíci}}", + "timeagoNbYearsAgo": "{count, plural, =1{před {count} rokem} few{před {count} lety} many{před {count} lety} other{před {count} lety}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Zbývá {count} minuta} few{Zbývají {count} minuty} many{Zbývá {count} minut} other{Zbývá {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Zbývá {count} hodina} few{Zbývají {count} hodiny} many{Zbývá {count} hodin} other{Zbývá {count} hodin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_da.arb b/lib/l10n/lila_da.arb index 95a9dd0689..a36e9c7030 100644 --- a/lib/l10n/lila_da.arb +++ b/lib/l10n/lila_da.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle partier", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Bind for øjnene", + "mobileCancelTakebackOffer": "Annuller tilbud om tilbagetagelse", + "mobileClearButton": "Ryd", + "mobileCorrespondenceClearSavedMove": "Ryd gemt træk", + "mobileCustomGameJoinAGame": "Deltag i et parti", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hej, {param}", + "mobileGreetingWithoutName": "Hej", + "mobileHideVariation": "Skjul variation", "mobileHomeTab": "Hjem", - "mobilePuzzlesTab": "Opgaver", - "mobileToolsTab": "Værktøjer", - "mobileWatchTab": "Se", - "mobileSettingsTab": "Indstillinger", + "mobileLiveStreamers": "Live-streamere", "mobileMustBeLoggedIn": "Du skal være logget ind for at se denne side.", - "mobileSystemColors": "Systemfarver", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Ingen resultater", + "mobileNotFollowingAnyUser": "Du følger ikke nogen brugere.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spillere med \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Forstør brik, som trækkes", + "mobilePuzzleStormConfirmEndRun": "Vil du afslutte dette løb?", + "mobilePuzzleStormFilterNothingToShow": "Intet at vise, ændr venligst filtre", + "mobilePuzzleStormNothingToShow": "Intet at vise. Spil nogle runder af Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løs så mange opgaver som muligt på 3 minutter.", + "mobilePuzzleStreakAbortWarning": "Du vil miste din nuværende stime og din score vil blive gemt.", + "mobilePuzzleThemesSubtitle": "Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.", + "mobilePuzzlesTab": "Opgaver", + "mobileRecentSearches": "Seneste søgninger", "mobileSettingsHapticFeedback": "Haptisk feedback", "mobileSettingsImmersiveMode": "Fordybelsestilstand", "mobileSettingsImmersiveModeSubtitle": "Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.", - "mobileNotFollowingAnyUser": "Du følger ikke nogen brugere.", - "mobileAllGames": "Alle partier", - "mobileRecentSearches": "Seneste søgninger", - "mobileClearButton": "Ryd", - "mobilePlayersMatchingSearchTerm": "Spillere med \"{param}\"", - "mobileNoSearchResults": "Ingen resultater", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du vil miste din nuværende stime og din score vil blive gemt.", - "mobilePuzzleStormNothingToShow": "Intet at vise. Spil nogle runder af Puzzle Storm.", - "mobileSharePuzzle": "Del denne opgave", - "mobileShareGameURL": "Del partiets URL", + "mobileSettingsTab": "Indstillinger", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del partiets URL", "mobileSharePositionAsFEN": "Del position som FEN", - "mobileShowVariations": "Vis variationer", - "mobileHideVariation": "Skjul variation", + "mobileSharePuzzle": "Del denne opgave", "mobileShowComments": "Vis kommentarer", - "mobilePuzzleStormConfirmEndRun": "Vil du afslutte dette løb?", - "mobilePuzzleStormFilterNothingToShow": "Intet at vise, ændr venligst filtre", - "mobileCancelTakebackOffer": "Annuller tilbud om tilbagetagelse", - "mobileCancelDrawOffer": "Træk tilbud om remis tilbage", - "mobileWaitingForOpponentToJoin": "Venter på at modstander slutter sig til...", - "mobileBlindfoldMode": "Bind for øjnene", - "mobileLiveStreamers": "Live-streamere", - "mobileCustomGameJoinAGame": "Deltag i et parti", - "mobileCorrespondenceClearSavedMove": "Ryd gemt træk", - "mobileSomethingWentWrong": "Noget gik galt.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.", - "mobilePuzzleStormSubtitle": "Løs så mange opgaver som muligt på 3 minutter.", - "mobileGreeting": "Hej, {param}", - "mobileGreetingWithoutName": "Hej", + "mobileShowVariations": "Vis variationer", + "mobileSomethingWentWrong": "Noget gik galt.", + "mobileSystemColors": "Systemfarver", + "mobileTheme": "Tema", + "mobileToolsTab": "Værktøjer", + "mobileWaitingForOpponentToJoin": "Venter på at modstander slutter sig til...", + "mobileWatchTab": "Se", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Hostede en livestream", "activityRankedInSwissTournament": "Rangeret #{param1} i {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Spillede {count} træk} other{Spillede {count} træk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} korrespondanceparti} other{i {count} korrespondancepartier}}", "activityCompletedNbGames": "{count, plural, =1{Afsluttede {count} korrespondanceparti} other{Afsluttede {count} korrespondancepartier}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Afsluttede {count} {param2} korrespondanceparti} other{Afsluttede {count} {param2} korrespondancepartier}}", "activityFollowedNbPlayers": "{count, plural, =1{Begyndte at følge {count} spiller} other{Begyndte at følge {count} spillere}}", "activityGainedNbFollowers": "{count, plural, =1{Fik {count} ny følger} other{Fik {count} nye følgere}}", "activityHostedNbSimuls": "{count, plural, =1{Var vært for {count} simultanskakarrangement} other{Var vært for {count} simultanskakarrangementer}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Deltog i {count} schweizerturnering} other{Deltog i {count} schweizerturneringer}}", "activityJoinedNbTeams": "{count, plural, =1{Blev medlem af {count} hold} other{Blev medlem af {count} hold}}", "broadcastBroadcasts": "Udsendelser", + "broadcastMyBroadcasts": "Mine udsendelser", "broadcastLiveBroadcasts": "Live turnerings-udsendelser", + "broadcastBroadcastCalendar": "Kaldender for udsendelser", + "broadcastNewBroadcast": "Ny live-udsendelse", + "broadcastSubscribedBroadcasts": "Udsendelser, du abonnerer på", + "broadcastAboutBroadcasts": "Om udsendelse", + "broadcastHowToUseLichessBroadcasts": "Sådan bruges Lichess-udsendelser.", + "broadcastTheNewRoundHelp": "Den nye runde vil have de samme medlemmer og bidragydere som den foregående.", + "broadcastAddRound": "Tilføj en runde", + "broadcastOngoing": "I gang", + "broadcastUpcoming": "Kommende", + "broadcastCompleted": "Afsluttet", + "broadcastCompletedHelp": "Lichess registrerer rund-færdiggørelse baseret på kildepartierne. Brug denne skifter, hvis der ikke er nogen kilde.", + "broadcastRoundName": "Rundenavn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnavn", + "broadcastTournamentDescription": "Kort beskrivelse af turnering", + "broadcastFullDescription": "Fuld beskrivelse af begivenheden", + "broadcastFullDescriptionHelp": "Valgfri lang beskrivelse af transmissionen. {param1} er tilgængelig. Længde skal være mindre end {param2} tegn.", + "broadcastSourceSingleUrl": "URL for PGN-kilde", + "broadcastSourceUrlHelp": "URL som Lichess vil trække på for at få PGN updates. Den skal være offentlig tilgængelig fra internettet.", + "broadcastSourceGameIds": "Op til 64 Lichess parti-ID'er, adskilt af mellemrum.", + "broadcastStartDateTimeZone": "Startdato i turneringens lokale tidszone: {param}", + "broadcastStartDateHelp": "Valgfri, hvis du ved, hvornår begivenheden starter", + "broadcastCurrentGameUrl": "Nuværende parti URL", + "broadcastDownloadAllRounds": "Download alle runder", + "broadcastResetRound": "Nulstil denne runde", + "broadcastDeleteRound": "Slet denne runde", + "broadcastDefinitivelyDeleteRound": "Slet runden og dens partier endegyldigt.", + "broadcastDeleteAllGamesOfThisRound": "Slet alle partier i denne runde. Kilden skal være aktiv for at genskabe dem.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slet denne turnering", + "broadcastDefinitivelyDeleteTournament": "Slet hele turneringen, alle dens runder og alle dens partier.", + "broadcastShowScores": "Vis spilleres point baseret på resultater fra partier", + "broadcastReplacePlayerTags": "Valgfrit: udskift spillernavne, ratings og titler", + "broadcastFideFederations": "FIDE-føderationer", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE-spillere", + "broadcastFidePlayerNotFound": "FIDE-spiller ikke fundet", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Føderation", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Uden rating", + "broadcastRecentTournaments": "Seneste turneringer", + "broadcastOpenLichess": "Åbn i Lichess", + "broadcastTeams": "Hold", + "broadcastBoards": "Brætter", + "broadcastOverview": "Oversigt", + "broadcastSubscribeTitle": "Abonner på at blive underrettet, når hver runde starter. Du kan skifte mellem klokke- eller push-meddelelser for udsendelser i dine kontoindstillinger.", + "broadcastUploadImage": "Upload turneringsbillede", + "broadcastNoBoardsYet": "Ingen brætter endnu. Disse vises når partier er uploadet.", + "broadcastBoardsCanBeLoaded": "Brætter kan indlæses med en kilde eller via {param}", + "broadcastStartsAfter": "Starter efter {param}", + "broadcastStartVerySoon": "Udsendelsen starter meget snart.", + "broadcastNotYetStarted": "Udsendelsen er endnu ikke startet.", + "broadcastOfficialWebsite": "Officielt websted", + "broadcastStandings": "Stillinger", + "broadcastOfficialStandings": "Officiel stilling", + "broadcastIframeHelp": "Flere muligheder på {param}", + "broadcastWebmastersPage": "webmasters side", + "broadcastPgnSourceHelp": "En offentlig, realtids PGN-kilde til denne runde. Vi tilbyder også en {param} for hurtigere og mere effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Indlejr denne udsendelse på dit website", + "broadcastEmbedThisRound": "Indlejr {param} på dit website", + "broadcastRatingDiff": "Rating-forskel", + "broadcastGamesThisTournament": "Partier i denne turnering", + "broadcastScore": "Score", + "broadcastAllTeams": "Alle hold", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringssted", + "broadcastTopPlayers": "Topspillere", + "broadcastTimezone": "Tidszone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valgfri detaljer", + "broadcastPastBroadcasts": "Tidligere udsendelser", + "broadcastAllBroadcastsByMonth": "Vis alle udsendelser efter måned", + "broadcastNbBroadcasts": "{count, plural, =1{{count} udsendelse} other{{count} udsendelser}}", "challengeChallengesX": "Udfordringer: {param1}", "challengeChallengeToPlay": "Udfordr til et spil", "challengeChallengeDeclined": "Udfordring afvist", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Enhed", "preferencesBellNotificationSound": "Notifikationslyd", + "preferencesBlindfold": "Blindskak", "puzzlePuzzles": "Taktikopgaver", "puzzlePuzzleThemes": "Opgavetemaer", "puzzleRecommended": "Anbefalet", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "En brik angriber eller forsvarer et felt gennem en af modstanderens brikker.", "puzzleThemeZugzwang": "Træktvang", "puzzleThemeZugzwangDescription": "Modstanderen har begrænsede muligheder for træk, og ethvert træk vil forværre positionen.", - "puzzleThemeHealthyMix": "Sund blanding", - "puzzleThemeHealthyMixDescription": "Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.", + "puzzleThemeMix": "Sund blanding", + "puzzleThemeMixDescription": "Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.", "puzzleThemePlayerGames": "Spiller-partier", "puzzleThemePlayerGamesDescription": "Find taktikopgaver lavet ud fra dine egne partier eller fra en anden spillers partier.", "puzzleThemePuzzleDownloadInformation": "Disse opgaver er i offentligt domæne og kan downloades fra {param}.", @@ -447,7 +525,7 @@ "blackDidntMove": "Sort flyttede ikke", "requestAComputerAnalysis": "Anmod om en computeranalyse", "computerAnalysis": "Computeranalyse", - "computerAnalysisAvailable": "Computeranalyse klar", + "computerAnalysisAvailable": "Computeranalyse tilgængelig", "computerAnalysisDisabled": "Computeranalyse deaktiveret", "analysis": "Analysebræt", "depthX": "Dybde {param}", @@ -505,7 +583,6 @@ "replayMode": "Genafspilning", "realtimeReplay": "Realtid", "byCPL": "CBT", - "openStudy": "Åben studie", "enable": "Aktivér", "bestMoveArrow": "Bedste træk pil", "showVariationArrows": "Vis variantpile", @@ -515,7 +592,6 @@ "memory": "Hukommelse", "infiniteAnalysis": "Uendelig analyse", "removesTheDepthLimit": "Fjerner dybdegrænsen, og holder din computer varm", - "engineManager": "Administration af skakprogram", "blunder": "Brøler", "mistake": "Fejl", "inaccuracy": "Unøjagtighed", @@ -597,6 +673,7 @@ "rank": "Rang", "rankX": "Rangering: {param}", "gamesPlayed": "Antal partier spillet", + "ok": "Ok", "cancel": "Annuller", "whiteTimeOut": "Tid udløbet for hvid", "blackTimeOut": "Tid udløbet for sort", @@ -713,7 +790,6 @@ "block": "Blokér", "blocked": "Blokeret", "unblock": "Stop blokering", - "followsYou": "Følger dig", "xStartedFollowingY": "{param1} følger nu {param2}", "more": "Mere", "memberSince": "Medlem siden", @@ -819,7 +895,9 @@ "cheat": "Snyd", "troll": "Troll", "other": "Andet", - "reportDescriptionHelp": "Indsæt et link til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel.", + "reportCheatBoostHelp": "Indsæt linket til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel. Sig ikke blot \"de snyder\", men fortæl os, hvordan du nåede frem til den konklusion.", + "reportUsernameHelp": "Forklar, hvad der er stødende ved dette brugernavn. Sig ikke blot \"det er stødende/upassende\", men fortæl os, hvordan du nåede frem til denne konklusion, især hvis fornærmelsen er sløret, ikke er på engelsk, er slang eller er en historisk/kulturel reference.", + "reportProcessedFasterInEnglish": "Din indberetning vil blive behandlet hurtigere, hvis den er skrevet på engelsk.", "error_provideOneCheatedGameLink": "Angiv mindst ét link til et parti med snyd.", "by": "Af {param}", "importedByX": "Importeret af {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Vis mig alt", "lichessPatronInfo": "Lichess er en velgørenhedsorganisation og helt gratis/libre open source-software.\nAlle driftsomkostninger, udvikling og indhold finansieres udelukkende af brugerdonationer.", "nothingToSeeHere": "Intet at se her i øjeblikket.", + "stats": "Statistik", "opponentLeftCounter": "{count, plural, =1{Din modstander har forladt partiet. Du kan kræve at få tildelt sejren om {count} sekund.} other{Din modstander har forladt partiet. Du kan kræve at få tildelt sejren om {count} sekunder.}}", "mateInXHalfMoves": "{count, plural, =1{Mat i {count} halv-træk} other{Mat i {count} halv-træk}}", "nbBlunders": "{count, plural, =1{{count} brøler} other{{count} brølere}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{Første runde} other{{count} runder}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Spillede en runde af {param2}} other{Spillede {count} runder af {param2}}}", "streamerLichessStreamers": "Lichess-streamere", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studier", + "studyStudiesIContributeTo": "Studier jeg bidrager til", + "studyMyPublicStudies": "Mine offentlige studier", + "studyMyPrivateStudies": "Mine private studier", + "studyMyFavoriteStudies": "Mine favoritstudier", + "studyWhatAreStudies": "Hvad er studier?", + "studyAllStudies": "Alle studier", + "studyStudiesCreatedByX": "Studier oprettet af {param}", + "studyNoneYet": "Ingen endnu.", + "studyHot": "Populært", + "studyDateAddedNewest": "Dato tilføjet (nyeste)", + "studyDateAddedOldest": "Dato tilføjet (ældste)", + "studyRecentlyUpdated": "Nyligt opdateret", + "studyMostPopular": "Mest populære", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Tilføj et nyt kapitel", + "studyAddMembers": "Tilføj medlemmer", + "studyInviteToTheStudy": "Inviter til studiet", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter venligst kun personer du kender, og som ønsker at være en del af dette studie.", + "studySearchByUsername": "Søg på brugernavn", + "studySpectator": "Tilskuer", + "studyContributor": "Bidragsyder", + "studyKick": "Smid ud", + "studyLeaveTheStudy": "Forlad dette studie", + "studyYouAreNowAContributor": "Du er nu bidragsyder", + "studyYouAreNowASpectator": "Du er nu tilskuer", + "studyPgnTags": "PGN tags", + "studyLike": "Synes godt om", + "studyUnlike": "Synes ikke godt om", + "studyNewTag": "Nyt tag", + "studyCommentThisPosition": "Kommenter på denne stilling", + "studyCommentThisMove": "Kommenter på dette træk", + "studyAnnotateWithGlyphs": "Annoter med glyffer", + "studyTheChapterIsTooShortToBeAnalysed": "Dette kapitel er for kort til at blive analyseret.", + "studyOnlyContributorsCanRequestAnalysis": "Kun studiets bidragsydere kan anmode om en computeranalyse.", + "studyGetAFullComputerAnalysis": "Få en fuld server-computeranalyse af hovedlinjen.", + "studyMakeSureTheChapterIsComplete": "Sikr dig at kapitlet er færdigt. Du kan kun anmode om analyse én gang.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC medlemmer forbliver på samme stilling", + "studyShareChanges": "Del ændringer med tilskuere og gem dem på serveren", + "studyPlaying": "Spiller", + "studyShowEvalBar": "Evalueringssøjler", + "studyFirst": "Første", + "studyPrevious": "Forrige", + "studyNext": "Næste", + "studyLast": "Sidste", "studyShareAndExport": "Del & eksport", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie PGN", + "studyDownloadAllGames": "Download alle partier", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Download parti", + "studyStudyUrl": "Studie URL", + "studyCurrentChapterUrl": "Nuværende kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan indsætte dette i forummet for at indlejre", + "studyStartAtInitialPosition": "Start ved indledende stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Indlejr på din hjemmeside eller blog", + "studyReadMoreAboutEmbedding": "Læs mere om indlejring", + "studyOnlyPublicStudiesCanBeEmbedded": "Kun offentlige studier kan indlejres!", + "studyOpen": "Åbn", + "studyXBroughtToYouByY": "{param1} bragt til dig af {param2}", + "studyStudyNotFound": "Studie ikke fundet", + "studyEditChapter": "Rediger kapitel", + "studyNewChapter": "Nyt kapitel", + "studyImportFromChapterX": "Import fra {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysetilstand", + "studyPinnedChapterComment": "Fastgjort kapitelkommentar", + "studySaveChapter": "Gem kapitel", + "studyClearAnnotations": "Ryd annoteringer", + "studyClearVariations": "Ryd varianter", + "studyDeleteChapter": "Slet kapitel", + "studyDeleteThisChapter": "Slet dette kapitel? Du kan ikke fortryde!", + "studyClearAllCommentsInThisChapter": "Ryd alle kommentarer og figurer i dette kapitel?", + "studyRightUnderTheBoard": "Lige under brættet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul næste træk", + "studyInteractiveLesson": "Interaktiv lektion", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved indledende stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start fra brugerdefinerede stilling", + "studyLoadAGameByUrl": "Indlæs et parti fra URL", + "studyLoadAPositionFromFen": "Indlæs en stilling fra FEN", + "studyLoadAGameFromPgn": "Indlæs et parti fra PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiet", + "studyLoadAGameFromXOrY": "Indlæs et parti fra {param1} eller {param2}", + "studyCreateChapter": "Opret kapitel", + "studyCreateStudy": "Opret studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlighed", + "studyPublic": "Offentlig", + "studyUnlisted": "Ikke listet", + "studyInviteOnly": "Kun inviterede", + "studyAllowCloning": "Tillad kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Kun mig", + "studyContributors": "Bidragydere", + "studyMembers": "Medlemmer", + "studyEveryone": "Enhver", + "studyEnableSync": "Aktiver synk", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behold alle på den samme stilling", + "studyNoLetPeopleBrowseFreely": "Nej: lad folk gennemse frit", + "studyPinnedStudyComment": "Fastgjort studie-kommentar", + "studyStart": "Start", + "studySave": "Gem", + "studyClearChat": "Ryd chat", + "studyDeleteTheStudyChatHistory": "Slet studiets chat-historik? Du kan ikke fortryde!", + "studyDeleteStudy": "Slet studie", + "studyConfirmDeleteStudy": "Slet hele studiet? Det kan ikke fortrydes! Skriv navnet på studiet for at bekræfte: {param}", + "studyWhereDoYouWantToStudyThat": "Hvor vil du studere det?", + "studyGoodMove": "Godt træk", + "studyMistake": "Fejl", + "studyBrilliantMove": "Fremragende træk", + "studyBlunder": "Brøler", + "studyInterestingMove": "Interessant træk", + "studyDubiousMove": "Tvivlsomt træk", + "studyOnlyMove": "Eneste mulige træk", + "studyZugzwang": "Træktvang", + "studyEqualPosition": "Lige stilling", + "studyUnclearPosition": "Uafklaret stilling", + "studyWhiteIsSlightlyBetter": "Hvid står lidt bedre", + "studyBlackIsSlightlyBetter": "Sort står lidt bedre", + "studyWhiteIsBetter": "Hvid står bedre", + "studyBlackIsBetter": "Sort står bedre", + "studyWhiteIsWinning": "Hvid vinder", + "studyBlackIsWinning": "Sort vinder", + "studyNovelty": "Nyfunden", + "studyDevelopment": "Udvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Angreb", + "studyCounterplay": "Modspil", + "studyTimeTrouble": "Tidsproblemer", + "studyWithCompensation": "Med kompensation", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Næste kapitel", + "studyPrevChapter": "Forrige kapitel", + "studyStudyActions": "Studiehandlinger", + "studyTopics": "Emner", + "studyMyTopics": "Mine emner", + "studyPopularTopics": "Populære emner", + "studyManageTopics": "Administrér emner", + "studyBack": "Tilbage", + "studyPlayAgain": "Spil igen", + "studyWhatWouldYouPlay": "Hvad ville du spille i denne position?", + "studyYouCompletedThisLesson": "Tillykke! Du har fuldført denne lektion.", + "studyPerPage": "{param} pr. side", + "studyNbChapters": "{count, plural, =1{{count} kapitel} other{{count} kapitler}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmer}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Indsæt din PGN-tekst her, op til {count} parti} other{Indsæt din PGN-tekst her, op til {count} partier}}", + "timeagoJustNow": "for lidt siden", + "timeagoRightNow": "netop nu", + "timeagoCompleted": "afsluttet", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minut} other{om {count} minutter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timer}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dage}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} uge} other{om {count} uger}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} måned} other{om {count} måneder}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut siden} other{{count} minutter siden}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} time siden} other{{count} timer siden}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag siden} other{{count} dage siden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} uge siden} other{{count} uger siden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} måned siden} other{{count} måneder siden}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år siden} other{{count} år siden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut tilbage} other{{count} minutter tilbage}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time tilbage} other{{count} timer tilbage}}" } \ No newline at end of file diff --git a/lib/l10n/lila_de.arb b/lib/l10n/lila_de.arb index 81b1798b89..7c6c1262b7 100644 --- a/lib/l10n/lila_de.arb +++ b/lib/l10n/lila_de.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle Partien", + "mobileAreYouSure": "Bist du sicher?", + "mobileBlindfoldMode": "Blind spielen", + "mobileCancelTakebackOffer": "Zugzurücknahme-Angebot abbrechen", + "mobileClearButton": "Löschen", + "mobileCorrespondenceClearSavedMove": "Gespeicherten Zug löschen", + "mobileCustomGameJoinAGame": "Einer Partie beitreten", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Variante ausblenden", "mobileHomeTab": "Start", - "mobilePuzzlesTab": "Aufgaben", - "mobileToolsTab": "Werkzeuge", - "mobileWatchTab": "Zuschauen", - "mobileSettingsTab": "Einstellungen", + "mobileLiveStreamers": "Livestreamer", "mobileMustBeLoggedIn": "Du musst eingeloggt sein, um diese Seite anzuzeigen.", - "mobileSystemColors": "Systemfarben", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Keine Ergebnisse", + "mobileNotFollowingAnyUser": "Du folgst keinem Nutzer.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spieler mit \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Vergrößern der gezogenen Figur", + "mobilePuzzleStormConfirmEndRun": "Möchtest du diesen Durchlauf beenden?", + "mobilePuzzleStormFilterNothingToShow": "Nichts anzuzeigen, bitte passe deine Filter an", + "mobilePuzzleStormNothingToShow": "Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Löse so viele Aufgaben wie möglich in 3 Minuten.", + "mobilePuzzleStreakAbortWarning": "Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.", + "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.", + "mobilePuzzlesTab": "Aufgaben", + "mobileRecentSearches": "Letzte Suchen", "mobileSettingsHapticFeedback": "Haptisches Feedback", "mobileSettingsImmersiveMode": "Immersiver Modus", "mobileSettingsImmersiveModeSubtitle": "System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.", - "mobileNotFollowingAnyUser": "Du folgst keinem Nutzer.", - "mobileAllGames": "Alle Partien", - "mobileRecentSearches": "Letzte Suchen", - "mobileClearButton": "Löschen", - "mobilePlayersMatchingSearchTerm": "Spieler mit \"{param}\"", - "mobileNoSearchResults": "Keine Ergebnisse", - "mobileAreYouSure": "Bist du sicher?", - "mobilePuzzleStreakAbortWarning": "Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.", - "mobilePuzzleStormNothingToShow": "Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.", - "mobileSharePuzzle": "Teile diese Aufgabe", - "mobileShareGameURL": "Link der Partie teilen", + "mobileSettingsTab": "Optionen", "mobileShareGamePGN": "PGN teilen", + "mobileShareGameURL": "Link der Partie teilen", "mobileSharePositionAsFEN": "Stellung als FEN teilen", - "mobileShowVariations": "Varianten anzeigen", - "mobileHideVariation": "Variante ausblenden", + "mobileSharePuzzle": "Teile diese Aufgabe", "mobileShowComments": "Kommentare anzeigen", - "mobilePuzzleStormConfirmEndRun": "Möchtest du diesen Durchlauf beenden?", - "mobilePuzzleStormFilterNothingToShow": "Nichts anzuzeigen, bitte passe deine Filter an", - "mobileCancelTakebackOffer": "Zugzurücknahme-Angebot abbrechen", - "mobileCancelDrawOffer": "Remisangebot zurücknehmen", - "mobileWaitingForOpponentToJoin": "Warte auf Beitritt eines Gegners...", - "mobileBlindfoldMode": "Blind spielen", - "mobileLiveStreamers": "Livestreamer", - "mobileCustomGameJoinAGame": "Einer Partie beitreten", - "mobileCorrespondenceClearSavedMove": "Gespeicherten Zug löschen", - "mobileSomethingWentWrong": "Etwas ist schiefgelaufen.", "mobileShowResult": "Ergebnis anzeigen", - "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Thema.", - "mobilePuzzleStormSubtitle": "Löse in 3 Minuten so viele Aufgaben wie möglich.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", + "mobileShowVariations": "Varianten anzeigen", + "mobileSomethingWentWrong": "Etwas ist schiefgelaufen.", + "mobileSystemColors": "Systemfarben", + "mobileTheme": "Erscheinungsbild", + "mobileToolsTab": "Werkzeuge", + "mobileWaitingForOpponentToJoin": "Warte auf Beitritt eines Gegners...", + "mobileWatchTab": "Zuschauen", "activityActivity": "Verlauf", "activityHostedALiveStream": "Hat live gestreamt", "activityRankedInSwissTournament": "Hat Platz #{param1} im Turnier {param2} belegt", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Spielte {count} Zug} other{Spielte {count} Züge}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} Fernschachpartie} other{in {count} Fernschachpartien}}", "activityCompletedNbGames": "{count, plural, =1{Hat {count} Fernschachpartie gespielt} other{Hat {count} Fernschachpartien gespielt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Hat {count} {param2}-Fernschachpartie gespielt} other{Hat {count} {param2}-Fernschachpartien gespielt}}", "activityFollowedNbPlayers": "{count, plural, =1{Folgt {count} Spieler} other{Folgt {count} Spielern}}", "activityGainedNbFollowers": "{count, plural, =1{Hat {count} neuen Follower} other{Hat {count} neue Follower}}", "activityHostedNbSimuls": "{count, plural, =1{Hat {count} Simultanvorstellung gegeben} other{Hat {count} Simultanvorstellungen gegeben}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Hat an {count} Turnier nach Schweizer System teilgenommen} other{Hat an {count} Turnieren nach Schweizer System teilgenommen}}", "activityJoinedNbTeams": "{count, plural, =1{Ist {count} Team beigetreten} other{Ist {count} Teams beigetreten}}", "broadcastBroadcasts": "Übertragungen", + "broadcastMyBroadcasts": "Meine Übertragungen", "broadcastLiveBroadcasts": "Live-Turnierübertragungen", + "broadcastBroadcastCalendar": "Sendekalender", + "broadcastNewBroadcast": "Neue Liveübertragung", + "broadcastSubscribedBroadcasts": "Abonnierte Übertragungen", + "broadcastAboutBroadcasts": "Über Übertragungen", + "broadcastHowToUseLichessBroadcasts": "Wie man Lichess-Übertragungen benutzt.", + "broadcastTheNewRoundHelp": "Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende.", + "broadcastAddRound": "Eine Runde hinzufügen", + "broadcastOngoing": "Laufend", + "broadcastUpcoming": "Demnächst", + "broadcastCompleted": "Beendet", + "broadcastCompletedHelp": "Lichess erkennt Rundenabschlüsse basierend auf den Quellspielen. Verwende diesen Schalter, wenn keine Quelle vorhanden ist.", + "broadcastRoundName": "Rundenname", + "broadcastRoundNumber": "Rundennummer", + "broadcastTournamentName": "Turniername", + "broadcastTournamentDescription": "Kurze Turnierbeschreibung", + "broadcastFullDescription": "Vollständige Ereignisbeschreibung", + "broadcastFullDescriptionHelp": "Optionale, ausführliche Beschreibung der Übertragung. {param1} ist verfügbar. Die Beschreibung muss kürzer als {param2} Zeichen sein.", + "broadcastSourceSingleUrl": "PGN Quell-URL", + "broadcastSourceUrlHelp": "URL die Lichess abfragt um PGN Aktualisierungen zu erhalten. Sie muss öffentlich aus dem Internet zugänglich sein.", + "broadcastSourceGameIds": "Bis zu 64 Lichess Partie-IDs, getrennt durch Leerzeichen.", + "broadcastStartDateTimeZone": "Startdatum in der Zeitzone des Tunierstandortes: {param}", + "broadcastStartDateHelp": "Optional, falls du weißt wann das Ereignis beginnt", + "broadcastCurrentGameUrl": "URL der aktuellen Partie", + "broadcastDownloadAllRounds": "Alle Runden herunterladen", + "broadcastResetRound": "Diese Runde zurücksetzen", + "broadcastDeleteRound": "Diese Runde löschen", + "broadcastDefinitivelyDeleteRound": "Lösche die Runde und ihre Partien endgültig.", + "broadcastDeleteAllGamesOfThisRound": "Lösche alle Partien dieser Runde. Die Quelle muss aktiv sein, um sie neu zu erstellen.", + "broadcastEditRoundStudy": "Rundenstudie bearbeiten", + "broadcastDeleteTournament": "Dieses Turnier löschen", + "broadcastDefinitivelyDeleteTournament": "Lösche definitiv das gesamte Turnier, alle seine Runden und Partien.", + "broadcastShowScores": "Punktestand der Spieler basierend auf Spielergebnissen anzeigen", + "broadcastReplacePlayerTags": "Optional: Spielernamen, Wertungen und Titel ersetzen", + "broadcastFideFederations": "FIDE-Verbände", + "broadcastTop10Rating": "Top-10-Wertung", + "broadcastFidePlayers": "FIDE-Spieler", + "broadcastFidePlayerNotFound": "FIDE-Spieler nicht gefunden", + "broadcastFideProfile": "FIDE-Profil", + "broadcastFederation": "Verband", + "broadcastAgeThisYear": "Alter in diesem Jahr", + "broadcastUnrated": "Ungewertet", + "broadcastRecentTournaments": "Letzte Turniere", + "broadcastOpenLichess": "In Lichess öffnen", + "broadcastTeams": "Teams", + "broadcastBoards": "Bretter", + "broadcastOverview": "Überblick", + "broadcastSubscribeTitle": "Abonnieren, um bei Rundenbeginn benachrichtigt zu werden. Du kannst in deinen Benutzereinstellungen für Übertragungen zwischen einer Benachrichtigung per Glocke oder per Push-Benachrichtigung wählen.", + "broadcastUploadImage": "Turnierbild hochladen", + "broadcastNoBoardsYet": "Noch keine Bretter vorhanden. Diese werden angezeigt, sobald die Partien hochgeladen werden.", + "broadcastBoardsCanBeLoaded": "Die Bretter können per Quelle oder via {param} geladen werden", + "broadcastStartsAfter": "Beginnt nach {param}", + "broadcastStartVerySoon": "Diese Übertragung wird in Kürze beginnen.", + "broadcastNotYetStarted": "Die Übertragung hat noch nicht begonnen.", + "broadcastOfficialWebsite": "Offizielle Webseite", + "broadcastStandings": "Rangliste", + "broadcastOfficialStandings": "Offizielle Rangliste", + "broadcastIframeHelp": "Weitere Optionen auf der {param}", + "broadcastWebmastersPage": "Webmaster-Seite", + "broadcastPgnSourceHelp": "Eine öffentliche Echtzeit-PGN-Quelle für diese Runde. Wir bieten auch eine {param} für eine schnellere und effizientere Synchronisation.", + "broadcastEmbedThisBroadcast": "Bette diese Übertragung in deine Webseite ein", + "broadcastEmbedThisRound": "Bette {param} in deine Webseite ein", + "broadcastRatingDiff": "Wertungsdifferenz", + "broadcastGamesThisTournament": "Partien in diesem Turnier", + "broadcastScore": "Punktestand", + "broadcastAllTeams": "Alle Teams", + "broadcastTournamentFormat": "Turnierformat", + "broadcastTournamentLocation": "Turnierort", + "broadcastTopPlayers": "Spitzenspieler", + "broadcastTimezone": "Zeitzone", + "broadcastFideRatingCategory": "FIDE-Wertungskategorie", + "broadcastOptionalDetails": "Optionale Details", + "broadcastPastBroadcasts": "Vergangene Übertragungen", + "broadcastAllBroadcastsByMonth": "Alle Übertragungen nach Monat anzeigen", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Übertragung} other{{count} Übertragungen}}", "challengeChallengesX": "Herausforderungen: {param1}", "challengeChallengeToPlay": "Zu einer Partie herausfordern", "challengeChallengeDeclined": "Herausforderung abgelehnt", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Gerät", "preferencesBellNotificationSound": "Glocken-Benachrichtigungston", + "preferencesBlindfold": "Blindschach", "puzzlePuzzles": "Taktikaufgaben", "puzzlePuzzleThemes": "Aufgabenthemen", "puzzleRecommended": "Empfohlen", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Eine Figur attackiert oder verteidigt ein Feld durch eine gegnerische Figur hindurch.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Der Gegner ist in der Anzahl seiner Züge limitiert und jeder seiner Züge verschlechtert seine Stellung.", - "puzzleThemeHealthyMix": "Gesunder Mix", - "puzzleThemeHealthyMixDescription": "Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.", + "puzzleThemeMix": "Gesunder Mix", + "puzzleThemeMixDescription": "Ein bisschen von allem. Du weißt nicht, was dich erwartet, deshalb bleibst du bereit für alles! Genau wie in echten Partien.", "puzzleThemePlayerGames": "Partien von Spielern", "puzzleThemePlayerGamesDescription": "Suche Aufgaben, die aus deinen Partien, oder den Partien eines anderen Spielers generiert wurden.", "puzzleThemePuzzleDownloadInformation": "Diese Aufgaben sind öffentlich zugänglich und können unter {param} heruntergeladen werden.", @@ -505,7 +583,6 @@ "replayMode": "Wiedergabemodus", "realtimeReplay": "Echtzeit", "byCPL": "Nach CPL", - "openStudy": "Studie öffnen", "enable": "Einschalten", "bestMoveArrow": "Pfeil für besten Zug", "showVariationArrows": "Varianten-Pfeile anzeigen", @@ -515,7 +592,6 @@ "memory": "Arbeitsspeicher", "infiniteAnalysis": "Endlose Analyse", "removesTheDepthLimit": "Entfernt die Tiefenbegrenzung und hält deinen Computer warm", - "engineManager": "Engineverwaltung", "blunder": "Grober Patzer", "mistake": "Fehler", "inaccuracy": "Ungenauigkeit", @@ -597,6 +673,7 @@ "rank": "Rang", "rankX": "Platz: {param}", "gamesPlayed": "Gespielte Partien", + "ok": "OK", "cancel": "Abbrechen", "whiteTimeOut": "Zeitüberschreitung von Weiß", "blackTimeOut": "Zeitüberschreitung von Schwarz", @@ -713,7 +790,6 @@ "block": "Blockieren", "blocked": "Blockiert", "unblock": "Nicht mehr blockieren", - "followsYou": "Folgt dir", "xStartedFollowingY": "{param1} folgt jetzt {param2}", "more": "Mehr", "memberSince": "Mitglied seit", @@ -819,7 +895,9 @@ "cheat": "Betrug", "troll": "Troll", "other": "Sonstiges", - "reportDescriptionHelp": "Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie Du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.", + "reportCheatBoostHelp": "Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Verhaltens des Spielers. Bitte schreibe nicht einfach nur „Die schummeln (bzw. betrügen)“, sondern begründe auch, wie Du zu diesem Schluss kommst.", + "reportUsernameHelp": "Erkläre, was an diesem Benutzernamen beleidigend oder unangemessen ist. Sage nicht einfach \"Der Name ist beleidigend/unangemessen\", sondern erkläre, wie du zu dieser Schlussfolgerung gekommen bist. Insbesondere wenn die Beleidigung verschleiert wird, nicht auf Englisch, ist in Slang, oder ist ein historischer/kultureller Bezugspunkt.", + "reportProcessedFasterInEnglish": "Ihr Bericht wird schneller bearbeitet, wenn er auf Englisch verfasst ist.", "error_provideOneCheatedGameLink": "Bitte gib mindestens einen Link zu einem Spiel an, in dem betrogen wurde.", "by": "von {param}", "importedByX": "Importiert von {param}", @@ -1210,13 +1288,14 @@ "since": "Seit", "until": "Bis", "lichessDbExplanation": "Aus gewerteten Partien aller Lichess-Spieler", - "switchSides": "andere Farbe", + "switchSides": "Seitenwechsel", "closingAccountWithdrawAppeal": "Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehen", "ourEventTips": "Unsere Tipps für die Organisation von Veranstaltungen", "instructions": "Anleitung", "showMeEverything": "Alles zeigen", "lichessPatronInfo": "Lichess ist eine Wohltätigkeitsorganisation und eine völlig kostenlose/freie Open-Source-Software.\nAlle Betriebskosten, Entwicklung und Inhalte werden ausschließlich durch Benutzerspenden finanziert.", "nothingToSeeHere": "Im Moment gibt es hier nichts zu sehen.", + "stats": "Statistiken", "opponentLeftCounter": "{count, plural, =1{Dein Gegner hat die Partie verlassen. Du kannst in {count} Sekunde den Sieg beanspruchen.} other{Dein Gegner hat die Partie verlassen. Du kannst in {count} Sekunden den Sieg beanspruchen.}}", "mateInXHalfMoves": "{count, plural, =1{Matt in {count} Halbzug} other{Matt in {count} Halbzügen}}", "nbBlunders": "{count, plural, =1{{count} grober Patzer} other{{count} grobe Patzer}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 Durchlauf} other{{count} Durchläufe}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Hat einen Durchlauf von {param2} gespielt} other{Hat {count} Durchläufe von {param2} gespielt}}", "streamerLichessStreamers": "Lichess Streamer", + "studyPrivate": "Privat", + "studyMyStudies": "Meine Studien", + "studyStudiesIContributeTo": "Studien, an denen ich mitwirke", + "studyMyPublicStudies": "Meine öffentlichen Studien", + "studyMyPrivateStudies": "Meine privaten Studien", + "studyMyFavoriteStudies": "Meine Lieblingsstudien", + "studyWhatAreStudies": "Was sind Studien?", + "studyAllStudies": "Alle Studien", + "studyStudiesCreatedByX": "Von {param} erstellte Studien", + "studyNoneYet": "Bisher keine.", + "studyHot": "Angesagt", + "studyDateAddedNewest": "Veröffentlichungsdatum (neueste)", + "studyDateAddedOldest": "Veröffentlichungsdatum (älteste)", + "studyRecentlyUpdated": "Kürzlich aktualisiert", + "studyMostPopular": "Beliebteste", + "studyAlphabetical": "Alphabetisch", + "studyAddNewChapter": "Neues Kapitel hinzufügen", + "studyAddMembers": "Mitglieder hinzufügen", + "studyInviteToTheStudy": "Zur Studie einladen", + "studyPleaseOnlyInvitePeopleYouKnow": "Bitte lade nur Leute ein, die dich kennen und die aktiv an dieser Studie teilnehmen möchten.", + "studySearchByUsername": "Suche nach Benutzernamen", + "studySpectator": "Zuschauer", + "studyContributor": "Mitwirkender", + "studyKick": "Rauswerfen", + "studyLeaveTheStudy": "Studie verlassen", + "studyYouAreNowAContributor": "Du bist jetzt ein Mitwirkender", + "studyYouAreNowASpectator": "Du bist jetzt Zuschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gefällt mir", + "studyUnlike": "Gefällt mir nicht mehr", + "studyNewTag": "Neuer Tag", + "studyCommentThisPosition": "Kommentiere diese Stellung", + "studyCommentThisMove": "Kommentiere diesen Zug", + "studyAnnotateWithGlyphs": "Mit Symbolen kommentieren", + "studyTheChapterIsTooShortToBeAnalysed": "Das Kapitel ist zu kurz zum Analysieren.", + "studyOnlyContributorsCanRequestAnalysis": "Nur Mitwirkende an der Studie können eine Computeranalyse anfordern.", + "studyGetAFullComputerAnalysis": "Erhalte eine vollständige serverseitige Computeranalyse der Hauptvariante.", + "studyMakeSureTheChapterIsComplete": "Stelle sicher, dass das Kapitel vollständig ist. Die Analyse kann nur einmal angefordert werden.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle synchronisierten Mitglieder sehen die gleiche Stellung", + "studyShareChanges": "Teile Änderungen mit den Zuschauern und speichere sie auf dem Server", + "studyPlaying": "Laufende Partien", + "studyShowEvalBar": "Stellungsbewertungs-Balken", + "studyFirst": "Erste Seite", + "studyPrevious": "Zurück", + "studyNext": "Weiter", + "studyLast": "Letzte Seite", "studyShareAndExport": "Teilen und exportieren", - "studyStart": "Start" + "studyCloneStudy": "Klonen", + "studyStudyPgn": "Studien PGN", + "studyDownloadAllGames": "Lade alle Partien herunter", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopieren", + "studyDownloadGame": "Lade die Partie herunter", + "studyStudyUrl": "Studien URL", + "studyCurrentChapterUrl": "URL des aktuellen Kapitels", + "studyYouCanPasteThisInTheForumToEmbed": "Zum Einbinden füge dies im Forum ein", + "studyStartAtInitialPosition": "Beginne mit der Anfangsstellung", + "studyStartAtX": "Beginne mit {param}", + "studyEmbedInYourWebsite": "In deine Webseite oder deinen Blog einbetten", + "studyReadMoreAboutEmbedding": "Lies mehr über das Einbinden", + "studyOnlyPublicStudiesCanBeEmbedded": "Nur öffentliche Studien können eingebunden werden!", + "studyOpen": "Öffnen", + "studyXBroughtToYouByY": "{param1} präsentiert von {param2}", + "studyStudyNotFound": "Studie nicht gefunden", + "studyEditChapter": "Kapitel bearbeiten", + "studyNewChapter": "Neues Kapitel", + "studyImportFromChapterX": "Importiere aus {param}", + "studyOrientation": "Ausrichtung", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Angepinnte Kapitelkommentare", + "studySaveChapter": "Kapitel speichern", + "studyClearAnnotations": "Anmerkungen löschen", + "studyClearVariations": "Varianten löschen", + "studyDeleteChapter": "Kapitel löschen", + "studyDeleteThisChapter": "Kapitel löschen. Dies kann nicht rückgängig gemacht werden!", + "studyClearAllCommentsInThisChapter": "Alle Kommentare, Symbole und gezeichnete Formen in diesem Kapitel löschen", + "studyRightUnderTheBoard": "Direkt unterhalb des Bretts", + "studyNoPinnedComment": "Keine", + "studyNormalAnalysis": "Normale Analyse", + "studyHideNextMoves": "Nächste Züge ausblenden", + "studyInteractiveLesson": "Interaktive Übung", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Leer", + "studyStartFromInitialPosition": "Von Ausgangsstellung starten", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Von benutzerdefinierter Stellung starten", + "studyLoadAGameByUrl": "Lade eine Partie mittels URL", + "studyLoadAPositionFromFen": "Lade eine Partie mittels FEN", + "studyLoadAGameFromPgn": "Lade eine Partie mittels PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL der Partie", + "studyLoadAGameFromXOrY": "Partie von {param1} oder {param2} laden", + "studyCreateChapter": "Kapitel erstellen", + "studyCreateStudy": "Studie erstellen", + "studyEditStudy": "Studie bearbeiten", + "studyVisibility": "Sichtbarkeit", + "studyPublic": "Öffentlich", + "studyUnlisted": "Ungelistet", + "studyInviteOnly": "Nur mit Einladung", + "studyAllowCloning": "Klonen erlaubt", + "studyNobody": "Niemand", + "studyOnlyMe": "Nur ich", + "studyContributors": "Mitwirkende", + "studyMembers": "Mitglieder", + "studyEveryone": "Alle", + "studyEnableSync": "Sync aktivieren", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: Gleiche Stellung für alle", + "studyNoLetPeopleBrowseFreely": "Nein: Unabhängige Navigation für alle", + "studyPinnedStudyComment": "Angepinnter Studienkommentar", + "studyStart": "Start", + "studySave": "Speichern", + "studyClearChat": "Chat löschen", + "studyDeleteTheStudyChatHistory": "Chatverlauf der Studie löschen? Dies kann nicht rückgängig gemacht werden!", + "studyDeleteStudy": "Studie löschen", + "studyConfirmDeleteStudy": "Die gesamte Studie löschen? Es gibt kein Zurück! Gib zur Bestätigung den Namen der Studie ein: {param}", + "studyWhereDoYouWantToStudyThat": "Welche Studie möchtest du nutzen?", + "studyGoodMove": "Guter Zug", + "studyMistake": "Fehler", + "studyBrilliantMove": "Brillanter Zug", + "studyBlunder": "Grober Patzer", + "studyInterestingMove": "Interessanter Zug", + "studyDubiousMove": "Fragwürdiger Zug", + "studyOnlyMove": "Einziger Zug", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Ausgeglichene Stellung", + "studyUnclearPosition": "Unklare Stellung", + "studyWhiteIsSlightlyBetter": "Weiß steht leicht besser", + "studyBlackIsSlightlyBetter": "Schwarz steht leicht besser", + "studyWhiteIsBetter": "Weiß steht besser", + "studyBlackIsBetter": "Schwarz steht besser", + "studyWhiteIsWinning": "Weiß steht auf Gewinn", + "studyBlackIsWinning": "Schwarz steht auf Gewinn", + "studyNovelty": "Neuerung", + "studyDevelopment": "Entwicklung", + "studyInitiative": "Initiative", + "studyAttack": "Angriff", + "studyCounterplay": "Gegenspiel", + "studyTimeTrouble": "Zeitnot", + "studyWithCompensation": "Mit Kompensation", + "studyWithTheIdea": "Mit der Idee", + "studyNextChapter": "Nächstes Kapitel", + "studyPrevChapter": "Vorheriges Kapitel", + "studyStudyActions": "Studien-Aktionen", + "studyTopics": "Themen", + "studyMyTopics": "Meine Themen", + "studyPopularTopics": "Beliebte Themen", + "studyManageTopics": "Themen verwalten", + "studyBack": "Zurück", + "studyPlayAgain": "Erneut spielen", + "studyWhatWouldYouPlay": "Was würdest du in dieser Stellung spielen?", + "studyYouCompletedThisLesson": "Gratulation! Du hast diese Lektion abgeschlossen.", + "studyPerPage": "{param} pro Seite", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", + "studyNbMembers": "{count, plural, =1{{count} Mitglied} other{{count} Mitglieder}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füge deinen PGN Text hier ein, bis zu {count} Partie} other{Füge dein PGN Text hier ein, bis zu {count} Partien}}", + "timeagoJustNow": "gerade eben", + "timeagoRightNow": "gerade jetzt", + "timeagoCompleted": "abgeschlossen", + "timeagoInNbSeconds": "{count, plural, =1{in {count} Sekunde} other{in {count} Sekunden}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} Minute} other{in {count} Minuten}}", + "timeagoInNbHours": "{count, plural, =1{in {count} Stunde} other{in {count} Stunden}}", + "timeagoInNbDays": "{count, plural, =1{in {count} Tag} other{in {count} Tagen}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} Woche} other{in {count} Wochen}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} Monat} other{in {count} Monaten}}", + "timeagoInNbYears": "{count, plural, =1{in {count} Jahr} other{in {count} Jahren}}", + "timeagoNbMinutesAgo": "{count, plural, =1{vor {count} Minute} other{vor {count} Minuten}}", + "timeagoNbHoursAgo": "{count, plural, =1{vor {count} Stunde} other{vor {count} Stunden}}", + "timeagoNbDaysAgo": "{count, plural, =1{vor {count} Tag} other{vor {count} Tagen}}", + "timeagoNbWeeksAgo": "{count, plural, =1{vor {count} Woche} other{vor {count} Wochen}}", + "timeagoNbMonthsAgo": "{count, plural, =1{vor {count} Monat} other{vor {count} Monaten}}", + "timeagoNbYearsAgo": "{count, plural, =1{vor {count} Jahr} other{vor {count} Jahren}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minute verbleibend} other{{count} Minuten verbleibend}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Stunde verbleiben} other{{count} Stunden verbleiben}}" } \ No newline at end of file diff --git a/lib/l10n/lila_el.arb b/lib/l10n/lila_el.arb index b7974e8820..4c45237e95 100644 --- a/lib/l10n/lila_el.arb +++ b/lib/l10n/lila_el.arb @@ -1,8 +1,47 @@ { - "mobileShowResult": "Εμφάνιση αποτελέσματος", - "mobilePuzzleStormSubtitle": "Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.", + "mobileAllGames": "Όλα τα παιχνίδια", + "mobileAreYouSure": "Είστε σίγουροι;", + "mobileBlindfoldMode": "Τυφλό", + "mobileCancelTakebackOffer": "Ακυρώστε την προσφορά αναίρεσης της κίνησης", + "mobileClearButton": "Εκκαθάριση", + "mobileCorrespondenceClearSavedMove": "Εκκαθάριση αποθηκευμένης κίνησης", + "mobileCustomGameJoinAGame": "Συμμετοχή σε παιχνίδι", + "mobileFeedbackButton": "Πείτε μας τη γνώμη σας", "mobileGreeting": "Καλωσορίσατε, {param}", "mobileGreetingWithoutName": "Καλωσορίσατε", + "mobileHideVariation": "Απόκρυψη παραλλαγής", + "mobileHomeTab": "Αρχική", + "mobileLiveStreamers": "Streamers ζωντανά αυτή τη στιγμή", + "mobileMustBeLoggedIn": "Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.", + "mobileNoSearchResults": "Δεν βρέθηκαν αποτελέσματα", + "mobileNotFollowingAnyUser": "Δεν ακολουθείτε κανέναν χρήστη.", + "mobileOkButton": "ΟΚ", + "mobilePlayersMatchingSearchTerm": "Παίκτες με \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Μεγέθυνση του επιλεγμένου κομματιού", + "mobilePuzzleStormConfirmEndRun": "Θέλετε να τερματίσετε αυτόν τον γύρο;", + "mobilePuzzleStormFilterNothingToShow": "Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες", + "mobilePuzzleStormNothingToShow": "Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.", + "mobilePuzzleThemesSubtitle": "Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.", + "mobilePuzzlesTab": "Γρίφοι", + "mobileRecentSearches": "Πρόσφατες αναζητήσεις", + "mobileSettingsHapticFeedback": "Απόκριση δόνησης", + "mobileSettingsImmersiveMode": "Λειτουργία εστίασης", + "mobileSettingsImmersiveModeSubtitle": "Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.", + "mobileSettingsTab": "Ρυθμίσεις", + "mobileShareGamePGN": "Κοινοποίηση PGN", + "mobileShareGameURL": "Κοινοποίηση URL παιχνιδιού", + "mobileSharePositionAsFEN": "Κοινοποίηση θέσης ως FEN", + "mobileSharePuzzle": "Κοινοποίηση γρίφου", + "mobileShowComments": "Εμφάνιση σχολίων", + "mobileShowResult": "Εμφάνιση αποτελέσματος", + "mobileShowVariations": "Εμφάνιση παραλλαγών", + "mobileSomethingWentWrong": "Κάτι πήγε στραβά.", + "mobileSystemColors": "Χρώματα συστήματος", + "mobileTheme": "Εμφάνιση", + "mobileToolsTab": "Εργαλεία", + "mobileWaitingForOpponentToJoin": "Αναμονή για αντίπαλο...", + "mobileWatchTab": "Δείτε", "activityActivity": "Δραστηριότητα", "activityHostedALiveStream": "Μεταδίδει ζωντανά", "activityRankedInSwissTournament": "Κατατάχθηκε #{param1} στο {param2}", @@ -15,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Έπαιξε {count} κίνηση} other{Έπαιξε {count} κινήσεις}}", "activityInNbCorrespondenceGames": "{count, plural, =1{σε {count} παρτίδα αλληλογραφίας} other{σε {count} παρτίδες αλληλογραφίας}}", "activityCompletedNbGames": "{count, plural, =1{Ολοκλήρωσε {count} παρτίδα αλληλογραφίας} other{Ολοκλήρωσε {count} παρτίδες αλληλογραφίας}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Ολοκλήρωσε {count} παρτίδα αλληλογραφίας {param2}} other{Ολοκλήρωσε {count} παρτίδες αλληλογραφίας {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{Άρχισε να ακολουθεί {count} παίκτη} other{Άρχισε να ακολουθεί {count} παίκτες}}", "activityGainedNbFollowers": "{count, plural, =1{Απέκτησε {count} νέο ακόλουθο} other{Απέκτησε {count} νέους ακόλουθους}}", "activityHostedNbSimuls": "{count, plural, =1{Φιλοξένησε {count} σιμουλτανέ} other{Φιλοξένησε {count} σιμουλτανέ}}", @@ -25,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Αγωνίστηκε σε {count} ελβετικό τουρνουά} other{Αγωνίστηκε σε {count} ελβετικά τουρνουά}}", "activityJoinedNbTeams": "{count, plural, =1{Έγινε μέλος {count} ομάδας} other{Έγινε μέλος {count} ομαδών}}", "broadcastBroadcasts": "Αναμεταδόσεις", + "broadcastMyBroadcasts": "Οι αναμεταδόσεις μου", "broadcastLiveBroadcasts": "Αναμεταδόσεις ζωντανών τουρνούα", + "broadcastBroadcastCalendar": "Ημερολόγιο αναμεταδόσεων", + "broadcastNewBroadcast": "Νέα ζωντανή αναμετάδοση", + "broadcastSubscribedBroadcasts": "Εγγεγραμμένες μεταδώσεις", + "broadcastAboutBroadcasts": "Σχετικά με εκπομπές", + "broadcastHowToUseLichessBroadcasts": "Πώς να χρησιμοποιήσετε τις εκπομπές Lichess.", + "broadcastTheNewRoundHelp": "Ο νέος γύρος θα έχει τα ίδια μέλη και τους ίδιους συνεισφέροντες όπως ο προηγούμενος.", + "broadcastAddRound": "Προσθήκη γύρου", + "broadcastOngoing": "Σε εξέλιξη", + "broadcastUpcoming": "Προσεχή", + "broadcastCompleted": "Ολοκληρώθηκε", + "broadcastCompletedHelp": "Το Lichess ανιχνεύει ολοκλήρωση γύρων, αλλά μπορεί να κάνει λάθος. Χρησιμοποιήστε αυτό για να το ρυθμίσετε χειροκίνητα.", + "broadcastRoundName": "Όνομα γύρου", + "broadcastRoundNumber": "Αριθμός γύρου", + "broadcastTournamentName": "Όνομα τουρνουά", + "broadcastTournamentDescription": "Σύντομη περιγραφή τουρνουά", + "broadcastFullDescription": "Πλήρης περιγραφή γεγονότος", + "broadcastFullDescriptionHelp": "Προαιρετική αναλυτική περιγραφή της αναμετάδοσης. Η μορφή {param1} είναι διαθέσιμη. Το μήκος πρέπει μικρότερο από {param2} χαρακτήρες.", + "broadcastSourceSingleUrl": "Πηγαίο URL για PGN", + "broadcastSourceUrlHelp": "URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.", + "broadcastStartDateHelp": "Προαιρετικό, εάν γνωρίζετε πότε αρχίζει η εκδήλωση", + "broadcastCurrentGameUrl": "Διεύθυνση URL αυτού του παιχνιδιού", + "broadcastDownloadAllRounds": "Λήψη όλων των γύρων", + "broadcastResetRound": "Επαναφορά αυτού του γύρου", + "broadcastDeleteRound": "Διαγραφή αυτού του γύρου", + "broadcastDefinitivelyDeleteRound": "Σίγουρα διαγράψτε τον γύρο και όλα τα παιχνίδια του.", + "broadcastDeleteAllGamesOfThisRound": "Διαγράψτε όλα τα παιχνίδια αυτού του γύρου. Η πηγή μετάδοσης θα πρέπει να είναι ενεργή για να τα ξαναδημιουργήσετε.", + "broadcastEditRoundStudy": "Επεξεργασία μελέτης γύρου", + "broadcastDeleteTournament": "Διαγραφή αυτού του τουρνουά", + "broadcastDefinitivelyDeleteTournament": "Σίγουρα διαγράψτε ολόκληρο τον διαγωνισμό, όλους τους γύρους του και όλα τα παιχνίδια του.", + "broadcastFideFederations": "Ομοσπονδίες FIDE", + "broadcastFidePlayers": "Παίκτες FIDE", + "broadcastFidePlayerNotFound": "Δε βρέθηκε παίκτης FIDE", + "broadcastFideProfile": "Προφίλ FIDE", + "broadcastFederation": "Ομοσπονδία", + "broadcastAgeThisYear": "Φετινή ηλικία", + "broadcastUnrated": "Μη βαθμολογημένο", + "broadcastRecentTournaments": "Πρόσφατα τουρνουά", + "broadcastOpenLichess": "Άνοιγμα στο Lichess", + "broadcastTeams": "Ομάδες", + "broadcastBoards": "Σκακιέρες", + "broadcastOverview": "Επισκόπηση", + "broadcastUploadImage": "Ανεβάστε εικόνα τουρνουά", + "broadcastBoardsCanBeLoaded": "Οι σκακιέρες μπορούν να φορτωθούν απο μια πηγή ή μέσω του {param}", + "broadcastStartsAfter": "Ξεκινάει μετά από {param}", + "broadcastStartVerySoon": "Η αναμετάδοση θα ξεκινήσει πολύ σύντομα.", + "broadcastNotYetStarted": "Η αναμετάδοση δεν έχει ξεκινήσει ακόμα.", + "broadcastOfficialWebsite": "Επίσημη ιστοσελίδα", + "broadcastStandings": "Κατάταξη", + "broadcastOfficialStandings": "Επίσημη Κατάταξη", + "broadcastIframeHelp": "Περισσότερες επιλογές στη {param}", + "broadcastWebmastersPage": "σελίδα για webmasters", + "broadcastPgnSourceHelp": "Μια δημόσια πηγή PGN πολύ λειτουργεί σε πραγματικό χρόνο για αυτόν τον γύρο. Προσφέρουμε επίσης το {param} για γρηγορότερο και αποτελεσματικότερο συγχρονισμό.", + "broadcastEmbedThisBroadcast": "Ενσωμάτωση αυτήν την αναμετάδοση στην ιστοσελίδα σας", + "broadcastEmbedThisRound": "Ενσωματώστε τον {param} στην ιστοσελίδα σας", + "broadcastRatingDiff": "Διαφορά βαθμολογίας", + "broadcastGamesThisTournament": "Παρτίδες σε αυτό το τουρνουά", + "broadcastScore": "Βαθμολογία", + "broadcastAllTeams": "Όλες οι ομάδες", + "broadcastTournamentLocation": "Τοποθεσία Τουρνουά", + "broadcastTimezone": "Ζώνη ώρας", + "broadcastOptionalDetails": "Προαιρετικές λεπτομέρειες", + "broadcastPastBroadcasts": "Προηγούμενες αναμετάδοσεις", + "broadcastAllBroadcastsByMonth": "Προβολή όλων των αναμεταδόσεων ανά μήνα", + "broadcastNbBroadcasts": "{count, plural, =1{{count} αναμετάδοση} other{{count} αναμεταδόσεις}}", "challengeChallengesX": "Προκλήσεις: {param1}", "challengeChallengeToPlay": "Προκαλέστε σε παιχνίδι", "challengeChallengeDeclined": "Η πρόκληση απορρίφθηκε", @@ -84,7 +189,7 @@ "perfStatNow": "τώρα", "preferencesPreferences": "Προτιμήσεις", "preferencesDisplay": "Εμφάνιση", - "preferencesPrivacy": "Απόρρητο", + "preferencesPrivacy": "Ιδιωτικότητα", "preferencesNotifications": "Ειδοποιήσεις", "preferencesPieceAnimation": "Κίνηση πιονιών", "preferencesMaterialDifference": "Διαφορά υλικού", @@ -121,7 +226,7 @@ "preferencesClaimDrawOnThreefoldRepetitionAutomatically": "Διεκδικήστε ισοπαλία αυτόματα σε τριπλή επανάληψη", "preferencesWhenTimeRemainingLessThanThirtySeconds": "Όταν απομένουν < 30 δευτερόλεπτα", "preferencesMoveConfirmation": "Επιβεβαίωση κίνησης", - "preferencesExplainCanThenBeTemporarilyDisabled": "Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού του ταμπλό", + "preferencesExplainCanThenBeTemporarilyDisabled": "Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού της σκακιέρας", "preferencesInCorrespondenceGames": "Στις παρτίδες δι' αλληλογραφίας", "preferencesCorrespondenceAndUnlimited": "Δι' αλληλογραφίας και απεριορίστου χρόνου", "preferencesConfirmResignationAndDrawOffers": "Επιβεβαίωση παραίτησης και προσφοράς ισοπαλίας", @@ -148,6 +253,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Συσκευή", "preferencesBellNotificationSound": "Ειδοποίηση με ήχο από καμπανάκι", + "preferencesBlindfold": "Τυφλό", "puzzlePuzzles": "Γρίφοι", "puzzlePuzzleThemes": "Θέματα γρίφων", "puzzleRecommended": "Προτεινόμενα", @@ -161,6 +267,8 @@ "puzzleSpecialMoves": "Ειδικές κινήσεις", "puzzleDidYouLikeThisPuzzle": "Σας άρεσε αυτός ο γρίφος;", "puzzleVoteToLoadNextOne": "Ψηφίστε για να προχωρήσετε στο επόμενο!", + "puzzleUpVote": "Μου άρεσε ο γρίφος", + "puzzleDownVote": "Δε μου άρεσε ο γρίφος", "puzzleYourPuzzleRatingWillNotChange": "Οι βαθμοί αξιολόγησής σας δε θα αλλάξουν. Αυτοί οι βαθμοί χρησιμεύουν στην επιλογή γρίφων για το επίπεδό σας και όχι στον ανταγωνισμό.", "puzzleFindTheBestMoveForWhite": "Βρείτε την καλύτερη κίνηση για τα λευκά.", "puzzleFindTheBestMoveForBlack": "Βρείτε την καλύτερη κίνηση για τα μαύρα.", @@ -174,6 +282,11 @@ "puzzleKeepGoing": "Συνεχίστε…", "puzzlePuzzleSuccess": "Επιτυχία!", "puzzlePuzzleComplete": "Ο γρίφος ολοκληρώθηκε!", + "puzzleByOpenings": "Ανά άνοιγμα", + "puzzlePuzzlesByOpenings": "Γρίφοι ανά άνοιγμα", + "puzzleOpeningsYouPlayedTheMost": "Ανοίγματα που παίξατε πιο συχνά σε βαθμολογημένες παρτίδες σκάκι", + "puzzleUseFindInPage": "Πατήστε «Εύρεση στη σελίδα» στο μενού του προγράμματος περιήγησης, για να βρείτε το αγαπημένο σας άνοιγμα!", + "puzzleUseCtrlF": "Πατήστε Ctrl+f για να βρείτε το αγαπημένο σας άνοιγμα!", "puzzleNotTheMove": "Δεν είναι αυτή η κίνηση!", "puzzleTrySomethingElse": "Δοκιμάστε κάτι άλλο.", "puzzleRatingX": "Βαθμολογία: {param}", @@ -336,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Κομμάτι που επιτίθεται ή αμύνεται ένα τετράγωνο, πίσω από εχθρικό κομμάτι.", "puzzleThemeZugzwang": "Τσούγκτσβανγκ", "puzzleThemeZugzwangDescription": "Ο αντίπαλος είναι περιορισμένος στις κινήσεις που μπορεί να κάνει και οποιαδήποτε κίνηση επιλέξει επιδεινώνει την θέση του.", - "puzzleThemeHealthyMix": "Προτεινόμενο μίγμα", - "puzzleThemeHealthyMixDescription": "Λίγο απ' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.", + "puzzleThemeMix": "Προτεινόμενο μίγμα", + "puzzleThemeMixDescription": "Λίγο απ' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.", "puzzleThemePlayerGames": "Παιχνίδια παίκτη", "puzzleThemePlayerGamesDescription": "Αναζητήστε γρίφους που δημιουργήθηκαν από παιχνίδια είτε δικά σας είτε άλλων παικτών.", "puzzleThemePuzzleDownloadInformation": "Αυτοί οι γρίφοι είναι δημόσιοι και μπορείτε να τους κατεβάσετε εδώ {param}.", @@ -416,6 +529,8 @@ "promoteVariation": "Προώθηση βαριάντας", "makeMainLine": "Δημιουργία κύριας γραμμής", "deleteFromHere": "Διαγραφή από εδώ", + "collapseVariations": "Σύμπτυξη παραλλαγών", + "expandVariations": "Εμφάνιση βαριάντων", "forceVariation": "Θέσε σε βαριάντα", "copyVariationPgn": "Αντιγραφή PGN αρχείου κινήσεων", "move": "Κίνηση", @@ -434,7 +549,7 @@ "averageRatingX": "Μέση βαθμολογία: {param}", "recentGames": "Πρόσφατα παιχνίδια", "topGames": "Κορυφαίες παρτίδες", - "masterDbExplanation": "Δύο εκατομμύρια OTB παρτίδες {param1} + παικτών με αξιολόγηση FIDE από {param2} έως {param3}", + "masterDbExplanation": "Παρτίδες OTB {param1} + παικτών με αξιολόγηση FIDE, από {param2} έως {param3}", "dtzWithRounding": "DTZ50'' με στρογγυλοποίηση, βάσει του αριθμού των κινήσεων μέχρι την επόμενη κίνηση πιονιού ή την επόμενη αλλαγή", "noGameFound": "Δεν βρέθηκε παρτίδα", "maxDepthReached": "Έχετε φτάσει το μέγιστο βάθος!", @@ -456,7 +571,6 @@ "replayMode": "Επανάληψη", "realtimeReplay": "Σε πραγματικό χρόνο", "byCPL": "Με CPL", - "openStudy": "Άνοιγμα μελέτης", "enable": "Ενεργοποίηση", "bestMoveArrow": "Βέλτιστη κίνηση βέλους", "showVariationArrows": "Εμφάνισε βελάκια παραλλαγών", @@ -466,7 +580,6 @@ "memory": "Μνήμη", "infiniteAnalysis": "Άπειρη ανάλυση", "removesTheDepthLimit": "Καταργεί το όριο βάθους και κρατά τον υπολογιστή σας ζεστό", - "engineManager": "Διαχειριστής μηχανής", "blunder": "Σοβαρό σφάλμα", "mistake": "Λάθος", "inaccuracy": "Ανακρίβεια", @@ -492,6 +605,7 @@ "latestForumPosts": "Τελευταίες δημοσιεύσεις στο φόρουμ", "players": "Παίκτες", "friends": "Φίλοι", + "otherPlayers": "άλλους παίκτες", "discussions": "Συζητήσεις", "today": "Σήμερα", "yesterday": "Χθες", @@ -547,6 +661,7 @@ "rank": "Κατάταξη", "rankX": "Κατάταξη: {param}", "gamesPlayed": "Παιγμένα παιχνίδια", + "ok": "ΟΚ", "cancel": "Ακύρωση", "whiteTimeOut": "Τέλος χρόνου για τα λευκά", "blackTimeOut": "Τέλος χρόνου για τα μαύρα", @@ -663,7 +778,6 @@ "block": "Αποκλείστε", "blocked": "Αποκλεισμένος", "unblock": "Κατάργηση απόκλεισης", - "followsYou": "Σας ακολουθεί", "xStartedFollowingY": "Ο {param1} άρχισε να ακολουθεί τον {param2}", "more": "Περισσότερα", "memberSince": "Μέλος από τις", @@ -722,6 +836,7 @@ "ifNoneLeaveEmpty": "Αν δεν υπάρχει, αφήστε κενό", "profile": "Προφίλ", "editProfile": "Επεξεργασία προφίλ", + "realName": "Πραγματικό όνομα", "setFlair": "Ορίστε τη νιφάδα σας", "flair": "Νιφάδα", "youCanHideFlair": "Υπάρχει μια ρύθμιση για να κρύψει όλες τις νιφάδες χρήστη σε ολόκληρη την ιστοσελίδα.", @@ -748,6 +863,8 @@ "descPrivateHelp": "Κείμενο που θα δουν μόνο τα μέλη της ομάδας. Εάν οριστεί, αντικαθιστά τη δημόσια περιγραφή για τα μέλη της ομάδας.", "no": "Όχι", "yes": "Ναι", + "website": "Ιστοσελίδα", + "mobile": "Εφαρμογή Κινητού", "help": "Βοήθεια:", "createANewTopic": "Δημιουργήστε καινούριο θέμα", "topics": "Θέματα", @@ -766,7 +883,9 @@ "cheat": "Απάτη", "troll": "Εμπαιγμός", "other": "Άλλο", - "reportDescriptionHelp": "Κάντε επικόλληση τον σύνδεσμο για το παιχνίδι(α) και εξηγήστε τι είναι παράξενο στη συμπεριφορά του χρήστη. Μην πείτε απλά «επειδή κλέβει», πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα. Η αναφορά σας θα επεξεργαστεί πιο γρήγορα αν είναι γραμμένη στα αγγλικά.", + "reportCheatBoostHelp": "Επικολλήστε τους συνδέσμους με τα παιχνίδια και εξηγήστε μας γιατί θεωρείτε ότι η συμπεριφορά του χρήστη είναι παράξενη σε αυτά. Μη λέτε απλώς ότι «κλέβει» (\"they cheat\"), αλλά πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα.", + "reportUsernameHelp": "Εξηγήστε μας γιατί είναι προσβλητικό το όνομα αυτού του χρήστη. Μη λέτε απλώς ότι \"είναι προσβλητικό/ακατάλληλο\" (\"it's offensive/inappropriate\"), αλλά πείτε μας πώς καταλήξατε σε αυτό το συμπέρασμα, ειδικά αν πρόκειται για προσβολή η οποία δεν είναι ιδιαίτερα εμφανής: για παράδειγμα αν δεν είναι στα αγγλικά, είναι σε κάποια αργκό ή κάνει κάποια προσβλητική ιστορική/πολιτιστική αναφορά.", + "reportProcessedFasterInEnglish": "Η αναφορά σας θα επεξεργαστεί γρηγορότερα αν είναι γραμμένη στα αγγλικά.", "error_provideOneCheatedGameLink": "Καταχωρίστε τουλάχιστον έναν σύνδεσμο σε ένα παιχνίδι εξαπάτησης.", "by": "από τον {param}", "importedByX": "Εισήχθη από τον χρήστη {param}", @@ -882,7 +1001,7 @@ "simulAddExtraTimePerPlayer": "Προσθήκη χρόνου στο ρολόι κάθε παίκτη που συνδέεται στο σιμουλτανέ.", "simulHostExtraTimePerPlayer": "Προσθήκη επιπλέον χρόνου ανά παίκτη", "lichessTournaments": "Τουρνουά στο Lichess", - "tournamentFAQ": "Τεκμηρίωση τουρνουά τύπου αρένας", + "tournamentFAQ": "Τεκμηρίωση για τουρνουά τύπου αρένας", "timeBeforeTournamentStarts": "Χρόνος προτού ξεκινήσει το τουρνουά", "averageCentipawnLoss": "Μέση απώλεια εκατοστοπιονιού", "accuracy": "Ακρίβεια", @@ -933,7 +1052,7 @@ "downloadRaw": "Λήψη ακατέργαστο", "downloadImported": "Λήψη εισαγόμενου", "crosstable": "Αποτελέσματα", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Μπορείτε επίσης να κινηθείτε πάνω στην σκακιέρα για να πάτε στο παιχνίδι.", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Μπορείτε επίσης να κινήσετε πάνω στην σκακιέρα για να μετακινηθείτε στο παιχνίδι.", "scrollOverComputerVariationsToPreviewThem": "Μετακινήστε το ποντίκι σας πάνω στις βαριάντες του υπολογιστή για την προεπισκόπησή τους.", "analysisShapesHowTo": "Πατήστε Shift + κλικ ή δεξί κλικ για να σχεδιάσετε κύκλους και βέλη στην σκακιέρα.", "letOtherPlayersMessageYou": "Επιτρέψτε άλλους παίκτες να σας στέλνουν μηνύματα", @@ -1164,6 +1283,7 @@ "showMeEverything": "Εμφάνιση όλων", "lichessPatronInfo": "Το Lichess είναι ένα φιλανθρωπικό και εντελώς ελεύθερο λογισμικό ανοιχτού κώδικα.\nΌλα τα εξοδα λειτουργίας, ανάπτυξης και περιεχομένου καλύπτοναι αποκλειστικά από δωρεές χρηστών.", "nothingToSeeHere": "Τίποτα για να δείτε εδώ αυτή τη στιγμή.", + "stats": "Στατιστικά", "opponentLeftCounter": "{count, plural, =1{Ο αντίπαλός σας έφυγε από το παιχνίδι. Διεκδίκηση νίκης σε {count} δευτερόλεπτο.} other{Ο αντίπαλος έφυγε από το παιχνίδι. Διεκδίκηση νίκης σε {count} δευτερόλεπτα.}}", "mateInXHalfMoves": "{count, plural, =1{Ματ σε {count} μισή κίνηση} other{Ματ σε {count} μισές κινήσεις}}", "nbBlunders": "{count, plural, =1{{count} σοβαρό σφάλμα} other{{count} σοβαρά σφάλματα}}", @@ -1260,6 +1380,177 @@ "stormXRuns": "{count, plural, =1{1 γύρος} other{{count} γύροι}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Έπαιξε έναν γύρο {param2}} other{Έπαιξε {count} γύρους {param2}}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Ιδιωτικό", + "studyMyStudies": "Οι μελέτες μου", + "studyStudiesIContributeTo": "Μελέτες που συνεισφέρω", + "studyMyPublicStudies": "Οι δημόσιες μελέτες μου", + "studyMyPrivateStudies": "Οι ιδιωτικές μελέτες μου", + "studyMyFavoriteStudies": "Οι αγαπημένες μελέτες μου", + "studyWhatAreStudies": "Τι είναι οι μελέτες;", + "studyAllStudies": "Όλες οι μελέτες", + "studyStudiesCreatedByX": "Μελέτες που δημιουργήθηκαν από τον/την {param}", + "studyNoneYet": "Τίποτα ακόμη εδώ.", + "studyHot": "Δημοφιλείς (hot)", + "studyDateAddedNewest": "Ημερομηνία προσθήκης (νεότερες)", + "studyDateAddedOldest": "Ημερομηνία προσθήκης (παλαιότερες)", + "studyRecentlyUpdated": "Πρόσφατα ενημερωμένες", + "studyMostPopular": "Οι πιο δημοφιλείς", + "studyAlphabetical": "Αλφαβητικά", + "studyAddNewChapter": "Προσθήκη νέου κεφαλαίου", + "studyAddMembers": "Προσθήκη μελών", + "studyInviteToTheStudy": "Προσκάλεσε στην μελέτη", + "studyPleaseOnlyInvitePeopleYouKnow": "Παρακαλώ, προσκαλέστε μόνο άτομα που γνωρίζετε και που θέλουν να συμμετέχουν ενεργά σε αυτήν την μελέτη.", + "studySearchByUsername": "Αναζήτηση με όνομα χρήστη", + "studySpectator": "Θεατής", + "studyContributor": "Συνεισφέρων", + "studyKick": "Διώξε", + "studyLeaveTheStudy": "Αποχώρησε από αυτήν την μελέτη", + "studyYouAreNowAContributor": "Μπορείτε τώρα να συνεισφέρετε στην μελέτη", + "studyYouAreNowASpectator": "Είστε πλέον θεατής", + "studyPgnTags": "PGN ετικέτες", + "studyLike": "Μου αρέσει", + "studyUnlike": "Δε μου αρέσει", + "studyNewTag": "Νέα ετικέτα", + "studyCommentThisPosition": "Σχολίασε την υπάρχουσα θέση", + "studyCommentThisMove": "Σχολίασε αυτήν την κίνηση", + "studyAnnotateWithGlyphs": "Σχολιασμός με σύμβολα", + "studyTheChapterIsTooShortToBeAnalysed": "Το κεφάλαιο είναι πολύ μικρό για να αναλυθεί.", + "studyOnlyContributorsCanRequestAnalysis": "Μόνο αυτοί που συνεισφέρουν στην σπουδή μπορούν να ζητήσουν ανάλυση από υπολογιστή.", + "studyGetAFullComputerAnalysis": "Αίτηση πλήρης ανάλυσης της κύριας γραμμής παρτίδας από μηχανή του σέρβερ.", + "studyMakeSureTheChapterIsComplete": "Σιγουρευτείτε ότι το κεφάλαιο είναι ολοκληρωμένο. Μπορείτε να ζητήσετε ανάλυση μόνο μια φορά.", + "studyAllSyncMembersRemainOnTheSamePosition": "Όλα τα συγχρονισμένα μέλη παραμένουν στην ίδια θέση", + "studyShareChanges": "Διαμοιρασμός στους θεατές των αλλαγών και αποθήκευση τους στο σέρβερ", + "studyPlaying": "Παίζονται", + "studyShowEvalBar": "Μπάρες αξιολόγησης", + "studyFirst": "Πρώτη", + "studyPrevious": "Προηγούμενη", + "studyNext": "Επόμενη", + "studyLast": "Τελευταία", "studyShareAndExport": "Διαμοιρασμός & εξαγωγή", - "studyStart": "Δημιουργία" + "studyCloneStudy": "Κλωνοποίησε", + "studyStudyPgn": "PGN της μελέτης", + "studyDownloadAllGames": "Λήψη όλων των παιχνιδιών", + "studyChapterPgn": "PGN του κεφαλαίου", + "studyCopyChapterPgn": "Αντιγραφή PGN", + "studyDownloadGame": "Λήψη παιχνιδιού", + "studyStudyUrl": "URL μελέτης", + "studyCurrentChapterUrl": "Τρέχον κεφάλαιο URL", + "studyYouCanPasteThisInTheForumToEmbed": "Επικολλήστε το παρόν για ενσωμάτωση στο φόρουμ", + "studyStartAtInitialPosition": "Ξεκινάει από αρχική θέση", + "studyStartAtX": "Ξεκινάει με {param}", + "studyEmbedInYourWebsite": "Ενσωματώστε στην ιστοσελίδα σας ή το μπλογκ σας", + "studyReadMoreAboutEmbedding": "Διαβάστε περισσότερα για την ενσωμάτωση", + "studyOnlyPublicStudiesCanBeEmbedded": "Μόνο δημόσιες μελέτες μπορούν να ενσωματωθούν!", + "studyOpen": "Άνοιξε", + "studyXBroughtToYouByY": "{param1}, δημιουργήθηκε από {param2}", + "studyStudyNotFound": "Η μελέτη δεν βρέθηκε", + "studyEditChapter": "Επεξεργάσου το κεφάλαιο", + "studyNewChapter": "Νέο κεφάλαιο", + "studyImportFromChapterX": "Εισαγωγή από {param}", + "studyOrientation": "Προσανατολισμός", + "studyAnalysisMode": "Τύπος ανάλυσης", + "studyPinnedChapterComment": "Καρφιτσωμένο σχόλιο κεφαλαίου", + "studySaveChapter": "Αποθήκευση κεφαλαίου", + "studyClearAnnotations": "Διαγραφή σχολιασμών", + "studyClearVariations": "Εκκαθάριση βαριάντων", + "studyDeleteChapter": "Διαγραφή κεφαλαίου", + "studyDeleteThisChapter": "Διαγραφή κεφαλαίου; Μη αναιρέσιμη ενέργεια!", + "studyClearAllCommentsInThisChapter": "Καθαρισμός όλων των σχολίων, συμβόλων και σχεδίων στο τρέχον κεφάλαιο;", + "studyRightUnderTheBoard": "Κάτω από την σκακιέρα", + "studyNoPinnedComment": "Καμία", + "studyNormalAnalysis": "Απλή ανάλυση", + "studyHideNextMoves": "Απόκρυψη επόμενων κινήσεων", + "studyInteractiveLesson": "Διαδραστικό μάθημα", + "studyChapterX": "Κεφάλαιο {param}", + "studyEmpty": "Κενή", + "studyStartFromInitialPosition": "Έναρξη από τρέχουσα θέση", + "studyEditor": "Επεξεργαστής", + "studyStartFromCustomPosition": "Έναρξη από τρέχουσα θέση", + "studyLoadAGameByUrl": "Φόρτωση παρτίδας με URL", + "studyLoadAPositionFromFen": "Φόρτωση θέσης από FEN", + "studyLoadAGameFromPgn": "Φόρτωσε μια παρτίδα από PGN", + "studyAutomatic": "Αυτόματο", + "studyUrlOfTheGame": "URL παρτίδων, ένα ανά γραμμή", + "studyLoadAGameFromXOrY": "Φόρτωση παρτίδων από {param1} ή {param2}", + "studyCreateChapter": "Δημιουργία κεφαλαίου", + "studyCreateStudy": "Δημιουργία μελέτης", + "studyEditStudy": "Επεξεργασία μελέτης", + "studyVisibility": "Ορατότητα", + "studyPublic": "Δημόσια", + "studyUnlisted": "Ακαταχώρητη", + "studyInviteOnly": "Με πρόσκληση", + "studyAllowCloning": "Επέτρεψε αντιγραφή", + "studyNobody": "Κανένας", + "studyOnlyMe": "Μόνο εγώ", + "studyContributors": "Συνεισφέροντες", + "studyMembers": "Μέλη", + "studyEveryone": "Οποιοσδήποτε", + "studyEnableSync": "Ενεργοποίηση συγχρονισμού", + "studyYesKeepEveryoneOnTheSamePosition": "Ναι: όλοι βλέπουν την ίδια θέση", + "studyNoLetPeopleBrowseFreely": "Όχι: ελεύθερη επιλογή θέσης", + "studyPinnedStudyComment": "Καρφιτσωμένο σχόλιο μελέτης", + "studyStart": "Δημιουργία", + "studySave": "Αποθήκευση", + "studyClearChat": "Εκκαθάριση συνομιλίας", + "studyDeleteTheStudyChatHistory": "Διαγραφή συνομιλίας μελέτης; Μη αναιρέσιμη ενέργεια!", + "studyDeleteStudy": "Διαγραφή μελέτης", + "studyConfirmDeleteStudy": "Να διαγραφεί όλη η μελέτη; Η ενέργεια αυτή δεν μπορεί να αναιρεθεί! Πληκτρολογήστε το όνομα της μελέτης για επιβεβαίωση: {param}", + "studyWhereDoYouWantToStudyThat": "Που θέλετε να δημιουργήσετε την μελέτη;", + "studyGoodMove": "Καλή κίνηση", + "studyMistake": "Λάθος", + "studyBrilliantMove": "Εξαιρετική κίνηση", + "studyBlunder": "Σοβαρό λάθος", + "studyInterestingMove": "Ενδιαφέρουσα κίνηση", + "studyDubiousMove": "Κίνηση αμφίβολης αξίας", + "studyOnlyMove": "Μοναδική κίνηση", + "studyZugzwang": "Τσούγκσβανγκ", + "studyEqualPosition": "Ισόπαλη θέση", + "studyUnclearPosition": "Ασαφής θέση", + "studyWhiteIsSlightlyBetter": "Το λευκά είναι ελαφρώς καλύτερα", + "studyBlackIsSlightlyBetter": "Το μαύρα είναι ελαφρώς καλύτερα", + "studyWhiteIsBetter": "Τα λευκά είναι καλύτερα", + "studyBlackIsBetter": "Τα μαύρα είναι καλύτερα", + "studyWhiteIsWinning": "Τα λευκά κερδίζουν", + "studyBlackIsWinning": "Τα μαύρα κερδίζουν", + "studyNovelty": "Novelty", + "studyDevelopment": "Ανάπτυξη", + "studyInitiative": "Πρωτοβουλία", + "studyAttack": "Επίθεση", + "studyCounterplay": "Αντεπίθεση", + "studyTimeTrouble": "Πίεση χρόνου", + "studyWithCompensation": "Με αντάλλαγμα", + "studyWithTheIdea": "Με ιδέα", + "studyNextChapter": "Επόμενο κεφάλαιο", + "studyPrevChapter": "Προηγούμενο κεφάλαιο", + "studyStudyActions": "Ρυθμίσεις μελέτης", + "studyTopics": "Θέματα", + "studyMyTopics": "Τα θέματά μου", + "studyPopularTopics": "Δημοφιλή θέματα", + "studyManageTopics": "Διαχείριση θεμάτων", + "studyBack": "Πίσω", + "studyPlayAgain": "Παίξτε ξανά", + "studyWhatWouldYouPlay": "Τι θα παίζατε σε αυτή τη θέση;", + "studyYouCompletedThisLesson": "Συγχαρητήρια! Ολοκληρώσατε αυτό το μάθημα.", + "studyNbChapters": "{count, plural, =1{{count} Κεφάλαιο} other{{count} Κεφάλαια}}", + "studyNbGames": "{count, plural, =1{{count} Παρτίδα} other{{count} Παρτίδες}}", + "studyNbMembers": "{count, plural, =1{{count} Μέλος} other{{count} Μέλη}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδα} other{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδες}}", + "timeagoJustNow": "μόλις τώρα", + "timeagoRightNow": "αυτή τη στιγμή", + "timeagoCompleted": "ολοκληρώθηκε", + "timeagoInNbSeconds": "{count, plural, =1{σε {count} δευτερόλεπτο} other{σε {count} δευτερόλεπτα}}", + "timeagoInNbMinutes": "{count, plural, =1{σε {count} λεπτό} other{σε {count} λεπτά}}", + "timeagoInNbHours": "{count, plural, =1{σε {count} ώρα} other{σε {count} ώρες}}", + "timeagoInNbDays": "{count, plural, =1{σε {count} ημέρα} other{σε {count} ημέρες}}", + "timeagoInNbWeeks": "{count, plural, =1{σε {count} εβδομάδα} other{σε {count} εβδομάδες}}", + "timeagoInNbMonths": "{count, plural, =1{σε {count} μήνα} other{σε {count} μήνες}}", + "timeagoInNbYears": "{count, plural, =1{σε {count} έτος} other{{count} έτη}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} λεπτό πριν} other{{count} λεπτά πριν}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ώρα πριν} other{{count} ώρες πριν}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} μέρα πριν} other{{count} ημέρες πριν}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} εβδομάδα πριν} other{{count} εβδομάδες πριν}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} μήνα πριν} other{{count} μήνες πριν}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} χρόνο πριν} other{{count} χρόνια πριν}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{απομένει {count} λεπτό} other{απομένουν {count} λεπτά}}", + "timeagoNbHoursRemaining": "{count, plural, =1{απομένει {count} ώρα} other{απομένουν {count} ώρες}}" } \ No newline at end of file diff --git a/lib/l10n/lila_en_US.arb b/lib/l10n/lila_en_US.arb index 1dcaf2ee9a..b4a9aaf79c 100644 --- a/lib/l10n/lila_en_US.arb +++ b/lib/l10n/lila_en_US.arb @@ -1,46 +1,48 @@ { + "mobileAllGames": "All games", + "mobileAreYouSure": "Are you sure?", + "mobileBlindfoldMode": "Blindfold", + "mobileCancelTakebackOffer": "Cancel takeback offer", + "mobileClearButton": "Clear", + "mobileCorrespondenceClearSavedMove": "Clear saved move", + "mobileCustomGameJoinAGame": "Join a game", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hello, {param}", + "mobileGreetingWithoutName": "Hello", + "mobileHideVariation": "Hide variation", "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Puzzles", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Watch", - "mobileSettingsTab": "Settings", + "mobileLiveStreamers": "Live streamers", "mobileMustBeLoggedIn": "You must be logged in to view this page.", - "mobileSystemColors": "System colors", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "No results", + "mobileNotFollowingAnyUser": "You are not following any user.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", + "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", + "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", + "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", + "mobilePuzzleStreakAbortWarning": "You will lose your current streak, but your score will be saved.", + "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", + "mobilePuzzlesTab": "Puzzles", + "mobileRecentSearches": "Recent searches", "mobileSettingsHapticFeedback": "Haptic feedback", "mobileSettingsImmersiveMode": "Immersive mode", "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", - "mobileNotFollowingAnyUser": "You are not following any user.", - "mobileAllGames": "All games", - "mobileRecentSearches": "Recent searches", - "mobileClearButton": "Clear", - "mobileNoSearchResults": "No results", - "mobileAreYouSure": "Are you sure?", - "mobilePuzzleStreakAbortWarning": "You will lose your current streak, but your score will be saved.", - "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", - "mobileSharePuzzle": "Share this puzzle", - "mobileShareGameURL": "Share game URL", + "mobileSettingsTab": "Settings", "mobileShareGamePGN": "Share PGN", + "mobileShareGameURL": "Share game URL", "mobileSharePositionAsFEN": "Share position as FEN", - "mobileShowVariations": "Show variations", - "mobileHideVariation": "Hide variation", + "mobileSharePuzzle": "Share this puzzle", "mobileShowComments": "Show comments", - "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", - "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", - "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileCancelDrawOffer": "Cancel draw offer", - "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", - "mobileBlindfoldMode": "Blindfold", - "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Join a game", - "mobileCorrespondenceClearSavedMove": "Clear saved move", - "mobileSomethingWentWrong": "Something went wrong.", "mobileShowResult": "Show result", - "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", - "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", - "mobileGreeting": "Hello, {param}", - "mobileGreetingWithoutName": "Hello", + "mobileShowVariations": "Show variations", + "mobileSomethingWentWrong": "Something went wrong.", + "mobileSystemColors": "System colors", + "mobileTheme": "Theme", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", + "mobileWatchTab": "Watch", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", @@ -53,23 +55,99 @@ "activityPlayedNbMoves": "{count, plural, =1{Played {count} move} other{Played {count} moves}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} correspondence game} other{in {count} correspondence games}}", "activityCompletedNbGames": "{count, plural, =1{Completed {count} correspondence game} other{Completed {count} correspondence games}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}", "activityFollowedNbPlayers": "{count, plural, =1{Started following {count} player} other{Started following {count} players}}", "activityGainedNbFollowers": "{count, plural, =1{Gained {count} new follower} other{Gained {count} new followers}}", "activityHostedNbSimuls": "{count, plural, =1{Hosted {count} simultaneous exhibition} other{Hosted {count} simultaneous exhibitions}}", "activityJoinedNbSimuls": "{count, plural, =1{Participated in {count} simultaneous exhibition} other{Participated in {count} simultaneous exhibitions}}", "activityCreatedNbStudies": "{count, plural, =1{Created {count} new study} other{Created {count} new studies}}", - "activityCompetedInNbTournaments": "{count, plural, =1{Competed in {count} tournament} other{Competed in {count} tournaments}}", + "activityCompetedInNbTournaments": "{count, plural, =1{Competed in {count} Arena tournament} other{Competed in {count} Arena tournaments}}", "activityRankedInTournament": "{count, plural, =1{Ranked #{count} (top {param2}%) with {param3} game in {param4}} other{Ranked #{count} (top {param2}%) with {param3} games in {param4}}}", "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competed in {count} Swiss tournament} other{Competed in {count} Swiss tournaments}}", "activityJoinedNbTeams": "{count, plural, =1{Joined {count} team} other{Joined {count} teams}}", "broadcastBroadcasts": "Broadcasts", + "broadcastMyBroadcasts": "My broadcasts", "broadcastLiveBroadcasts": "Live tournament broadcasts", + "broadcastBroadcastCalendar": "Broadcast calendar", + "broadcastNewBroadcast": "New live broadcast", + "broadcastSubscribedBroadcasts": "Subscribed broadcasts", + "broadcastAboutBroadcasts": "About broadcasts", + "broadcastHowToUseLichessBroadcasts": "How to use Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "The new round will have the same members and contributors as the previous one.", + "broadcastAddRound": "Add a round", + "broadcastOngoing": "Ongoing", + "broadcastUpcoming": "Upcoming", + "broadcastCompleted": "Completed", + "broadcastCompletedHelp": "Lichess detects round completion, but can get it wrong. Use this to set it manually.", + "broadcastRoundName": "Round name", + "broadcastRoundNumber": "Round number", + "broadcastTournamentName": "Tournament name", + "broadcastTournamentDescription": "Short tournament description", + "broadcastFullDescription": "Full tournament description", + "broadcastFullDescriptionHelp": "Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.", + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.", + "broadcastSourceGameIds": "Up to 64 Lichess game IDs, separated by spaces.", + "broadcastStartDateTimeZone": "Start date in the tournament local timezone: {param}", + "broadcastStartDateHelp": "Optional, if you know when the event starts", + "broadcastCurrentGameUrl": "Current game URL", + "broadcastDownloadAllRounds": "Download all rounds", + "broadcastResetRound": "Reset this round", + "broadcastDeleteRound": "Delete this round", + "broadcastDefinitivelyDeleteRound": "Definitively delete the round and all its games.", + "broadcastDeleteAllGamesOfThisRound": "Delete all games of this round. The source will need to be active in order to re-create them.", + "broadcastEditRoundStudy": "Edit round study", + "broadcastDeleteTournament": "Delete this tournament", + "broadcastDefinitivelyDeleteTournament": "Definitively delete the entire tournament, all its rounds and all its games.", + "broadcastShowScores": "Show players' scores based on game results", + "broadcastReplacePlayerTags": "Optional: replace player names, ratings and titles", + "broadcastFideFederations": "FIDE federations", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE players", + "broadcastFidePlayerNotFound": "FIDE player not found", + "broadcastFideProfile": "FIDE profile", + "broadcastFederation": "Federation", + "broadcastAgeThisYear": "Age this year", + "broadcastUnrated": "Unrated", + "broadcastRecentTournaments": "Recent tournaments", + "broadcastOpenLichess": "Open in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Boards", + "broadcastOverview": "Overview", + "broadcastSubscribeTitle": "Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.", + "broadcastUploadImage": "Upload tournament image", + "broadcastNoBoardsYet": "No boards yet. These will appear once games are uploaded.", + "broadcastBoardsCanBeLoaded": "Boards can be loaded with a source or via the {param}", + "broadcastStartsAfter": "Starts after {param}", + "broadcastStartVerySoon": "The broadcast will start very soon.", + "broadcastNotYetStarted": "The broadcast has not yet started.", + "broadcastOfficialWebsite": "Official website", + "broadcastStandings": "Standings", + "broadcastOfficialStandings": "Official Standings", + "broadcastIframeHelp": "More options on the {param}", + "broadcastWebmastersPage": "webmasters page", + "broadcastPgnSourceHelp": "A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronization.", + "broadcastEmbedThisBroadcast": "Embed this broadcast in your website", + "broadcastEmbedThisRound": "Embed {param} in your website", + "broadcastRatingDiff": "Rating diff", + "broadcastGamesThisTournament": "Games in this tournament", + "broadcastScore": "Score", + "broadcastAllTeams": "All teams", + "broadcastTournamentFormat": "Tournament format", + "broadcastTournamentLocation": "Tournament Location", + "broadcastTopPlayers": "Top players", + "broadcastTimezone": "Time zone", + "broadcastFideRatingCategory": "FIDE rating category", + "broadcastOptionalDetails": "Optional details", + "broadcastPastBroadcasts": "Past broadcasts", + "broadcastAllBroadcastsByMonth": "View all broadcasts by month", + "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", "challengeChallengesX": "Challenges: {param1}", "challengeChallengeToPlay": "Challenge to a game", - "challengeChallengeDeclined": "Challenge declined", + "challengeChallengeDeclined": "Challenge declined.", "challengeChallengeAccepted": "Challenge accepted!", "challengeChallengeCanceled": "Challenge canceled.", - "challengeRegisterToSendChallenges": "Please register to send challenges.", + "challengeRegisterToSendChallenges": "Please register to send challenges to this user.", "challengeYouCannotChallengeX": "You cannot challenge {param}.", "challengeXDoesNotAcceptChallenges": "{param} does not accept challenges.", "challengeYourXRatingIsTooFarFromY": "Your {param1} rating is too far from {param2}.", @@ -136,7 +214,7 @@ "preferencesZenMode": "Zen mode", "preferencesShowPlayerRatings": "Show player ratings", "preferencesShowFlairs": "Show player flairs", - "preferencesExplainShowPlayerRatings": "This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see.", + "preferencesExplainShowPlayerRatings": "This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see.", "preferencesDisplayBoardResizeHandle": "Show board resize handle", "preferencesOnlyOnInitialPosition": "Only on initial position", "preferencesInGameOnly": "In-game only", @@ -187,7 +265,8 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Device", "preferencesBellNotificationSound": "Bell notification sound", - "puzzlePuzzles": "Chess Puzzles", + "preferencesBlindfold": "Blindfold", + "puzzlePuzzles": "Puzzles", "puzzlePuzzleThemes": "Puzzle Themes", "puzzleRecommended": "Recommended", "puzzlePhases": "Phases", @@ -202,7 +281,7 @@ "puzzleVoteToLoadNextOne": "Vote to load the next one!", "puzzleUpVote": "Upvote puzzle", "puzzleDownVote": "Downvote puzzle", - "puzzleYourPuzzleRatingWillNotChange": "Your puzzle rating will not change. Note that puzzles are not a competition. Ratings help select the best puzzles for your current skill.", + "puzzleYourPuzzleRatingWillNotChange": "Your puzzle rating will not change. Note that puzzles are not a competition. Your rating helps selecting the best puzzles for your current skill.", "puzzleFindTheBestMoveForWhite": "Find the best move for white.", "puzzleFindTheBestMoveForBlack": "Find the best move for black.", "puzzleToGetPersonalizedPuzzles": "To get personalized puzzles:", @@ -241,7 +320,7 @@ "puzzleStrengths": "Strengths", "puzzleHistory": "Puzzle history", "puzzleSolved": "solved", - "puzzleFailed": "failed", + "puzzleFailed": "incorrect", "puzzleStreakDescription": "Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it's game over! But you can skip one move per session.", "puzzleYourStreakX": "Your streak: {param}", "puzzleStreakSkipExplanation": "Skip this move to preserve your streak! Only works once per run.", @@ -251,7 +330,7 @@ "puzzleLookupOfPlayer": "Search puzzles from a player's games", "puzzleFromXGames": "Puzzles from {param}'s games", "puzzleSearchPuzzles": "Search puzzles", - "puzzleFromMyGamesNone": "You have no puzzles in the database, but Lichess still loves you very much.\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!", + "puzzleFromMyGamesNone": "You have no puzzles in the database, but Lichess still loves you very much.\n\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!", "puzzleFromXGamesFound": "{param1} puzzles found in {param2} games", "puzzlePuzzleDashboardDescription": "Train, analyse, improve", "puzzlePercentSolved": "{param} solved", @@ -382,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "A piece attacks or defends a square, through an enemy piece.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "The opponent is limited in the moves they can make, and all moves worsen their position.", - "puzzleThemeHealthyMix": "Healthy mix", - "puzzleThemeHealthyMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", + "puzzleThemeMix": "Healthy mix", + "puzzleThemeMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", "puzzleThemePlayerGames": "Player games", "puzzleThemePlayerGamesDescription": "Lookup puzzles generated from your games, or from another player's games.", "puzzleThemePuzzleDownloadInformation": "These puzzles are in the public domain, and can be downloaded from {param}.", @@ -451,7 +530,7 @@ "analysis": "Analysis board", "depthX": "Depth {param}", "usingServerAnalysis": "Using server analysis", - "loadingEngine": "Loading engine ...", + "loadingEngine": "Loading engine...", "calculatingMoves": "Calculating moves...", "engineFailed": "Error loading engine", "cloudAnalysis": "Cloud analysis", @@ -504,7 +583,6 @@ "replayMode": "Replay mode", "realtimeReplay": "Realtime", "byCPL": "By CPL", - "openStudy": "Open study", "enable": "Enable", "bestMoveArrow": "Best move arrow", "showVariationArrows": "Show variation arrows", @@ -514,7 +592,6 @@ "memory": "Memory", "infiniteAnalysis": "Infinite analysis", "removesTheDepthLimit": "Removes the depth limit, and keeps your computer warm", - "engineManager": "Engine manager", "blunder": "Blunder", "mistake": "Mistake", "inaccuracy": "Inaccuracy", @@ -596,6 +673,7 @@ "rank": "Rank", "rankX": "Rank: {param}", "gamesPlayed": "Games played", + "ok": "OK", "cancel": "Cancel", "whiteTimeOut": "White time out", "blackTimeOut": "Black time out", @@ -610,7 +688,7 @@ "accept": "Accept", "decline": "Decline", "playingRightNow": "Playing right now", - "eventInProgress": "Playing right now", + "eventInProgress": "Playing now", "finished": "Finished", "abortGame": "Abort game", "gameAborted": "Game aborted", @@ -691,7 +769,7 @@ "continueFromHere": "Continue from here", "toStudy": "Study", "importGame": "Import game", - "importGameExplanation": "Paste a game PGN to get a browsable replay,\ncomputer analysis, game chat and shareable URL.", + "importGameExplanation": "Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL.", "importGameCaveat": "Variations will be erased. To keep them, import the PGN via a study.", "importGameDataPrivacyWarning": "This PGN can be accessed by the public. To import a game privately, use a study.", "thisIsAChessCaptcha": "This is a chess CAPTCHA.", @@ -712,7 +790,6 @@ "block": "Block", "blocked": "Blocked", "unblock": "Unblock", - "followsYou": "Follows you", "xStartedFollowingY": "{param1} started following {param2}", "more": "More", "memberSince": "Member since", @@ -776,7 +853,7 @@ "flair": "Flair", "youCanHideFlair": "There is a setting to hide all user flairs across the entire site.", "biography": "Biography", - "countryRegion": "Region or country", + "countryRegion": "Country or region", "thankYou": "Thank you!", "socialMediaLinks": "Social media links", "oneUrlPerLine": "One URL per line.", @@ -818,7 +895,9 @@ "cheat": "Cheat", "troll": "Troll", "other": "Other", - "reportDescriptionHelp": "Paste the link to the game(s) and explain what is wrong about this user behavior. Don't just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.", + "reportCheatBoostHelp": "Paste a link to the game(s) and explain what is wrong with this user's behavior. Don't just say \"they cheat,\" but tell us how you came to this conclusion.", + "reportUsernameHelp": "Explain why this username is offensive. Don't just say \"it's offensive/inappropriate,\" but tell us how you came to this conclusion, especially if the offense is obscure, not in English, in slang, or a historical/cultural reference.", + "reportProcessedFasterInEnglish": "Your report will be processed faster if written in English.", "error_provideOneCheatedGameLink": "Please provide at least one link to a cheated game.", "by": "by {param}", "importedByX": "Imported by {param}", @@ -884,7 +963,7 @@ "error_email_unique": "Email address invalid or already taken", "error_email_different": "This is already your email address", "error_minLength": "Must be at least {param} characters long", - "error_maxLength": "Maximum length is {param}", + "error_maxLength": "Must be at most {param} characters long", "error_min": "Must be at least {param}", "error_max": "Must be at most {param}", "ifRatingIsPlusMinusX": "If rating is ± {param}", @@ -897,7 +976,7 @@ "blackCastlingKingside": "Black O-O", "tpTimeSpentPlaying": "Time spent playing: {param}", "watchGames": "Watch games", - "tpTimeSpentOnTV": "Time on TV: {param}", + "tpTimeSpentOnTV": "Time featured on TV: {param}", "watch": "Watch", "videoLibrary": "Video library", "streamersMenu": "Streamers", @@ -923,14 +1002,14 @@ "aboutSimul": "Simuls involve a single player facing several players at once.", "aboutSimulImage": "Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.", "aboutSimulRealLife": "The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.", - "aboutSimulRules": "When the simul starts, every player starts a game with the host, who gets to play the white pieces. The simul ends when all games are complete.", + "aboutSimulRules": "When the simul starts, every player starts a game with the host. The simul ends when all games are complete.", "aboutSimulSettings": "Simuls are always casual. Rematches, takebacks and adding time are disabled.", "create": "Create", "whenCreateSimul": "When you create a Simul, you get to play several players at once.", "simulVariantsHint": "If you select several variants, each player gets to choose which one to play.", "simulClockHint": "Fischer Clock setup. The more players you take on, the more time you may need.", - "simulAddExtraTime": "You may add extra time to your clock to help cope with the simul.", - "simulHostExtraTime": "Host extra clock time", + "simulAddExtraTime": "You may add extra initial time to your clock to help you cope with the simul.", + "simulHostExtraTime": "Host extra initial clock time", "simulAddExtraTimePerPlayer": "Add initial time to your clock for each player joining the simul.", "simulHostExtraTimePerPlayer": "Host extra clock time per player", "lichessTournaments": "Lichess tournaments", @@ -944,8 +1023,8 @@ "keyCycleSelectedVariation": "Cycle selected variation", "keyShowOrHideComments": "show/hide comments", "keyEnterOrExitVariation": "enter/exit variation", - "keyRequestComputerAnalysis": "Request computer analysis, learn from your mistakes", - "keyNextLearnFromYourMistakes": "Next (learn from your mistakes)", + "keyRequestComputerAnalysis": "Request computer analysis, Learn from your mistakes", + "keyNextLearnFromYourMistakes": "Next (Learn from your mistakes)", "keyNextBlunder": "Next blunder", "keyNextMistake": "Next mistake", "keyNextInaccuracy": "Next inaccuracy", @@ -977,10 +1056,10 @@ "weHaveSentYouAnEmailClickTheLink": "We've sent you an email. Click the link in the email to activate your account.", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders.", "weHaveSentYouAnEmailTo": "We've sent an email to {param}. Click the link in the email to reset your password.", - "byRegisteringYouAgreeToBeBoundByOur": "By registering, you agree to be bound by our {param}.", + "byRegisteringYouAgreeToBeBoundByOur": "By registering, you agree to the {param}.", "readAboutOur": "Read about our {param}.", - "networkLagBetweenYouAndLichess": "Network lag between you and lichess", - "timeToProcessAMoveOnLichessServer": "Time to process a move on lichess server", + "networkLagBetweenYouAndLichess": "Network lag between you and Lichess", + "timeToProcessAMoveOnLichessServer": "Time to process a move on Lichess's server", "downloadAnnotated": "Download annotated", "downloadRaw": "Download raw", "downloadImported": "Download imported", @@ -995,9 +1074,9 @@ "withFriends": "With friends", "withEverybody": "With everybody", "kidMode": "Kid mode", - "kidModeIsEnabled": "Child-mode is enabled.", + "kidModeIsEnabled": "Kid mode is enabled.", "kidModeExplanation": "This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.", - "inKidModeTheLichessLogoGetsIconX": "In kid mode, the lichess logo gets a {param} icon, so you know your kids are safe.", + "inKidModeTheLichessLogoGetsIconX": "In kid mode, the Lichess logo gets a {param} icon, so you know your kids are safe.", "askYourChessTeacherAboutLiftingKidMode": "Your account is managed. Ask your chess teacher about lifting kid mode.", "enableKidMode": "Enable Kid mode", "disableKidMode": "Disable Kid mode", @@ -1005,7 +1084,7 @@ "sessions": "Sessions", "revokeAllSessions": "revoke all sessions", "playChessEverywhere": "Play chess everywhere", - "asFreeAsLichess": "As free as lichess", + "asFreeAsLichess": "As free as Lichess", "builtForTheLoveOfChessNotMoney": "Built for the love of chess, not money", "everybodyGetsAllFeaturesForFree": "Everybody gets all features for free", "zeroAdvertisement": "Zero advertisement", @@ -1114,10 +1193,10 @@ "lifetimeScore": "Lifetime score", "currentMatchScore": "Current match score", "agreementAssistance": "I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).", - "agreementNice": "I agree that I will always be nice to other players.", + "agreementNice": "I agree that I will always be respectful to other players.", "agreementMultipleAccounts": "I agree that I will not create multiple accounts (except for the reasons stated in the {param}).", "agreementPolicy": "I agree that I will follow all Lichess policies.", - "searchOrStartNewDiscussion": "Search or start new discussion", + "searchOrStartNewDiscussion": "Search or start new conversation", "edit": "Edit", "bullet": "Bullet", "blitz": "Blitz", @@ -1158,14 +1237,14 @@ "lostAgainstTOSViolator": "You lost rating points to someone who violated the Lichess TOS", "refundXpointsTimeControlY": "Refund: {param1} {param2} rating points.", "timeAlmostUp": "Time is almost up!", - "clickToRevealEmailAddress": "[Click to reveal email address.]", + "clickToRevealEmailAddress": "[Click to reveal email address]", "download": "Download", "coachManager": "Coach manager", "streamerManager": "Streamer manager", "cancelTournament": "Cancel the tournament", "tournDescription": "Tournament description", "tournDescriptionHelp": "Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)", - "ratedFormHelp": "Games are rated\nand impact players ratings", + "ratedFormHelp": "Games are rated and impact players ratings", "onlyMembersOfTeam": "Only members of team", "noRestriction": "No restriction", "minimumRatedGames": "Minimum rated games", @@ -1216,6 +1295,7 @@ "showMeEverything": "Show me everything", "lichessPatronInfo": "Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.", "nothingToSeeHere": "Nothing to see here at the moment.", + "stats": "Stats", "opponentLeftCounter": "{count, plural, =1{Your opponent left the game. You can claim victory in {count} second.} other{Your opponent left the game. You can claim victory in {count} seconds.}}", "mateInXHalfMoves": "{count, plural, =1{Mate in {count} half-move} other{Mate in {count} half-moves}}", "nbBlunders": "{count, plural, =1{{count} blunder} other{{count} blunders}}", @@ -1312,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 run} other{{count} runs}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Played one run of {param2}} other{Played {count} runs of {param2}}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Private", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies I contribute to", + "studyMyPublicStudies": "My public studies", + "studyMyPrivateStudies": "My private studies", + "studyMyFavoriteStudies": "My favorite studies", + "studyWhatAreStudies": "What are studies?", + "studyAllStudies": "All studies", + "studyStudiesCreatedByX": "Studies created by {param}", + "studyNoneYet": "None yet.", + "studyHot": "Hot", + "studyDateAddedNewest": "Date added (newest)", + "studyDateAddedOldest": "Date added (oldest)", + "studyRecentlyUpdated": "Recently updated", + "studyMostPopular": "Most popular", + "studyAlphabetical": "Alphabetical", + "studyAddNewChapter": "Add a new chapter", + "studyAddMembers": "Add members", + "studyInviteToTheStudy": "Invite to the study", + "studyPleaseOnlyInvitePeopleYouKnow": "Please only invite people who know you, and who actively want to join this study.", + "studySearchByUsername": "Search by username", + "studySpectator": "Spectator", + "studyContributor": "Contributor", + "studyKick": "Kick", + "studyLeaveTheStudy": "Leave the study", + "studyYouAreNowAContributor": "You are now a contributor", + "studyYouAreNowASpectator": "You are now a spectator", + "studyPgnTags": "PGN tags", + "studyLike": "Like", + "studyUnlike": "Unlike", + "studyNewTag": "New tag", + "studyCommentThisPosition": "Comment on this position", + "studyCommentThisMove": "Comment on this move", + "studyAnnotateWithGlyphs": "Annotate with glyphs", + "studyTheChapterIsTooShortToBeAnalysed": "The chapter is too short to be analyzed.", + "studyOnlyContributorsCanRequestAnalysis": "Only the study contributors can request a computer analysis.", + "studyGetAFullComputerAnalysis": "Get a full server-side computer analysis of the mainline.", + "studyMakeSureTheChapterIsComplete": "Make sure the chapter is complete. You can only request analysis once.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC members remain on the same position", + "studyShareChanges": "Share changes with spectators and save them on the server", + "studyPlaying": "Playing", + "studyShowEvalBar": "Evaluation gauge", + "studyFirst": "First", + "studyPrevious": "Previous", + "studyNext": "Next", + "studyLast": "Last", "studyShareAndExport": "Share & export", - "studyStart": "Start" + "studyCloneStudy": "Clone", + "studyStudyPgn": "Study PGN", + "studyDownloadAllGames": "Download all games", + "studyChapterPgn": "Chapter PGN", + "studyCopyChapterPgn": "Copy PGN", + "studyDownloadGame": "Download game", + "studyStudyUrl": "Study URL", + "studyCurrentChapterUrl": "Current chapter URL", + "studyYouCanPasteThisInTheForumToEmbed": "You can paste this in the forum or your Lichess blog to embed", + "studyStartAtInitialPosition": "Start at initial position", + "studyStartAtX": "Start at {param}", + "studyEmbedInYourWebsite": "Embed in your website", + "studyReadMoreAboutEmbedding": "Read more about embedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Only public studies can be embedded!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1}, brought to you by {param2}", + "studyStudyNotFound": "Study not found", + "studyEditChapter": "Edit chapter", + "studyNewChapter": "New chapter", + "studyImportFromChapterX": "Import from {param}", + "studyOrientation": "Orientation", + "studyAnalysisMode": "Analysis mode", + "studyPinnedChapterComment": "Pinned chapter comment", + "studySaveChapter": "Save chapter", + "studyClearAnnotations": "Clear annotations", + "studyClearVariations": "Clear variations", + "studyDeleteChapter": "Delete chapter", + "studyDeleteThisChapter": "Delete this chapter? There is no going back!", + "studyClearAllCommentsInThisChapter": "Clear all comments, glyphs and drawn shapes in this chapter?", + "studyRightUnderTheBoard": "Right under the board", + "studyNoPinnedComment": "None", + "studyNormalAnalysis": "Normal analysis", + "studyHideNextMoves": "Hide next moves", + "studyInteractiveLesson": "Interactive lesson", + "studyChapterX": "Chapter {param}", + "studyEmpty": "Empty", + "studyStartFromInitialPosition": "Start from initial position", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start from custom position", + "studyLoadAGameByUrl": "Load games by URL", + "studyLoadAPositionFromFen": "Load a position from FEN", + "studyLoadAGameFromPgn": "Load games from PGN", + "studyAutomatic": "Automatic", + "studyUrlOfTheGame": "URL of the games, one per line", + "studyLoadAGameFromXOrY": "Load games from {param1} or {param2}", + "studyCreateChapter": "Create chapter", + "studyCreateStudy": "Create study", + "studyEditStudy": "Edit study", + "studyVisibility": "Visibility", + "studyPublic": "Public", + "studyUnlisted": "Unlisted", + "studyInviteOnly": "Invite only", + "studyAllowCloning": "Allow cloning", + "studyNobody": "Nobody", + "studyOnlyMe": "Only me", + "studyContributors": "Contributors", + "studyMembers": "Members", + "studyEveryone": "Everyone", + "studyEnableSync": "Enable sync", + "studyYesKeepEveryoneOnTheSamePosition": "Yes: keep everyone on the same position", + "studyNoLetPeopleBrowseFreely": "No: let people browse freely", + "studyPinnedStudyComment": "Pinned study comment", + "studyStart": "Start", + "studySave": "Save", + "studyClearChat": "Clear chat", + "studyDeleteTheStudyChatHistory": "Delete the study chat history? There is no going back!", + "studyDeleteStudy": "Delete study", + "studyConfirmDeleteStudy": "Delete the entire study? There is no going back! Type the name of the study to confirm: {param}", + "studyWhereDoYouWantToStudyThat": "Where do you want to study that?", + "studyGoodMove": "Good move", + "studyMistake": "Mistake", + "studyBrilliantMove": "Brilliant move", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interesting move", + "studyDubiousMove": "Dubious move", + "studyOnlyMove": "Only move", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Equal position", + "studyUnclearPosition": "Unclear position", + "studyWhiteIsSlightlyBetter": "White is slightly better", + "studyBlackIsSlightlyBetter": "Black is slightly better", + "studyWhiteIsBetter": "White is better", + "studyBlackIsBetter": "Black is better", + "studyWhiteIsWinning": "White is winning", + "studyBlackIsWinning": "Black is winning", + "studyNovelty": "Novelty", + "studyDevelopment": "Development", + "studyInitiative": "Initiative", + "studyAttack": "Attack", + "studyCounterplay": "Counterplay", + "studyTimeTrouble": "Time trouble", + "studyWithCompensation": "With compensation", + "studyWithTheIdea": "With the idea", + "studyNextChapter": "Next chapter", + "studyPrevChapter": "Previous chapter", + "studyStudyActions": "Study actions", + "studyTopics": "Topics", + "studyMyTopics": "My topics", + "studyPopularTopics": "Popular topics", + "studyManageTopics": "Manage topics", + "studyBack": "Back", + "studyPlayAgain": "Play again", + "studyWhatWouldYouPlay": "What would you play in this position?", + "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyPerPage": "{param} per page", + "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", + "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}", + "timeagoJustNow": "just now", + "timeagoRightNow": "right now", + "timeagoCompleted": "completed", + "timeagoInNbSeconds": "{count, plural, =1{in {count} second} other{in {count} seconds}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} minute} other{in {count} minutes}}", + "timeagoInNbHours": "{count, plural, =1{in {count} hour} other{in {count} hours}}", + "timeagoInNbDays": "{count, plural, =1{in {count} day} other{in {count} days}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weeks}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} month} other{in {count} months}}", + "timeagoInNbYears": "{count, plural, =1{in {count} year} other{in {count} years}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minute ago} other{{count} minutes ago}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} hour ago} other{{count} hours ago}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} day ago} other{{count} days ago}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week ago} other{{count} weeks ago}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} month ago} other{{count} months ago}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} year ago} other{{count} years ago}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eo.arb b/lib/l10n/lila_eo.arb index f5e8c19877..49b4fac67a 100644 --- a/lib/l10n/lila_eo.arb +++ b/lib/l10n/lila_eo.arb @@ -1,22 +1,22 @@ { + "mobileAllGames": "Ĉiuj ludoj", + "mobileAreYouSure": "Ĉu vi certas?", + "mobileClearButton": "Malplenigi", + "mobileFeedbackButton": "Prikomentado", "mobileHomeTab": "Hejmo", - "mobilePuzzlesTab": "Puzloj", - "mobileToolsTab": "Iloj", - "mobileWatchTab": "Spekti", - "mobileSettingsTab": "Agordoj", "mobileMustBeLoggedIn": "Vi devas esti ensalutata por spekti ĉi tiun paĝon.", - "mobileSystemColors": "Sistemaj koloroj", - "mobileFeedbackButton": "Prikomentado", + "mobileNoSearchResults": "Neniu rezultoj", + "mobileNotFollowingAnyUser": "Vi ne abonas ĉiun uzanton.", "mobileOkButton": "Bone", + "mobilePlayersMatchingSearchTerm": "Ludantanto kun \"{param}\"", + "mobilePuzzlesTab": "Puzloj", + "mobileRecentSearches": "Lastaj serĉoj", "mobileSettingsHapticFeedback": "Tuŝ-retrokuplado", "mobileSettingsImmersiveMode": "Enakviĝa reĝimo", - "mobileNotFollowingAnyUser": "Vi ne abonas ĉiun uzanton.", - "mobileAllGames": "Ĉiuj ludoj", - "mobileRecentSearches": "Lastaj serĉoj", - "mobileClearButton": "Malplenigi", - "mobilePlayersMatchingSearchTerm": "Ludantanto kun \"{param}\"", - "mobileNoSearchResults": "Neniu rezultoj", - "mobileAreYouSure": "Ĉu vi certas?", + "mobileSettingsTab": "Agordoj", + "mobileSystemColors": "Sistemaj koloroj", + "mobileToolsTab": "Iloj", + "mobileWatchTab": "Spekti", "activityActivity": "Aktiveco", "activityHostedALiveStream": "Gastigis vivan rivereton", "activityRankedInSwissTournament": "Rangita #{param1} en {param2}", @@ -39,7 +39,36 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Konkuris en {count} svisa turniro} other{Konkuris en {count} svisaj turniroj}}", "activityJoinedNbTeams": "{count, plural, =1{Aliĝis {count} teamo} other{Aliĝis {count} teamoj}}", "broadcastBroadcasts": "Elsendoj", + "broadcastMyBroadcasts": "Miaj elsendoj", "broadcastLiveBroadcasts": "Vivaj turniraj elsendoj", + "broadcastNewBroadcast": "Nova viva elsendo", + "broadcastSubscribedBroadcasts": "Abonitaj elsendoj", + "broadcastAboutBroadcasts": "Pri elsendoj", + "broadcastHowToUseLichessBroadcasts": "Kiel uzi Lichess Elsendojn.", + "broadcastTheNewRoundHelp": "La nova raŭndo havos la samajn membrojn kaj kontribuantojn, kiom la antaŭa.", + "broadcastAddRound": "Aldoni raŭndon", + "broadcastOngoing": "Nun funkcianta", + "broadcastUpcoming": "Baldaŭ", + "broadcastCompleted": "Kompletigita", + "broadcastCompletedHelp": "Lichess detektas raŭndan finiĝon baze sur la fontaj ludoj. Uzu ĉi tiun baskuligo, se ne estas fonto.", + "broadcastRoundName": "Raŭndnomo", + "broadcastRoundNumber": "Rondnumero", + "broadcastTournamentName": "Nomo de la turniro", + "broadcastTournamentDescription": "Mallonga turnira priskribo", + "broadcastFullDescription": "Plena eventa priskribo", + "broadcastFullDescriptionHelp": "Laŭvola longa priskribo de la elsendo. {param1} haveblas. Longeco devas esti malpli ol {param2} literoj.", + "broadcastSourceUrlHelp": "URL kiun Lichess kontrolos por akiri PGN ĝisdatigojn. Ĝi devas esti publike alirebla en interreto.", + "broadcastStartDateHelp": "Laŭvola, se vi scias, kiam komenciĝas la evento", + "broadcastCurrentGameUrl": "Nuna luda URL", + "broadcastDownloadAllRounds": "Elŝuti ĉiujn raŭndojn", + "broadcastResetRound": "Restarigi ĉi tiun raŭndon", + "broadcastDeleteRound": "Forigi ĉi tiun raŭndon", + "broadcastDefinitivelyDeleteRound": "Sendube forigi la raŭndon kaj ĉiujn ĝiajn ludojn.", + "broadcastDeleteAllGamesOfThisRound": "Forigi ĉiujn ludojn de ĉi tiu raŭndo. La fonto devos esti aktiva por rekrei ilin.", + "broadcastEditRoundStudy": "Redakti raŭndan studon", + "broadcastDeleteTournament": "Forigi ĉi tiun turniron", + "broadcastDefinitivelyDeleteTournament": "Sendube forigi la tuta turniro, kaj ĝiajn raŭndojn kaj ĉiujn ĝiajn ludojn.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} elsendo} other{{count} elsendoj}}", "challengeChallengesX": "Defioj: {param1}", "challengeChallengeToPlay": "Defii al nova ludo", "challengeChallengeDeclined": "Defio malakceptita", @@ -358,8 +387,8 @@ "puzzleThemeXRayAttackDescription": "Peco atakas aŭ defendas kvadraton, tra malamika peco.", "puzzleThemeZugzwang": "Movdevigo", "puzzleThemeZugzwangDescription": "La opcioj de la kontraŭulo por moviĝi estas limigitaj, kaj ĉiuj movoj plimalbonigas rian pozicion.", - "puzzleThemeHealthyMix": "Sana miksaĵo", - "puzzleThemeHealthyMixDescription": "Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.", + "puzzleThemeMix": "Sana miksaĵo", + "puzzleThemeMixDescription": "Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.", "puzzleThemePlayerGames": "Ludantaj ludoj", "puzzleThemePlayerGamesDescription": "Serĉu enigmojn generitajn de viaj ludoj, aŭ de la ludoj de alia ludanto.", "puzzleThemePuzzleDownloadInformation": "Ĉi tiuj enigmoj estas en la publika domeno, kaj povas esti elŝutitaj de {param}.", @@ -480,7 +509,6 @@ "replayMode": "Reluda reĝimo", "realtimeReplay": "Reala tempo", "byCPL": "Per eraroj", - "openStudy": "Malfermi analizon", "enable": "Ebligi", "bestMoveArrow": "Sago por optimuma movo", "showVariationArrows": "Montri variaĵojn sagojn", @@ -490,7 +518,6 @@ "memory": "Memoro", "infiniteAnalysis": "Senfina analizo", "removesTheDepthLimit": "Forigas la profundlimon, kaj tenas vian komputilon varma", - "engineManager": "Motora administranto", "blunder": "Erarego", "mistake": "Eraro", "inaccuracy": "Erareto", @@ -687,7 +714,6 @@ "block": "Bloki", "blocked": "Blokita", "unblock": "Malbloki", - "followsYou": "Sekvas vin", "xStartedFollowingY": "{param1} eksekvis {param2}", "more": "Pli", "memberSince": "Membro ekde", @@ -791,7 +817,6 @@ "cheat": "Trompo", "troll": "Trolo", "other": "Io alia", - "reportDescriptionHelp": "Inkluzivu la ligilon al la ludo(j) kaj ekspliku tion, kio malbonas pri la konduto de ĉi tiu uzanto.", "error_provideOneCheatedGameLink": "Bonvolu doni almenaŭ unu ligilon al ludo en kiu oni friponis.", "by": "de {param}", "importedByX": "Importita de {param}", @@ -1285,6 +1310,177 @@ "stormXRuns": "{count, plural, =1{1 kuro} other{{count} kuroj}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ludis unu kuron de {param2}} other{Ludis {count} kurojn de {param2}}}", "streamerLichessStreamers": "Lichess filmprezentistoj", + "studyPrivate": "Privata", + "studyMyStudies": "Miaj studoj", + "studyStudiesIContributeTo": "Studoj en kiuj mi kontribuas", + "studyMyPublicStudies": "Miaj publikaj studoj", + "studyMyPrivateStudies": "Miaj privataj studoj", + "studyMyFavoriteStudies": "Miaj preferataj studoj", + "studyWhatAreStudies": "Kio estas la studoj?", + "studyAllStudies": "Ĉiuj studoj", + "studyStudiesCreatedByX": "Studoj kreitaj de {param}", + "studyNoneYet": "Neniu ankoraŭ.", + "studyHot": "Tendenca", + "studyDateAddedNewest": "Dato aldonita (plej novaj)", + "studyDateAddedOldest": "Dato aldonita (plej malnovaj)", + "studyRecentlyUpdated": "Lastatempe ĝisdatigita", + "studyMostPopular": "Plej popularaj", + "studyAlphabetical": "Alfabete", + "studyAddNewChapter": "Aldoni novan ĉapitron", + "studyAddMembers": "Aldoni membrojn", + "studyInviteToTheStudy": "Inviti al la studo", + "studyPleaseOnlyInvitePeopleYouKnow": "Bonvolu inviti nur homojn, kiujn vi konas kaj kiuj aktive volas aliĝi al tiu ĉi studo.", + "studySearchByUsername": "Serĉi laŭ uzantnomo", + "studySpectator": "Spektanto", + "studyContributor": "Kontribuanto", + "studyKick": "Forpuŝi", + "studyLeaveTheStudy": "Forlasi la studon", + "studyYouAreNowAContributor": "Nun vi estas kunlaboranto", + "studyYouAreNowASpectator": "Nun vi estas spektanto", + "studyPgnTags": "PGN etikedoj", + "studyLike": "Ŝati", + "studyUnlike": "Malŝati", + "studyNewTag": "Nova etikedo", + "studyCommentThisPosition": "Komenti tiun posicion", + "studyCommentThisMove": "Komenti tiun movon", + "studyAnnotateWithGlyphs": "Komenti per signobildo", + "studyTheChapterIsTooShortToBeAnalysed": "La ĉapitro estas tro mallonga por esti analizita.", + "studyOnlyContributorsCanRequestAnalysis": "Nur la kontribuantoj de la studo povas peti komputilan analizon.", + "studyGetAFullComputerAnalysis": "Akiru kompletan servilan komputilan analizon de la ĉefa linio.", + "studyMakeSureTheChapterIsComplete": "Certiĝu, ke la ĉapitro estas kompleta. Vi nur povas peti analizon unu foje.", + "studyAllSyncMembersRemainOnTheSamePosition": "Ĉiuj sinkronigitaj membroj restas ĉe la sama pozicio", + "studyShareChanges": "Diskonigi ŝanĝojn al spektantoj kaj konservi tiujn ĉe la servilo", + "studyPlaying": "Ludanta", + "studyShowEvalBar": "Taksaj stangoj", + "studyFirst": "Al la unua", + "studyPrevious": "Antaŭa", + "studyNext": "Sekva", + "studyLast": "Al la lasta", "studyShareAndExport": "Konigi & eksporti", - "studyStart": "Komenci" + "studyCloneStudy": "Kloni", + "studyStudyPgn": "PGN de la studo", + "studyDownloadAllGames": "Elŝuti ĉiujn ludojn", + "studyChapterPgn": "PGN de la ĉapitro", + "studyCopyChapterPgn": "Kopii PGN", + "studyDownloadGame": "Elŝuti ludon", + "studyStudyUrl": "URL de la studo", + "studyCurrentChapterUrl": "URL de tiu ĉi ĉapitro", + "studyYouCanPasteThisInTheForumToEmbed": "Vi povas alglui ĉi tiun en la forumo aŭ via Lichess blogo por enkorpigi", + "studyStartAtInitialPosition": "Starti ekde komenca pozicio", + "studyStartAtX": "Komenci je {param}", + "studyEmbedInYourWebsite": "Enkorpigi en via retejo", + "studyReadMoreAboutEmbedding": "Legi pli pri enkorpigo", + "studyOnlyPublicStudiesCanBeEmbedded": "Nur publikaj studoj eblas enkorpiĝi!", + "studyOpen": "Malfermi", + "studyXBroughtToYouByY": "{param1}, provizia al vi de {param2}", + "studyStudyNotFound": "Studo ne trovita", + "studyEditChapter": "Redakti ĉapitron", + "studyNewChapter": "Nova ĉapitro", + "studyImportFromChapterX": "Importi el {param}", + "studyOrientation": "Orientiĝo", + "studyAnalysisMode": "Analiza modo", + "studyPinnedChapterComment": "Alpinglita ĉapitra komento", + "studySaveChapter": "Konservi ĉapitron", + "studyClearAnnotations": "Forigi notojn", + "studyClearVariations": "Forigi variaĵojn", + "studyDeleteChapter": "Forigi ĉapitron", + "studyDeleteThisChapter": "Ĉu forigi ĉi tiun ĉapitron? Tiun agon vi ne povos malfari!", + "studyClearAllCommentsInThisChapter": "Forigi ĉiujn komentojn, signobildoj, kaj skribintaj formoj en ĉi tiu ĉapitro", + "studyRightUnderTheBoard": "Ĝuste sub la tabulo", + "studyNoPinnedComment": "Neniu", + "studyNormalAnalysis": "Normala analizo", + "studyHideNextMoves": "Kaŝi la sekvajn movojn", + "studyInteractiveLesson": "Interaga leciono", + "studyChapterX": "Ĉapitro {param}", + "studyEmpty": "Malplena", + "studyStartFromInitialPosition": "Starti el la komenca pozicio", + "studyEditor": "Redaktanto", + "studyStartFromCustomPosition": "Starti el propra pozicio", + "studyLoadAGameByUrl": "Ŝarĝi ludon el URL", + "studyLoadAPositionFromFen": "Ŝarĝi posicion el FEN kodo", + "studyLoadAGameFromPgn": "Ŝarĝi ludon el PGN", + "studyAutomatic": "Aŭtomata", + "studyUrlOfTheGame": "URL de la ludo", + "studyLoadAGameFromXOrY": "Ŝarĝu ludon el {param1} aŭ {param2}", + "studyCreateChapter": "Krei ĉapitron", + "studyCreateStudy": "Krei studon", + "studyEditStudy": "Redakti studon", + "studyVisibility": "Videbleco", + "studyPublic": "Publika", + "studyUnlisted": "Nelistigita", + "studyInviteOnly": "Per invito", + "studyAllowCloning": "Permesi klonadon", + "studyNobody": "Neniu", + "studyOnlyMe": "Nur mi", + "studyContributors": "Kontribuantoj", + "studyMembers": "Membroj", + "studyEveryone": "Ĉiuj", + "studyEnableSync": "Ebligi sinkronigon", + "studyYesKeepEveryoneOnTheSamePosition": "Jes: ĉiuj vidas la saman pozicion", + "studyNoLetPeopleBrowseFreely": "Ne: lasu homojn esplori libere", + "studyPinnedStudyComment": "Komento alpinglita al la studo", + "studyStart": "Komenci", + "studySave": "Konservi", + "studyClearChat": "Vakigi babiladon", + "studyDeleteTheStudyChatHistory": "Ĉu forigi la historian babilejon de la ĉapitro? Tiun agon vi ne povos malfari!", + "studyDeleteStudy": "Forigi studon", + "studyConfirmDeleteStudy": "Ĉu forigi la tuta studo? Ne estas reiro! Tajpi la nomon de la studo por konfirmi: {param}", + "studyWhereDoYouWantToStudyThat": "Kie vi volas studi tion?", + "studyGoodMove": "Bona movo", + "studyMistake": "Eraro", + "studyBrilliantMove": "Brilianta movo", + "studyBlunder": "Erarego", + "studyInterestingMove": "Interesa movo", + "studyDubiousMove": "Dubinda movo", + "studyOnlyMove": "Nura movo", + "studyZugzwang": "Movdevigo", + "studyEqualPosition": "Egala aranĝo", + "studyUnclearPosition": "Malklara aranĝo", + "studyWhiteIsSlightlyBetter": "Blanko estas iomete pli bona", + "studyBlackIsSlightlyBetter": "Nigro estas iomete pli bona", + "studyWhiteIsBetter": "Blanko estas pli bona", + "studyBlackIsBetter": "Nigro estas pli bona", + "studyWhiteIsWinning": "Blanko estas gajnanta", + "studyBlackIsWinning": "Nigro estas gajnanta", + "studyNovelty": "Novaĵo", + "studyDevelopment": "Programado", + "studyInitiative": "Iniciato", + "studyAttack": "Atako", + "studyCounterplay": "Kontraŭludo", + "studyTimeTrouble": "Tempa ĝeno", + "studyWithCompensation": "Kun kompenso", + "studyWithTheIdea": "Kun la ideo", + "studyNextChapter": "Sekva ĉapitro", + "studyPrevChapter": "Antaŭa ĉapitro", + "studyStudyActions": "Studaj agoj", + "studyTopics": "Temoj", + "studyMyTopics": "Miaj temoj", + "studyPopularTopics": "Popularaj temoj", + "studyManageTopics": "Administri temojn", + "studyBack": "Reen", + "studyPlayAgain": "Reludi", + "studyWhatWouldYouPlay": "Kion vi ludus en ĉi tiu pozicio?", + "studyYouCompletedThisLesson": "Gratulon! Vi kompletigis la lecionon.", + "studyNbChapters": "{count, plural, =1{{count} Ĉapitro} other{{count} Ĉapitroj}}", + "studyNbGames": "{count, plural, =1{{count} Ludo} other{{count} Ludoj}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membroj}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Algluu ĉi tie vian PGN kodon, maksimume ĝis {count} ludo} other{Algluu ĉi tie vian PGN kodon, ĝis maksimume {count} ludoj}}", + "timeagoJustNow": "ĵus nun", + "timeagoRightNow": "ĵuse", + "timeagoCompleted": "finita", + "timeagoInNbSeconds": "{count, plural, =1{en {count} sekundo} other{en {count} sekundoj}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutoj}}", + "timeagoInNbHours": "{count, plural, =1{en {count} horo} other{en {count} horoj}}", + "timeagoInNbDays": "{count, plural, =1{en {count} tago} other{en {count} tagoj}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semajno} other{en {count} semajnoj}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} monato} other{en {count} monatoj}}", + "timeagoInNbYears": "{count, plural, =1{en {count} jaro} other{en {count} jaroj}}", + "timeagoNbMinutesAgo": "{count, plural, =1{antaŭ {count} minuto} other{antaŭ {count} minutoj}}", + "timeagoNbHoursAgo": "{count, plural, =1{antaŭ {count} horo} other{antaŭ {count} horoj}}", + "timeagoNbDaysAgo": "{count, plural, =1{antaŭ {count} tago} other{antaŭ {count} tagoj}}", + "timeagoNbWeeksAgo": "{count, plural, =1{antaŭ {count} semajno} other{antaŭ {count} semajnoj}}", + "timeagoNbMonthsAgo": "{count, plural, =1{antaŭ {count} monato} other{antaŭ {count} monatoj}}", + "timeagoNbYearsAgo": "{count, plural, =1{antaŭ {count} jaro} other{antaŭ {count} jaroj}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restas} other{{count} minutoj restas}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} horo restas} other{{count} horoj restas}}" } \ No newline at end of file diff --git a/lib/l10n/lila_es.arb b/lib/l10n/lila_es.arb index 403728c5af..f6d132be07 100644 --- a/lib/l10n/lila_es.arb +++ b/lib/l10n/lila_es.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todas las partidas", + "mobileAreYouSure": "¿Estás seguro?", + "mobileBlindfoldMode": "A ciegas", + "mobileCancelTakebackOffer": "Cancelar oferta de deshacer movimiento", + "mobileClearButton": "Limpiar", + "mobileCorrespondenceClearSavedMove": "Borrar movimiento guardado", + "mobileCustomGameJoinAGame": "Únete a una partida", + "mobileFeedbackButton": "Comentarios", + "mobileGreeting": "Hola {param}", + "mobileGreetingWithoutName": "Hola", + "mobileHideVariation": "Ocultar variación", "mobileHomeTab": "Inicio", - "mobilePuzzlesTab": "Ejercicios", - "mobileToolsTab": "Herramientas", - "mobileWatchTab": "Ver", - "mobileSettingsTab": "Ajustes", + "mobileLiveStreamers": "Presentadores en vivo", "mobileMustBeLoggedIn": "Debes iniciar sesión para ver esta página.", - "mobileSystemColors": "Colores del sistema", - "mobileFeedbackButton": "Comentarios", + "mobileNoSearchResults": "No hay resultados", + "mobileNotFollowingAnyUser": "No estás siguiendo a ningún usuario.", "mobileOkButton": "Aceptar", + "mobilePlayersMatchingSearchTerm": "Jugadores con \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Aumentar la pieza arrastrada", + "mobilePuzzleStormConfirmEndRun": "¿Quieres finalizar esta ronda?", + "mobilePuzzleStormFilterNothingToShow": "Nada que mostrar, por favor cambia los filtros", + "mobilePuzzleStormNothingToShow": "Nada que mostrar. Juega algunas rondas de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resuelve tantos ejercicios como puedas en 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderás tu racha actual y tu puntuación será guardada.", + "mobilePuzzleThemesSubtitle": "Realiza ejercicios de tus aperturas favoritas o elige un tema.", + "mobilePuzzlesTab": "Ejercicios", + "mobileRecentSearches": "Búsquedas recientes", "mobileSettingsHapticFeedback": "Respuesta táctil", "mobileSettingsImmersiveMode": "Modo inmersivo", "mobileSettingsImmersiveModeSubtitle": "Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.", - "mobileNotFollowingAnyUser": "No estás siguiendo a ningún usuario.", - "mobileAllGames": "Todas las partidas", - "mobileRecentSearches": "Búsquedas recientes", - "mobileClearButton": "Limpiar", - "mobilePlayersMatchingSearchTerm": "Jugadores con \"{param}\"", - "mobileNoSearchResults": "No hay resultados", - "mobileAreYouSure": "¿Estás seguro?", - "mobilePuzzleStreakAbortWarning": "Perderás tu racha actual y tu puntuación será guardada.", - "mobilePuzzleStormNothingToShow": "Nada que mostrar. Juega algunas rondas de Puzzle Storm.", - "mobileSharePuzzle": "Compartir este ejercicio", - "mobileShareGameURL": "Compartir enlace de la partida", + "mobileSettingsTab": "Ajustes", "mobileShareGamePGN": "Compartir PGN", + "mobileShareGameURL": "Compartir enlace de la partida", "mobileSharePositionAsFEN": "Compartir posición como FEN", - "mobileShowVariations": "Mostrar variaciones", - "mobileHideVariation": "Ocultar variación", + "mobileSharePuzzle": "Compartir este ejercicio", "mobileShowComments": "Mostrar comentarios", - "mobilePuzzleStormConfirmEndRun": "¿Quieres finalizar esta ronda?", - "mobilePuzzleStormFilterNothingToShow": "Nada que mostrar, por favor cambia los filtros", - "mobileCancelTakebackOffer": "Cancelar oferta de deshacer movimiento", - "mobileCancelDrawOffer": "Cancelar ofertas de tablas", - "mobileWaitingForOpponentToJoin": "Esperando a que se una un oponente...", - "mobileBlindfoldMode": "A ciegas", - "mobileLiveStreamers": "Presentadores en vivo", - "mobileCustomGameJoinAGame": "Únete a una partida", - "mobileCorrespondenceClearSavedMove": "Borrar movimiento guardado", - "mobileSomethingWentWrong": "Algo salió mal.", "mobileShowResult": "Ver resultado", - "mobilePuzzleThemesSubtitle": "Realiza ejercicios de tus aperturas favoritas o elige un tema.", - "mobilePuzzleStormSubtitle": "Resuelve tantos ejercicios como puedas en 3 minutos.", - "mobileGreeting": "Hola {param}", - "mobileGreetingWithoutName": "Hola", + "mobileShowVariations": "Mostrar variaciones", + "mobileSomethingWentWrong": "Algo salió mal.", + "mobileSystemColors": "Colores del sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Herramientas", + "mobileWaitingForOpponentToJoin": "Esperando a que se una un oponente...", + "mobileWatchTab": "Ver", "activityActivity": "Actividad", "activityHostedALiveStream": "Emitió en directo", "activityRankedInSwissTournament": "#{param1} En la Clasificatoria de {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Ha hecho {count} movimiento} other{Ha hecho {count} movimientos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{en {count} partida por correspondencia} other{en {count} partidas por correspondencia}}", "activityCompletedNbGames": "{count, plural, =1{Ha jugado {count} partida por correspondencia} other{Ha jugado {count} partidas por correspondencia}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Ha completado {count} {param2} partida por correspondencia} other{Ha completado {count} {param2} partidas por correspondencia}}", "activityFollowedNbPlayers": "{count, plural, =1{Sigue a {count} jugador} other{Sigue a {count} jugadores}}", "activityGainedNbFollowers": "{count, plural, =1{Tiene {count} seguidor nuevo} other{Tiene {count} seguidores nuevos}}", "activityHostedNbSimuls": "{count, plural, =1{Ha ofrecido {count} exhibición simultánea} other{Ha ofrecido {count} exhibiciones simultáneas}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha competido en {count} torneo suizo} other{Ha competido en {count} torneos suizos}}", "activityJoinedNbTeams": "{count, plural, =1{Miembro de {count} equipo} other{Miembro de {count} equipos}}", "broadcastBroadcasts": "Emisiones", + "broadcastMyBroadcasts": "Mis transmisiones", "broadcastLiveBroadcasts": "Emisiones de torneos en directo", + "broadcastBroadcastCalendar": "Calendario de transmisiones", + "broadcastNewBroadcast": "Nueva emisión en directo", + "broadcastSubscribedBroadcasts": "Transmisiones suscritas", + "broadcastAboutBroadcasts": "Acerca de las transmisiones", + "broadcastHowToUseLichessBroadcasts": "Como utilizar las transmisiones de Lichess.", + "broadcastTheNewRoundHelp": "La nueva ronda tendrá los mismos miembros y contribuyentes que la anterior.", + "broadcastAddRound": "Añadir una ronda", + "broadcastOngoing": "En curso", + "broadcastUpcoming": "Próximamente", + "broadcastCompleted": "Completadas", + "broadcastCompletedHelp": "Lichess detecta la terminación de la ronda según las partidas de origen. Usa este interruptor si no hay ninguna.", + "broadcastRoundName": "Nombre de la ronda", + "broadcastRoundNumber": "Número de ronda", + "broadcastTournamentName": "Nombre del torneo", + "broadcastTournamentDescription": "Breve descripción del torneo", + "broadcastFullDescription": "Descripción completa del evento", + "broadcastFullDescriptionHelp": "Descripción larga opcional de la emisión. {param1} está disponible. La longitud debe ser inferior a {param2} caracteres.", + "broadcastSourceSingleUrl": "URL origen del archivo PGN", + "broadcastSourceUrlHelp": "URL que Lichess comprobará para obtener actualizaciones PGN. Debe ser públicamente accesible desde Internet.", + "broadcastSourceGameIds": "Hasta 64 identificadores de partidas de Lichess, separados por espacios.", + "broadcastStartDateTimeZone": "Fecha de inicio en la zona horaria local del torneo: {param}", + "broadcastStartDateHelp": "Opcional, si sabes cuando comienza el evento", + "broadcastCurrentGameUrl": "Enlace de la partida actual", + "broadcastDownloadAllRounds": "Descargar todas las rondas", + "broadcastResetRound": "Restablecer esta ronda", + "broadcastDeleteRound": "Eliminar esta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente la ronda y sus partidas.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todas las partidas de esta ronda. La fuente tendrá que estar activa para volver a crearlos.", + "broadcastEditRoundStudy": "Editar estudio de ronda", + "broadcastDeleteTournament": "Elimina este torneo", + "broadcastDefinitivelyDeleteTournament": "Elimina definitivamente todo el torneo, rondas y partidas incluidas.", + "broadcastShowScores": "Mostrar las puntuaciones de los jugadores según los resultados de las partidas", + "broadcastReplacePlayerTags": "Opcional: reemplazar nombres de jugadores, puntuaciones y títulos", + "broadcastFideFederations": "Federaciones FIDE", + "broadcastTop10Rating": "Los 10 mejores", + "broadcastFidePlayers": "Jugadores FIDE", + "broadcastFidePlayerNotFound": "Jugador FIDE no encontrado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federación", + "broadcastAgeThisYear": "Edad actual", + "broadcastUnrated": "Sin puntuación", + "broadcastRecentTournaments": "Torneos recientes", + "broadcastOpenLichess": "Abrir en Lichess", + "broadcastTeams": "Equipos", + "broadcastBoards": "Tableros", + "broadcastOverview": "Resumen", + "broadcastSubscribeTitle": "Suscríbete para ser notificado cuando comience cada ronda. Puedes alternar entre notificaciones de campana o de dispositivo para emisiones en las preferencias de tu cuenta.", + "broadcastUploadImage": "Subir imagen del torneo", + "broadcastNoBoardsYet": "Aún no hay tableros. Estos aparecerán una vez se suban las partidas.", + "broadcastBoardsCanBeLoaded": "Los tableros pueden cargarse gracias a una fuente o a través de {param}", + "broadcastStartsAfter": "Comienza en {param}", + "broadcastStartVerySoon": "La transmisión comenzará muy pronto.", + "broadcastNotYetStarted": "La transmisión aún no ha comenzado.", + "broadcastOfficialWebsite": "Sitio oficial", + "broadcastStandings": "Clasificación", + "broadcastOfficialStandings": "Clasificación oficial", + "broadcastIframeHelp": "Más opciones en {param}", + "broadcastWebmastersPage": "la página del webmaster", + "broadcastPgnSourceHelp": "Una fuente PGN pública en tiempo real para esta ronda. También ofrecemos {param} para una sincronización más rápida y eficiente.", + "broadcastEmbedThisBroadcast": "Inserta esta transmisión en tu sitio web", + "broadcastEmbedThisRound": "Inserta la {param} en tu sitio web", + "broadcastRatingDiff": "Diferencia de valoración", + "broadcastGamesThisTournament": "Partidas en este torneo", + "broadcastScore": "Resultado", + "broadcastAllTeams": "Todos los equipos", + "broadcastTournamentFormat": "Formato del torneo", + "broadcastTournamentLocation": "Ubicación del torneo", + "broadcastTopPlayers": "Mejores jugadores", + "broadcastTimezone": "Zona horaria", + "broadcastFideRatingCategory": "Categoría de calificación de FIDE", + "broadcastOptionalDetails": "Detalles opcionales", + "broadcastPastBroadcasts": "Transmisiones pasadas", + "broadcastAllBroadcastsByMonth": "Ver todas las transmisiones por mes", + "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmisión} other{{count} retransmisiones}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafiar a una partida", "challengeChallengeDeclined": "Desafío rechazado", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Campana de notificación", + "preferencesBlindfold": "A ciegas", "puzzlePuzzles": "Ejercicios", "puzzlePuzzleThemes": "Ejercicios por temas", "puzzleRecommended": "Recomendado", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Una pieza ataca o defiende una casilla, a través de una pieza del oponente.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "El oponente está limitado en los movimientos que puede realizar, y todos los movimientos empeoran su posición.", - "puzzleThemeHealthyMix": "Mezcla equilibrada", - "puzzleThemeHealthyMixDescription": "Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.", + "puzzleThemeMix": "Mezcla equilibrada", + "puzzleThemeMixDescription": "Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.", "puzzleThemePlayerGames": "Partidas de jugadores", "puzzleThemePlayerGamesDescription": "Busca ejercicios generados a partir de tus partidas o de las de otros jugadores.", "puzzleThemePuzzleDownloadInformation": "Estos ejercicios son de dominio público y pueden descargarse desde {param}.", @@ -505,7 +583,6 @@ "replayMode": "Modo de repetición", "realtimeReplay": "Tiempo real", "byCPL": "Por PCP", - "openStudy": "Abrir estudio", "enable": "Activar", "bestMoveArrow": "Indicar la mejor jugada", "showVariationArrows": "Mostrar flechas de variantes", @@ -515,7 +592,6 @@ "memory": "Memoria", "infiniteAnalysis": "Análisis infinito", "removesTheDepthLimit": "Elimina el límite de profundidad del análisis y hace trabajar a tu ordenador", - "engineManager": "Gestor de motores", "blunder": "Error grave", "mistake": "Error", "inaccuracy": "Imprecisión", @@ -597,6 +673,7 @@ "rank": "Posición", "rankX": "Clasificación: {param}", "gamesPlayed": "Partidas jugadas", + "ok": "Aceptar", "cancel": "Cancelar", "whiteTimeOut": "Las blancas agotaron su tiempo", "blackTimeOut": "Las negras agotaron su tiempo", @@ -713,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Te sigue", "xStartedFollowingY": "{param1} comenzó a seguir a {param2}", "more": "Más", "memberSince": "Miembro desde", @@ -819,7 +895,9 @@ "cheat": "Trampa", "troll": "Acoso", "other": "Otro", - "reportDescriptionHelp": "Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\"; explícanos cómo has llegado a esta conclusión. Tu informe será procesado más rápido si está escrito en inglés.", + "reportCheatBoostHelp": "Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\", sino cómo has llegado a esta conclusión.", + "reportUsernameHelp": "Explica qué es lo que te resulta ofensivo de este nombre de usuario. No digas solo \"es ofensivo\" o \"inapropiado\", sino cómo llegaste a esta conclusión, sobre todo si el insulto no es tan obvio, no está en inglés, es jerga o una referencia histórica o cultural.", + "reportProcessedFasterInEnglish": "Tu informe será procesado más rápido si está escrito en inglés.", "error_provideOneCheatedGameLink": "Por favor, proporciona al menos un enlace a una partida en la que se hicieron trampas.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Mostrarme todo", "lichessPatronInfo": "Lichess es una organización benéfica y un software totalmente libre y de código abierto.\nTodos los gastos de funcionamiento, desarrollo y contenidos se financian exclusivamente mediante las donaciones de sus usuarios.", "nothingToSeeHere": "Nada que ver aquí por ahora.", + "stats": "Estadísticas", "opponentLeftCounter": "{count, plural, =1{Tu oponente ha salido de la partida. Podrás reclamar la victoria en {count} segundo.} other{Tu oponente ha salido de la partida. Podrás reclamar la victoria en {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} medio movimiento} other{Mate en {count} medios movimientos}}", "nbBlunders": "{count, plural, =1{{count} error grave} other{{count} errores graves}}", @@ -1224,7 +1303,7 @@ "nbInaccuracies": "{count, plural, =1{{count} imprecisión} other{{count} imprecisiones}}", "nbPlayers": "{count, plural, =1{{count} jugador} other{{count} jugadores}}", "nbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", - "ratingXOverYGames": "{count, plural, =1{puntuación {count} en {param2} partida} other{Puntuación de {count} en {param2} partidas}}", + "ratingXOverYGames": "{count, plural, =1{Puntuación {count} en {param2} partida} other{Puntuación de {count} en {param2} partidas}}", "nbBookmarks": "{count, plural, =1{{count} partida favorita} other{{count} partidas favoritas}}", "nbDays": "{count, plural, =1{{count} día} other{{count} días}}", "nbHours": "{count, plural, =1{{count} hora} other{{count} horas}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 ronda} other{{count} rondas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Has jugado una ronda de {param2}} other{Has jugado {count} rondas de {param2}}}", "streamerLichessStreamers": "Presentadores de Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Mis estudios", + "studyStudiesIContributeTo": "Estudios en los que colaboro", + "studyMyPublicStudies": "Mis estudios públicos", + "studyMyPrivateStudies": "Mis estudios privados", + "studyMyFavoriteStudies": "Mis estudios favoritos", + "studyWhatAreStudies": "¿Qué son los estudios?", + "studyAllStudies": "Todos los estudios", + "studyStudiesCreatedByX": "Estudios creados por {param}", + "studyNoneYet": "Ninguno por ahora.", + "studyHot": "De interés actualmente", + "studyDateAddedNewest": "Fecha (más recientes)", + "studyDateAddedOldest": "Fecha (más antiguos)", + "studyRecentlyUpdated": "Actualizados recientemente", + "studyMostPopular": "Más populares", + "studyAlphabetical": "Alfabético", + "studyAddNewChapter": "Añadir nuevo capítulo", + "studyAddMembers": "Añadir miembros", + "studyInviteToTheStudy": "Invitar al estudio", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, invita sólo a personas que conozcas y que deseen unirse a este estudio.", + "studySearchByUsername": "Buscar por nombre de usuario", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Dejar el estudio", + "studyYouAreNowAContributor": "Ahora eres un colaborador", + "studyYouAreNowASpectator": "Ahora eres un espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Me gusta", + "studyUnlike": "No me gusta", + "studyNewTag": "Nueva etiqueta", + "studyCommentThisPosition": "Comentar esta posición", + "studyCommentThisMove": "Comentar este movimiento", + "studyAnnotateWithGlyphs": "Anotar con iconos", + "studyTheChapterIsTooShortToBeAnalysed": "El capítulo es demasiado corto para analizarlo.", + "studyOnlyContributorsCanRequestAnalysis": "Sólo los colaboradores del estudio pueden solicitar un análisis por ordenador.", + "studyGetAFullComputerAnalysis": "Obtén un análisis completo de la línea principal en el servidor.", + "studyMakeSureTheChapterIsComplete": "Asegúrate de que el capítulo está completo. Sólo puede solicitar el análisis una vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos los miembros de SYNC permanecen en la misma posición", + "studyShareChanges": "Comparte cambios con los espectadores y guárdalos en el servidor", + "studyPlaying": "Jugando", + "studyShowEvalBar": "Barras de evaluación", + "studyFirst": "Primero", + "studyPrevious": "Anterior", + "studyNext": "Siguiente", + "studyLast": "Último", "studyShareAndExport": "Compartir y exportar", - "studyStart": "Comenzar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN del estudio", + "studyDownloadAllGames": "Descargar todas las partidas", + "studyChapterPgn": "PGN del capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descargar partida", + "studyStudyUrl": "URL del estudio", + "studyCurrentChapterUrl": "URL del capítulo actual", + "studyYouCanPasteThisInTheForumToEmbed": "Puedes pegar esto en el foro para insertar la partida", + "studyStartAtInitialPosition": "Comenzar desde la posición inicial", + "studyStartAtX": "Comenzar en {param}", + "studyEmbedInYourWebsite": "Insértalo en tu página o blog", + "studyReadMoreAboutEmbedding": "Leer más sobre insertar contenido", + "studyOnlyPublicStudiesCanBeEmbedded": "¡Solo los estudios públicos pueden ser insertados!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1}, proporcionado por {param2}", + "studyStudyNotFound": "No se encontró el estudio", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Capítulo nuevo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientación", + "studyAnalysisMode": "Modo de análisis", + "studyPinnedChapterComment": "Comentario fijo para el capítulo", + "studySaveChapter": "Guardar capítulo", + "studyClearAnnotations": "Borrar anotaciones", + "studyClearVariations": "Borrar variantes", + "studyDeleteChapter": "Borrar capítulo", + "studyDeleteThisChapter": "¿Realmente quieres borrar el capítulo? ¡Esta acción no se puede deshacer!", + "studyClearAllCommentsInThisChapter": "¿Borrar todos los comentarios, iconos y marcas de este capítulo?", + "studyRightUnderTheBoard": "Justo debajo del tablero", + "studyNoPinnedComment": "Ninguno", + "studyNormalAnalysis": "Análisis normal", + "studyHideNextMoves": "Ocultar los siguientes movimientos", + "studyInteractiveLesson": "Lección interactiva", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vacío", + "studyStartFromInitialPosition": "Comenzar desde la posición inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Comenzar desde una posición personalizada", + "studyLoadAGameByUrl": "Cargar una partida desde una URL", + "studyLoadAPositionFromFen": "Cargar una posición vía código FEN", + "studyLoadAGameFromPgn": "Cargar una partida vía código PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "URL de la partida", + "studyLoadAGameFromXOrY": "Cargar una partida desde {param1} o {param2}", + "studyCreateChapter": "Crear capítulo", + "studyCreateStudy": "Crear estudio", + "studyEditStudy": "Editar estudio", + "studyVisibility": "Visibilidad", + "studyPublic": "Público", + "studyUnlisted": "Sin listar", + "studyInviteOnly": "Acceso mediante invitación", + "studyAllowCloning": "Permitir clonado", + "studyNobody": "Nadie", + "studyOnlyMe": "Sólo yo", + "studyContributors": "Colaboradores", + "studyMembers": "Miembros", + "studyEveryone": "Todo el mundo", + "studyEnableSync": "Habilitar sincronización", + "studyYesKeepEveryoneOnTheSamePosition": "Sí: todo el mundo ve la misma posición", + "studyNoLetPeopleBrowseFreely": "No: permitir que la gente navegue libremente", + "studyPinnedStudyComment": "Comentario fijado del estudio", + "studyStart": "Comenzar", + "studySave": "Guardar", + "studyClearChat": "Limpiar el chat", + "studyDeleteTheStudyChatHistory": "¿Realmente quieres borrar el historial de chat? ¡Esta acción no se puede deshacer!", + "studyDeleteStudy": "Borrar estudio", + "studyConfirmDeleteStudy": "¿Seguro que quieres eliminar el estudio? Ten en cuenta que esta acción no se puede deshacer. Para confirmar, escribe el nombre del estudio: {param}", + "studyWhereDoYouWantToStudyThat": "¿Dónde quieres estudiar eso?", + "studyGoodMove": "Jugada buena", + "studyMistake": "Error", + "studyBrilliantMove": "Jugada muy buena", + "studyBlunder": "Error grave", + "studyInterestingMove": "Jugada interesante", + "studyDubiousMove": "Jugada dudosa", + "studyOnlyMove": "Movimiento único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posición igualada", + "studyUnclearPosition": "Posición poco clara", + "studyWhiteIsSlightlyBetter": "Las blancas están ligeramente mejor", + "studyBlackIsSlightlyBetter": "Las negras están ligeramente mejor", + "studyWhiteIsBetter": "Las blancas están mejor", + "studyBlackIsBetter": "Las negras están mejor", + "studyWhiteIsWinning": "Las blancas están ganando", + "studyBlackIsWinning": "Las negras están ganando", + "studyNovelty": "Novedad", + "studyDevelopment": "Desarrollo", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contrajuego", + "studyTimeTrouble": "Problema de tiempo", + "studyWithCompensation": "Con compensación", + "studyWithTheIdea": "Con la idea", + "studyNextChapter": "Capítulo siguiente", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Acciones de estudio", + "studyTopics": "Temas", + "studyMyTopics": "Mis temas", + "studyPopularTopics": "Temas populares", + "studyManageTopics": "Administrar temas", + "studyBack": "Volver", + "studyPlayAgain": "Jugar de nuevo", + "studyWhatWouldYouPlay": "¿Qué jugarías en esta posición?", + "studyYouCompletedThisLesson": "¡Felicidades! Has completado esta lección.", + "studyPerPage": "{param} por página", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", + "studyNbMembers": "{count, plural, =1{{count} Miembro} other{{count} Miembros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega aquí el código PGN, {count} partida como máximo} other{Pega aquí el código PGN, {count} partidas como máximo}}", + "timeagoJustNow": "ahora mismo", + "timeagoRightNow": "ahora mismo", + "timeagoCompleted": "completado", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segundo} other{en {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{en {count} día} other{en {count} días}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semana} other{en {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{en {count} año} other{en {count} años}}", + "timeagoNbMinutesAgo": "{count, plural, =1{hace {count} minuto} other{hace {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{hace {count} hora} other{hace {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{hace {count} día} other{hace {count} días}}", + "timeagoNbWeeksAgo": "{count, plural, =1{hace {count} semana} other{hace {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{hace {count} mes} other{hace {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{hace {count} año} other{hace {count} años}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutos restantes} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} horas restantes} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_et.arb b/lib/l10n/lila_et.arb index ea099f7fa3..c2e6886858 100644 --- a/lib/l10n/lila_et.arb +++ b/lib/l10n/lila_et.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =1{Liitus {count} rühmaga} other{Liitus {count} rühmaga}}", "broadcastBroadcasts": "Otseülekanded", "broadcastLiveBroadcasts": "Otseülekanded turniirilt", + "broadcastNewBroadcast": "Uus otseülekanne", + "broadcastAddRound": "Lisa voor", + "broadcastOngoing": "Käimas", + "broadcastUpcoming": "Tulemas", + "broadcastCompleted": "Lõppenud", + "broadcastRoundName": "Vooru nimi", + "broadcastRoundNumber": "Vooru number", + "broadcastTournamentName": "Turniiri nimi", + "broadcastTournamentDescription": "Lühike turniiri kirjeldus", + "broadcastFullDescription": "Sündmuse täielik kirjeldus", + "broadcastFullDescriptionHelp": "Valikuline otseülekande kirjeldus. {param1} on saadaval. Pikkus peab olema maksimaalselt {param2} tähemärki.", + "broadcastSourceUrlHelp": "URL, kust Lichess saab PGN-i värskenduse. See peab olema Internetist kättesaadav.", + "broadcastStartDateHelp": "Valikuline, kui tead millal sündmus algab", + "broadcastCurrentGameUrl": "Praeguse mängu URL", + "broadcastDownloadAllRounds": "Lae alla kõik voorud", + "broadcastResetRound": "Lähtesta see voor", + "broadcastDeleteRound": "Kustuta see voor", + "broadcastDefinitivelyDeleteRound": "Kustuta lõplikult voor ja selle mängud.", + "broadcastDeleteAllGamesOfThisRound": "Kustuta kõik mängud sellest voorust. Allikas peab olema aktiveeritud nende taastamiseks.", "challengeChallengeToPlay": "Väljakutse mängule", "challengeChallengeDeclined": "Väljakutse tagasi lükatud", "challengeChallengeAccepted": "Väljakutse vastu võetud!", @@ -297,8 +316,8 @@ "puzzleThemeVeryLong": "Väga pikk ülesanne", "puzzleThemeZugzwang": "Vahekäik", "puzzleThemeZugzwangDescription": "Vastasel on piiratud võimalused teha lubatud käike ja kõik halvendavad vastase olukorda.", - "puzzleThemeHealthyMix": "Tervislik segu", - "puzzleThemeHealthyMixDescription": "Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.", + "puzzleThemeMix": "Tervislik segu", + "puzzleThemeMixDescription": "Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.", "searchSearch": "Otsi", "settingsSettings": "Seaded", "settingsCloseAccount": "Sulge konto", @@ -410,7 +429,6 @@ "replayMode": "Kordusrežiim", "realtimeReplay": "Reaalajas", "byCPL": "CPL järgi", - "openStudy": "Ava uuring", "enable": "Luba", "bestMoveArrow": "Parima käigu nool", "evaluationGauge": "Hinnangunäidik", @@ -609,7 +627,6 @@ "block": "Blokeeri", "blocked": "Blokeeritud", "unblock": "Tühista blokeering", - "followsYou": "Jälgib sind", "xStartedFollowingY": "{param1} hakkas jälgima {param2}", "more": "Rohkem", "memberSince": "Liitunud", @@ -706,7 +723,6 @@ "cheat": "Sohk", "troll": "Troll", "other": "Muu", - "reportDescriptionHelp": "Kleebi link mängu(de)st ja selgita, mis on valesti selle kasutaja käitumises. Ära ütle lihtsalt \"ta teeb sohki\", vaid seleta, kuidas sa selle järelduseni jõudsid. Sõnum käsitletakse kiiremini kui see on kirjutatud inglise keeles.", "error_provideOneCheatedGameLink": "Palun andke vähemalt üks link pettust sisaldavale mängule.", "by": "autor {param}", "importedByX": "Importis {param}", @@ -1074,7 +1090,7 @@ "ourEventTips": "Meie nõuanded ürituste korraldamiseks", "lichessPatronInfo": "Lichess on heategevuslik ja täiesti tasuta avatud lähtekoodiga tarkvara.\nKõik tegevuskulud, arendus ja sisu rahastatakse ainult kasutajate annetustest.", "opponentLeftCounter": "{count, plural, =1{Vastane lahkus mängust. Saate panna vastase alistuma {count} sekundi pärast.} other{Vastane lahkus mängust. Saad kuulutada ennast võitjaks {count} sekundi pärast.}}", - "mateInXHalfMoves": "{count, plural, =1{Šahh ja Matt {count} käiguga} other{Šahh ja matt {count} käiguga}}", + "mateInXHalfMoves": "{count, plural, =1{Matt {count} käiguga} other{Matt {count} käiguga}}", "nbBlunders": "{count, plural, =1{{count} prohmakas} other{{count} prohmakat}}", "nbMistakes": "{count, plural, =1{{count} viga} other{{count} viga}}", "nbInaccuracies": "{count, plural, =1{{count} ebatäpsus} other{{count} ebatäpsust}}", @@ -1167,6 +1183,169 @@ "stormXRuns": "{count, plural, =1{1 mäng} other{{count} mängu}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Mängis ühe mängu {param2}i} other{Mängis {count} mängu {param2}i}}", "streamerLichessStreamers": "Lichessi striimijad", + "studyPrivate": "Privaatne", + "studyMyStudies": "Minu uuringud", + "studyStudiesIContributeTo": "Uuringud, milles osalen", + "studyMyPublicStudies": "Minu avalikud uuringud", + "studyMyPrivateStudies": "Minu privaatsed uuringud", + "studyMyFavoriteStudies": "Minu lemmikuuringud", + "studyWhatAreStudies": "Mis on uuringud?", + "studyAllStudies": "Kõik uuringud", + "studyStudiesCreatedByX": "{param} loodud uuringud", + "studyNoneYet": "Veel mitte ühtegi.", + "studyHot": "Kuum", + "studyDateAddedNewest": "Lisamisaeg (uusimad)", + "studyDateAddedOldest": "Lisamisaeg (vanimad)", + "studyRecentlyUpdated": "Hiljuti uuendatud", + "studyMostPopular": "Kõige populaarsemad", + "studyAlphabetical": "Tähestikuline", + "studyAddNewChapter": "Lisa uus peatükk", + "studyAddMembers": "Lisa liikmeid", + "studyInviteToTheStudy": "Kutsu uuringule", + "studyPleaseOnlyInvitePeopleYouKnow": "Palun kutsuge ainult inimesi keda te teate ning kes soovivad aktiivselt selle uuringuga liituda.", + "studySearchByUsername": "Otsi kasutajanime järgi", + "studySpectator": "Vaatleja", + "studyContributor": "Panustaja", + "studyKick": "Viska välja", + "studyLeaveTheStudy": "Lahku uuringust", + "studyYouAreNowAContributor": "Te olete nüüd panustaja", + "studyYouAreNowASpectator": "Te olete nüüd vaatleja", + "studyPgnTags": "PGN sildid", + "studyLike": "Meeldib", + "studyUnlike": "Eemalda meeldimine", + "studyNewTag": "Uus silt", + "studyCommentThisPosition": "Kommenteeri seda seisu", + "studyCommentThisMove": "Kommenteeri seda käiku", + "studyAnnotateWithGlyphs": "Annoteerige glüüfidega", + "studyTheChapterIsTooShortToBeAnalysed": "See peatükk on liiga lühike analüüsimiseks.", + "studyOnlyContributorsCanRequestAnalysis": "Ainult selle uuringu panustajad saavad taotleda arvuti analüüsi.", + "studyGetAFullComputerAnalysis": "Taotle täielikku serveripoolset arvuti analüüsi põhiliinist.", + "studyPlaying": "Mängimas", + "studyFirst": "Esimene", + "studyPrevious": "Eelmine", + "studyNext": "Järgmine", + "studyLast": "Viimane", "studyShareAndExport": "Jaga & ekspordi", - "studyStart": "Alusta" + "studyCloneStudy": "Klooni", + "studyStudyPgn": "Uuringu PGN", + "studyDownloadAllGames": "Lae alla kõik mängud", + "studyChapterPgn": "Peatüki PGN", + "studyCopyChapterPgn": "Kopeeri PGN", + "studyDownloadGame": "Lae alla mäng", + "studyStudyUrl": "Uuringu URL", + "studyCurrentChapterUrl": "Praeguse peatüki URL", + "studyYouCanPasteThisInTheForumToEmbed": "Te saate selle asetada foorumisse või oma Lichessi blogisse sängitamiseks", + "studyStartAtInitialPosition": "Alusta algseisus", + "studyStartAtX": "Alusta {param}", + "studyEmbedInYourWebsite": "Sängita oma veebilehele", + "studyReadMoreAboutEmbedding": "Loe rohkem sängitamisest", + "studyOnlyPublicStudiesCanBeEmbedded": "Ainult avalikud uurimused on sängitatavad!", + "studyOpen": "Ava", + "studyXBroughtToYouByY": "{param1}, leheküljelt {param2}", + "studyStudyNotFound": "Uuringut ei leitud", + "studyEditChapter": "Muuda peatükki", + "studyNewChapter": "Uus peatükk", + "studyImportFromChapterX": "Too peatükist {param}", + "studyOrientation": "Suund", + "studyAnalysisMode": "Analüüsirežiim", + "studyPinnedChapterComment": "Kinnitatud peatüki kommentaar", + "studySaveChapter": "Salvesta peatükk", + "studyClearAnnotations": "Eemalda kommentaarid", + "studyClearVariations": "Eemalda variatsioonid", + "studyDeleteChapter": "Kustuta peatükk", + "studyDeleteThisChapter": "Kustuta see peatükk? Seda ei saa tühistada!", + "studyClearAllCommentsInThisChapter": "Puhasta kõik kommentaarid, glüüfid ja joonistatud kujundid sellest peatükist", + "studyRightUnderTheBoard": "Otse laua all", + "studyNoPinnedComment": "Puudub", + "studyNormalAnalysis": "Tavaline analüüs", + "studyHideNextMoves": "Peida järgmised käigud", + "studyInteractiveLesson": "Interaktiivne õppetund", + "studyChapterX": "Peatükk {param}", + "studyEmpty": "Tühi", + "studyStartFromInitialPosition": "Alusta algsest positsioonist", + "studyEditor": "Muuda", + "studyStartFromCustomPosition": "Alusta kohandatud positsioonist", + "studyLoadAGameByUrl": "Lae mäng alla URL-ist", + "studyLoadAPositionFromFen": "Laadi alla positsioon FEN-ist", + "studyLoadAGameFromPgn": "Lae mänge PGN-ist", + "studyAutomatic": "Automaatne", + "studyUrlOfTheGame": "URL mängu", + "studyLoadAGameFromXOrY": "Lae mäng alla {param1} või {param2}", + "studyCreateChapter": "Alusta peatükk", + "studyCreateStudy": "Koosta uuring", + "studyEditStudy": "Muuda uuringut", + "studyVisibility": "Nähtavus", + "studyPublic": "Avalik", + "studyUnlisted": "Mitte avalik", + "studyInviteOnly": "Ainult kutsega", + "studyAllowCloning": "Luba kloneerimine", + "studyNobody": "Mitte keegi", + "studyOnlyMe": "Ainult mina", + "studyContributors": "Panustajad", + "studyMembers": "Liikmed", + "studyEveryone": "Kõik", + "studyEnableSync": "Luba sünkroneerimine", + "studyYesKeepEveryoneOnTheSamePosition": "Jah: hoia kõik samal positsioonil", + "studyNoLetPeopleBrowseFreely": "Ei: lase inimestel sirvida vabalt", + "studyPinnedStudyComment": "Kinnitatud uuringu kommentaar", + "studyStart": "Alusta", + "studySave": "Salvesta", + "studyDeleteTheStudyChatHistory": "Kas soovite kustutada uuringu vestluse ajaloo? Seda otsust ei saa tagasi võtta!", + "studyDeleteStudy": "Kustuta uuring", + "studyConfirmDeleteStudy": "Kas soovite kustutada terve uuringu? Seda otsust ei saa tagasi võtta! Kirjutage uuringu nimi otsuse kinnitamiseks: {param}", + "studyWhereDoYouWantToStudyThat": "Kus te seda lauda soovite uurida?", + "studyGoodMove": "Hea käik", + "studyMistake": "Viga", + "studyBrilliantMove": "Suurepärane käik", + "studyBlunder": "Tõsine viga", + "studyInterestingMove": "Huvitav käik", + "studyDubiousMove": "Kahtlane käik", + "studyOnlyMove": "Ainus käik", + "studyZugzwang": "Sundkäik", + "studyEqualPosition": "Võrdne positsioon", + "studyUnclearPosition": "Ebaselge positsioon", + "studyWhiteIsSlightlyBetter": "Valgel on kerge eelis", + "studyBlackIsSlightlyBetter": "Mustal on kerge eelis", + "studyWhiteIsBetter": "Valgel on eelis", + "studyBlackIsBetter": "Mustal on eelis", + "studyWhiteIsWinning": "Valge on võitmas", + "studyBlackIsWinning": "Must on võitmas", + "studyNovelty": "Uudsus", + "studyDevelopment": "Arendus", + "studyInitiative": "Algatus", + "studyAttack": "Rünnak", + "studyCounterplay": "Vastumäng", + "studyNextChapter": "Järgmine peatükk", + "studyPrevChapter": "Eelmine peatükk", + "studyStudyActions": "Uuringu toimingud", + "studyTopics": "Teemad", + "studyMyTopics": "Minu teemad", + "studyPopularTopics": "Populaarsed teemad", + "studyManageTopics": "Halda teemasid", + "studyBack": "Tagasi", + "studyPlayAgain": "Mängi uuesti", + "studyWhatWouldYouPlay": "Mis sa mängiksid selles positsioonis?", + "studyYouCompletedThisLesson": "Palju õnne! Oled läbinud selle õppetunni.", + "studyNbChapters": "{count, plural, =1{{count} peatükk} other{{count} peatükki}}", + "studyNbGames": "{count, plural, =1{{count} mäng} other{{count} mängu}}", + "studyNbMembers": "{count, plural, =1{{count} liige} other{{count} liiget}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Aseta oma PGN tekst siia, kuni {count} mäng} other{Aseta oma PGN tekst siia, kuni {count} mängu}}", + "timeagoJustNow": "äsja", + "timeagoRightNow": "praegu", + "timeagoCompleted": "lõppenud", + "timeagoInNbSeconds": "{count, plural, =1{{count} sekundi pärast} other{{count} sekundi pärast}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} minuti pärast} other{{count} minuti pärast}}", + "timeagoInNbHours": "{count, plural, =1{{count} tunni pärast} other{{count} tunni pärast}}", + "timeagoInNbDays": "{count, plural, =1{{count} päeva pärast} other{{count} päeva pärast}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} nädala pärast} other{{count} nädala pärast}}", + "timeagoInNbMonths": "{count, plural, =1{{count} kuu pärast} other{{count} kuu pärast}}", + "timeagoInNbYears": "{count, plural, =1{{count} aasta pärast} other{{count} aasta pärast}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut tagasi} other{{count} minutit tagasi}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} tund tagasi} other{{count} tundi tagasi}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} päev tagasi} other{{count} päeva tagasi}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} nädal tagasi} other{{count} nädalat tagasi}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} kuu tagasi} other{{count} kuud tagasi}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} aasta tagasi} other{{count} aastat tagasi}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut jäänud} other{{count} minutit jäänud}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} tund jäänud} other{{count} tundi jäänud}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eu.arb b/lib/l10n/lila_eu.arb index b5b98311c3..8234578b92 100644 --- a/lib/l10n/lila_eu.arb +++ b/lib/l10n/lila_eu.arb @@ -1,4 +1,48 @@ { + "mobileAllGames": "Partida guztiak", + "mobileAreYouSure": "Ziur zaude?", + "mobileBlindfoldMode": "Itsuka", + "mobileCancelTakebackOffer": "Bertan behera utzi atzera-egite eskaera", + "mobileClearButton": "Garbitu", + "mobileCorrespondenceClearSavedMove": "Garbitu gordetako jokaldia", + "mobileCustomGameJoinAGame": "Sartu partida baten", + "mobileFeedbackButton": "Iritzia", + "mobileGreeting": "Kaixo {param}", + "mobileGreetingWithoutName": "Kaixo", + "mobileHideVariation": "Ezkutatu aukera", + "mobileHomeTab": "Hasiera", + "mobileLiveStreamers": "Zuzeneko streamerrak", + "mobileMustBeLoggedIn": "Sartu egin behar zara orri hau ikusteko.", + "mobileNoSearchResults": "Emaitzarik ez", + "mobileNotFollowingAnyUser": "Ez zaude erabiltzailerik jarraitzen.", + "mobileOkButton": "Ados", + "mobilePlayersMatchingSearchTerm": "\"{param}\" duten jokalariak", + "mobilePrefMagnifyDraggedPiece": "Handitu arrastatutako pieza", + "mobilePuzzleStormConfirmEndRun": "Saiakera hau amaitu nahi duzu?", + "mobilePuzzleStormFilterNothingToShow": "Ez dago erakusteko ezer, aldatu filtroak", + "mobilePuzzleStormNothingToShow": "Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.", + "mobilePuzzleStormSubtitle": "Ebatzi ahalik eta ariketa gehien 3 minututan.", + "mobilePuzzleStreakAbortWarning": "Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.", + "mobilePuzzleThemesSubtitle": "Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.", + "mobilePuzzlesTab": "Ariketak", + "mobileRecentSearches": "Azken bilaketak", + "mobileSettingsHapticFeedback": "Ukipen-erantzuna", + "mobileSettingsImmersiveMode": "Murgiltze modua", + "mobileSettingsImmersiveModeSubtitle": "Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.", + "mobileSettingsTab": "Ezarpenak", + "mobileShareGamePGN": "Partekatu PGNa", + "mobileShareGameURL": "Partekatu partidaren URLa", + "mobileSharePositionAsFEN": "Partekatu posizioa FEN gisa", + "mobileSharePuzzle": "Partekatu ariketa hau", + "mobileShowComments": "Erakutsi iruzkinak", + "mobileShowResult": "Erakutsi emaitza", + "mobileShowVariations": "Erakutsi aukerak", + "mobileSomethingWentWrong": "Zerbait gaizki joan da.", + "mobileSystemColors": "Sistemaren koloreak", + "mobileTheme": "Itxura", + "mobileToolsTab": "Tresnak", + "mobileWaitingForOpponentToJoin": "Aurkaria sartzeko zain...", + "mobileWatchTab": "Ikusi", "activityActivity": "Jarduera", "activityHostedALiveStream": "Zuzeneko emanaldi bat egin du", "activityRankedInSwissTournament": "Sailkapena {param1}/{param2}", @@ -11,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Jokaldi {count} egin du} other{Jokaldi {count} egin du}}", "activityInNbCorrespondenceGames": "{count, plural, =1{posta bidezko partida {count}-en} other{posta bidezko partida {count}-en}}", "activityCompletedNbGames": "{count, plural, =1{Posta bidezko partida {count} jokatu du} other{Posta bidezko {count} partida jokatu ditu}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{param2} posta bidezko partida {count} osatuta} other{{param2} posta bidezko {count} partida osatuta}}", "activityFollowedNbPlayers": "{count, plural, =1{Jokalari {count} jarraitzen hasi da} other{{count} jokalari jarraitzen hasi da}}", "activityGainedNbFollowers": "{count, plural, =1{Jarraitzaile berri {count} lortu du} other{{count} jarraitzaile berri lortu ditu}}", "activityHostedNbSimuls": "{count, plural, =1{Aldibereko partiden saio {count} antolatu du} other{Aldibereko partiden {count} saio antolatu ditu}}", @@ -21,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Txapelketa suitzar {count}en hartu du parte} other{{count} txapelketa suitzarretan hartu du parte}}", "activityJoinedNbTeams": "{count, plural, =1{Talde {count}era sartu da} other{{count} taldetara sartu da}}", "broadcastBroadcasts": "Emanaldiak", + "broadcastMyBroadcasts": "Nire zuzenekoak", "broadcastLiveBroadcasts": "Txapelketen zuzeneko emanaldiak", + "broadcastBroadcastCalendar": "Emanaldien egutegia", + "broadcastNewBroadcast": "Zuzeneko emanaldi berria", + "broadcastSubscribedBroadcasts": "Harpidetutako emanaldiak", + "broadcastAboutBroadcasts": "Zuzeneko emanaldiei buruz", + "broadcastHowToUseLichessBroadcasts": "Nola erabili Lichessen Zuzenekoak.", + "broadcastTheNewRoundHelp": "Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu.", + "broadcastAddRound": "Gehitu txanda bat", + "broadcastOngoing": "Orain martxan", + "broadcastUpcoming": "Hurrengo emanaldiak", + "broadcastCompleted": "Amaitutako emanaldiak", + "broadcastCompletedHelp": "Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago.", + "broadcastRoundName": "Txandaren izena", + "broadcastRoundNumber": "Txanda zenbaki", + "broadcastTournamentName": "Txapelketaren izena", + "broadcastTournamentDescription": "Txapelketaren deskribapen laburra", + "broadcastFullDescription": "Ekitaldiaren deskribapen osoa", + "broadcastFullDescriptionHelp": "Emanaldiaren azalpen luzea, hautazkoa da. {param1} badago. Luzera {param2} karaktere edo laburragoa izan behar da.", + "broadcastSourceSingleUrl": "PGNaren jatorrizko URLa", + "broadcastSourceUrlHelp": "Lichessek PGNaren eguneraketak jasoko dituen URLa. Interneteko helbide bat izan behar da.", + "broadcastSourceGameIds": "Gehienez ere Lichesseko 64 partidren idak, espazioekin banatuta.", + "broadcastStartDateTimeZone": "Txapelketaren hasiera ordua ordu-zona lokalean: {param}", + "broadcastStartDateHelp": "Hautazkoa, ekitaldia noiz hasten den baldin badakizu", + "broadcastCurrentGameUrl": "Uneko partidaren URL helbidea", + "broadcastDownloadAllRounds": "Deskargatu txanda guztiak", + "broadcastResetRound": "Berrezarri txanda hau", + "broadcastDeleteRound": "Ezabatu txanda hau", + "broadcastDefinitivelyDeleteRound": "Betiko ezabatu txanda eta bere partida guztiak.", + "broadcastDeleteAllGamesOfThisRound": "Ezabatu txanda honetako partida guztiak. Jatorria aktibo egon behar da berriz sortzeko.", + "broadcastEditRoundStudy": "Editatu txandako azterlana", + "broadcastDeleteTournament": "Ezabatu txapelketa hau", + "broadcastDefinitivelyDeleteTournament": "Txapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne.", + "broadcastShowScores": "Erakutsi jokalarien puntuazioak partiden emaitzen arabera", + "broadcastReplacePlayerTags": "Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak", + "broadcastFideFederations": "FIDE federazioak", + "broadcastTop10Rating": "10 onenak", + "broadcastFidePlayers": "FIDE jokalariak", + "broadcastFidePlayerNotFound": "FIDE jokalaria ez da aurkitu", + "broadcastFideProfile": "FIDE profila", + "broadcastFederation": "Federazioa", + "broadcastAgeThisYear": "Adina", + "broadcastUnrated": "Ez du sailkapenik", + "broadcastRecentTournaments": "Azken txapelketak", + "broadcastOpenLichess": "Ireki Lichessen", + "broadcastTeams": "Taldeak", + "broadcastBoards": "Taulak", + "broadcastOverview": "Laburpena", + "broadcastSubscribeTitle": "Harpidetu txanda bakoitza hastean jakinarazpena jasotzeko. Kanpaia edo push erako notifikazioak zure kontuaren hobespenetan aktibatu ditzakezu.", + "broadcastUploadImage": "Kargatu txapelketaren irudia", + "broadcastNoBoardsYet": "Taularik ez oraindik. Partidak igotzean agertuko dira.", + "broadcastBoardsCanBeLoaded": "Taulak iturburu batekin edo {param}ren bidez kargatu daitezke", + "broadcastStartsAfter": "{param}ren ondoren hasiko da", + "broadcastStartVerySoon": "Zuzenekoa berehala hasiko da.", + "broadcastNotYetStarted": "Zuzenekoa ez da oraindik hasi.", + "broadcastOfficialWebsite": "Webgune ofiziala", + "broadcastStandings": "Sailkapena", + "broadcastOfficialStandings": "Sailkapen ofiziala", + "broadcastIframeHelp": "Aukera gehiago {param}ean", + "broadcastWebmastersPage": "webmasterraren webgune", + "broadcastPgnSourceHelp": "Txanda honen zuzeneko PGN iturburua. {param} ere eskaintzen dugu sinkronizazio zehatzagoa nahi baduzu.", + "broadcastEmbedThisBroadcast": "Txertatu zuzeneko hau zure webgunean", + "broadcastEmbedThisRound": "Txertatu {param} zure webgunean", + "broadcastRatingDiff": "Elo diferentzia", + "broadcastGamesThisTournament": "Txapelketa honetako partidak", + "broadcastScore": "Emaitza", + "broadcastAllTeams": "Talde guztiak", + "broadcastTournamentFormat": "Txapelketaren formatua", + "broadcastTournamentLocation": "Txapelketaren kokalekua", + "broadcastTopPlayers": "Jokalari onenak", + "broadcastTimezone": "Ordu-zona", + "broadcastFideRatingCategory": "FIDE rating kategoria", + "broadcastOptionalDetails": "Hautazko xehetasunak", + "broadcastPastBroadcasts": "Pasatutako zuzenekoak", + "broadcastAllBroadcastsByMonth": "Ikusi zuzeneko guztiak hilabeteka", + "broadcastNbBroadcasts": "{count, plural, =1{Zuzeneko {count}} other{{count} zuzeneko}}", "challengeChallengesX": "Erronkak: {param1}", "challengeChallengeToPlay": "Partida baterako erronka egin", "challengeChallengeDeclined": "Erronka baztertuta", @@ -145,6 +265,7 @@ "preferencesNotifyWeb": "Nabigatzailea", "preferencesNotifyDevice": "Gailua", "preferencesBellNotificationSound": "Kanpaiaren jakinarazpen soinua", + "preferencesBlindfold": "Itsuka", "puzzlePuzzles": "Ariketak", "puzzlePuzzleThemes": "Ariketen gaiak", "puzzleRecommended": "Gomendatutakoak", @@ -340,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Aurkariaren pieza baten artetik, pieza batek lauki bat erasotu edo defendatzen duenean.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Aurkariak jokaldi mugatuak ditu eta jokaldi guztien bere posizioa okertu egiten dute.", - "puzzleThemeHealthyMix": "Denetik pixkat", - "puzzleThemeHealthyMixDescription": "Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.", + "puzzleThemeMix": "Denetik pixkat", + "puzzleThemeMixDescription": "Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.", "puzzleThemePlayerGames": "Jokalarien partidak", "puzzleThemePlayerGamesDescription": "Ikusi zure edo beste jokalarien partidetatik sortutako ariketak.", "puzzleThemePuzzleDownloadInformation": "Ariketa hauek publikoak dira, {param} helbidetik deskargatu daitezke.", @@ -420,6 +541,8 @@ "promoteVariation": "Aldaera nagusi bihurtu", "makeMainLine": "Linea nagusi bihurtu", "deleteFromHere": "Ezabatu hemendik aurrera", + "collapseVariations": "Ezkutatu aldaerak", + "expandVariations": "Erakutsi aldaerak", "forceVariation": "Aldaera derrigortu", "copyVariationPgn": "Kopiatu ingurabidearen PGNa", "move": "Jokaldia", @@ -460,7 +583,6 @@ "replayMode": "Partida berriz ikusteko modua", "realtimeReplay": "Denbora errealean", "byCPL": "CPL", - "openStudy": "Ikerketa ireki", "enable": "Aktibatu", "bestMoveArrow": "Jokaldi onenaren gezia", "showVariationArrows": "Erakutsi aldaeren geziak", @@ -470,7 +592,6 @@ "memory": "Memoria", "infiniteAnalysis": "Analisi infinitua", "removesTheDepthLimit": "Sakonera muga ezabatzendu eta zure ordenagailua epel mantentzen du", - "engineManager": "Motore kudeatzailea", "blunder": "Hanka-sartzea", "mistake": "Akatsa", "inaccuracy": "Akats txikia", @@ -496,6 +617,7 @@ "latestForumPosts": "Foroko azken mezuak", "players": "Jokalariak", "friends": "Lagunak", + "otherPlayers": "beste jokalariak", "discussions": "Eztabaidak", "today": "Gaur", "yesterday": "Atzo", @@ -551,6 +673,7 @@ "rank": "Maila", "rankX": "Sailkapena: {param}", "gamesPlayed": "Partida jokaturik", + "ok": "OK", "cancel": "Ezeztatu", "whiteTimeOut": "Zuriaren denbora agortu egin da", "blackTimeOut": "Beltzaren denbora agortu egin da", @@ -570,6 +693,7 @@ "abortGame": "Partida geldiarazi", "gameAborted": "Geldiarazitako partida", "standard": "Ohikoa", + "customPosition": "Posizio pertsonalizatua", "unlimited": "Mugagabea", "mode": "Modua", "casual": "Lagunartekoa", @@ -666,7 +790,6 @@ "block": "Blokeatu", "blocked": "Blokeatuta", "unblock": "Desblokeatu", - "followsYou": "Zu jarraitzen", "xStartedFollowingY": "{param1} {param2} jarraitzen hasi da", "more": "Gehiago", "memberSince": "Noiztik kidea:", @@ -725,6 +848,7 @@ "ifNoneLeaveEmpty": "Ez baduzu, hutsik utzi", "profile": "Profila", "editProfile": "Nire profila editatu", + "realName": "Benetako izena", "setFlair": "Ezarri zure iruditxoa", "flair": "Iruditxoa", "youCanHideFlair": "Webgune guztian zehar erabiltzaile guztien iruditxoak ezkutatzeko ezarpen bat dago.", @@ -744,12 +868,15 @@ "automaticallyProceedToNextGameAfterMoving": "Mugitu ondoren, hurrengo partidara joan", "autoSwitch": "Hurrengo partidara", "puzzles": "Ariketak", + "onlineBots": "Online dauden botak", "name": "Izena", "description": "Deskribapena", "descPrivate": "Deskribapen pribatua", "descPrivateHelp": "Taldekideek bakarrik ikusiko duten testua. Ezarrita badago, taldekideei taldearen deskribapenaren ordez agertuko zaie testu hau.", "no": "Ez", "yes": "Bai", + "website": "Webgunea", + "mobile": "Mobila", "help": "Laguntza:", "createANewTopic": "Gai berria sortu", "topics": "Gaiak", @@ -768,7 +895,9 @@ "cheat": "Tranpak", "troll": "Trolla", "other": "Bestelakoak", - "reportDescriptionHelp": "Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak. Zure mezua azkarrago begiratuko dugu ingelesez idazten baduzu.", + "reportCheatBoostHelp": "Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak.", + "reportUsernameHelp": "Azaldu erabiltzaile-izen honek zer duen iraingarria. Ez esan \"iraingarria da\" soilik, eman arrazoiak, batez ere iraina ezkutatuta badago, ez bada ingelesezko hitz bat edo errefererantzia historiko edo kulturala bada.", + "reportProcessedFasterInEnglish": "Zure mezua azkarrago kudeatuko dugu ingelesez idazten baduzu.", "error_provideOneCheatedGameLink": "Iruzurra izandako partida baten lotura bidali gutxienez.", "by": "egilea {param}", "importedByX": "{param} erabiltzaileak inportatuta", @@ -801,6 +930,7 @@ "slow": "Geldoa", "insideTheBoard": "Taula barruan", "outsideTheBoard": "Taulatik kanpo", + "allSquaresOfTheBoard": "Taulako lauki guztiak", "onSlowGames": "Partida moteletan", "always": "Beti", "never": "Inoiz ere ez", @@ -980,6 +1110,12 @@ "transparent": "Gardena", "deviceTheme": "Gailuaren gaia", "backgroundImageUrl": "Atzeko-planoko irudia:", + "board": "Taula", + "size": "Tamaina", + "opacity": "Gardentasuna", + "brightness": "Argitasuna", + "hue": "Ñabardura", + "boardReset": "Berrezarri koloreak defektuzkoetara", "pieceSet": "Pieza formatua", "embedInYourWebsite": "Zure webgunean txertatu", "usernameAlreadyUsed": "Erabiltzaile izen hori hartuta dago, erabili beste bat.", @@ -1158,6 +1294,8 @@ "instructions": "Jarraibideak", "showMeEverything": "Erakutsi guztia", "lichessPatronInfo": "Lichess software librea da.\nGarapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira.", + "nothingToSeeHere": "Hemen ez dago ezer zuretzat.", + "stats": "Estatistikak", "opponentLeftCounter": "{count, plural, =1{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu segundo {count}en.} other{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu {count} segundotan.}}", "mateInXHalfMoves": "{count, plural, =1{Mate jokaldi erdi {count}n} other{Mate {count} jokaldi erditan}}", "nbBlunders": "{count, plural, =1{Hanka-sartze {count}} other{{count} hanka-sartze}}", @@ -1254,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{Saiakera 1} other{{count} saiakera}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{{param2} ariketaren saiakera bat egin duzu} other{{param2} ariketaren {count} saiakera egin dituzu}}", "streamerLichessStreamers": "Lichess streamerrak", + "studyPrivate": "Pribatua", + "studyMyStudies": "Nire azterlanak", + "studyStudiesIContributeTo": "Nik parte hartzen dudan azterlanak", + "studyMyPublicStudies": "Nire azterlan publikoak", + "studyMyPrivateStudies": "Nire azterlan pribatuak", + "studyMyFavoriteStudies": "Nire azterlan gogokoenak", + "studyWhatAreStudies": "Zer dira azterlanak?", + "studyAllStudies": "Azterlan guztiak", + "studyStudiesCreatedByX": "{param} erabiltzaileak sortutako azterlanak", + "studyNoneYet": "Bat ere ez.", + "studyHot": "Nabarmendutakoak", + "studyDateAddedNewest": "Sorrera-data (berriena)", + "studyDateAddedOldest": "Sorrera-data (zaharrena)", + "studyRecentlyUpdated": "Eguneratutako azkenak", + "studyMostPopular": "Arrakasta gehien duena", + "studyAlphabetical": "Alfabetikoa", + "studyAddNewChapter": "Kapitulu berria gehitu", + "studyAddMembers": "Kideak gehitu", + "studyInviteToTheStudy": "Azterlanera gonbidatu", + "studyPleaseOnlyInvitePeopleYouKnow": "Ezagutzen duzun eta benetan azterlanean interesa duen jendea gonbidatu bakarrik.", + "studySearchByUsername": "Erabiltzaile izenaren arabera bilatu", + "studySpectator": "Ikuslea", + "studyContributor": "Laguntzailea", + "studyKick": "Kanporatu", + "studyLeaveTheStudy": "Azterlana utzi", + "studyYouAreNowAContributor": "Laguntzailea zara orain", + "studyYouAreNowASpectator": "Ikuslea zara orain", + "studyPgnTags": "PGN etiketak", + "studyLike": "Datsegit", + "studyUnlike": "Ez dut atsegin", + "studyNewTag": "Etiketa berria", + "studyCommentThisPosition": "Posizio hau komentatu", + "studyCommentThisMove": "Jokaldi hau komentatu", + "studyAnnotateWithGlyphs": "Ikonoekin komentatu", + "studyTheChapterIsTooShortToBeAnalysed": "Komentatzeko laburregia da kapitulua.", + "studyOnlyContributorsCanRequestAnalysis": "Azterlanaren laguntzaileek bakarrik eskatu dezakete ordenagailu bidezko analisia.", + "studyGetAFullComputerAnalysis": "Linea nagusiaren ordenagailu bidezko analisia lortu.", + "studyMakeSureTheChapterIsComplete": "Ziurtatu kapitulua guztiz osatu duzula. Analisia behin bakarrik eskatu dezakezu.", + "studyAllSyncMembersRemainOnTheSamePosition": "Kide sinkronizatu guztiak posizio berean jarraitzen dute", + "studyShareChanges": "Aldaketak ikusleekin partekatu eta zerbitzarian gorde", + "studyPlaying": "Jokatzen", + "studyShowEvalBar": "Ebaluazio barrak", + "studyFirst": "Lehenengoa", + "studyPrevious": "Aurrekoa", + "studyNext": "Hurrengoa", + "studyLast": "Azkena", "studyShareAndExport": "Partekatu & esportatu", - "studyStart": "Hasi" + "studyCloneStudy": "Klonatu", + "studyStudyPgn": "Azterlanaren PGNa", + "studyDownloadAllGames": "Partida guztiak deskargatu", + "studyChapterPgn": "Kapituluaren PGNa", + "studyCopyChapterPgn": "Kopiatu PGNa", + "studyDownloadGame": "Partida deskargatu", + "studyStudyUrl": "Azterlanaren helbidea", + "studyCurrentChapterUrl": "Uneko kapituluaren helbidea", + "studyYouCanPasteThisInTheForumToEmbed": "Hau foroan itsatsi dezakezu", + "studyStartAtInitialPosition": "Hasierako posizioan hasi", + "studyStartAtX": "Hemen asi {param}", + "studyEmbedInYourWebsite": "Zure webgunean itsatsi", + "studyReadMoreAboutEmbedding": "Itsasteari buruz gehiago irakurri", + "studyOnlyPublicStudiesCanBeEmbedded": "Azterlan publikoak bakarrik txertatu daitezke beste webguneetan!", + "studyOpen": "Ireki", + "studyXBroughtToYouByY": "{param1} azterlana {param2} erabiltzaileak prestatu du", + "studyStudyNotFound": "Azterlana ez da aurkitu", + "studyEditChapter": "Kapitulua aldatu", + "studyNewChapter": "Kapitulu berria", + "studyImportFromChapterX": "Inportatu {param} kapitulotik", + "studyOrientation": "Kokapena", + "studyAnalysisMode": "Analisi modua", + "studyPinnedChapterComment": "Kapituluaren iltzatutako iruzkina", + "studySaveChapter": "Kapitulua gorde", + "studyClearAnnotations": "Iruzkinak garbitu", + "studyClearVariations": "Garbitu aldaerak", + "studyDeleteChapter": "Kapitulua ezabatu", + "studyDeleteThisChapter": "Kapitulu hau ezabatu egin nahi duzu? Ez dago atzera egiterik!", + "studyClearAllCommentsInThisChapter": "Kapitulu honetako iruzkin guztiak ezabatu?", + "studyRightUnderTheBoard": "Xake-taularen azpian", + "studyNoPinnedComment": "Ez erakutsi", + "studyNormalAnalysis": "Analisi arrunta", + "studyHideNextMoves": "Hurrengo jokaldiak ezkutatu", + "studyInteractiveLesson": "Ikasgai interaktiboa", + "studyChapterX": "{param} kapitulua", + "studyEmpty": "Hutsa", + "studyStartFromInitialPosition": "Hasierako posiziotik hasi", + "studyEditor": "Editorea", + "studyStartFromCustomPosition": "Pertsonalizatutako posiziotik hasi", + "studyLoadAGameByUrl": "Partida interneteko helbide batetik kargatu", + "studyLoadAPositionFromFen": "Posizioa FEN batetik kargatu", + "studyLoadAGameFromPgn": "Partida PGN batetik kargatu", + "studyAutomatic": "Automatikoa", + "studyUrlOfTheGame": "Partidaren URLa", + "studyLoadAGameFromXOrY": "Hemendik kargatu partida bat: {param1} edo {param2}", + "studyCreateChapter": "Kapitulua sortu", + "studyCreateStudy": "Azterlana sortu", + "studyEditStudy": "Azterlana aldatu", + "studyVisibility": "Ikusgaitasuna", + "studyPublic": "Publikoa", + "studyUnlisted": "Ez zerrendatu", + "studyInviteOnly": "Gonbidatuentzat bakarrik", + "studyAllowCloning": "Kopiatzea utzi", + "studyNobody": "Inor ere ez", + "studyOnlyMe": "Ni bakarrik", + "studyContributors": "Laguntzaileak", + "studyMembers": "Kideak", + "studyEveryone": "Guztiak", + "studyEnableSync": "Sinkronizazioa aktibatu", + "studyYesKeepEveryoneOnTheSamePosition": "Bai: guztiak posizio berean mantendu", + "studyNoLetPeopleBrowseFreely": "Ez: erabiltzaileei nahi dutena egiten utzi", + "studyPinnedStudyComment": "Azterlanaren iltzatutako iruzkina", + "studyStart": "Hasi", + "studySave": "Gorde", + "studyClearChat": "Txata garbitu", + "studyDeleteTheStudyChatHistory": "Azterlaneko txata ezabatu? Ez dago atzera egiterik!", + "studyDeleteStudy": "Azterlana ezabatu", + "studyConfirmDeleteStudy": "Azterlan osoa ezabatu? Ez dago atzera egiterik! Idatzi azterlanaren izena baieztapena emateko: {param}", + "studyWhereDoYouWantToStudyThat": "Non nahi duzu hori aztertu?", + "studyGoodMove": "Jokaldi ona", + "studyMistake": "Akatsa", + "studyBrilliantMove": "Jokaldi bikaina", + "studyBlunder": "Akats larria", + "studyInterestingMove": "Jokaldi interesgarria", + "studyDubiousMove": "Zalantzazko jokaldia", + "studyOnlyMove": "Jokaldi bakarra", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Berdindutako posizioa", + "studyUnclearPosition": "Posizioa ez da argia", + "studyWhiteIsSlightlyBetter": "Zuria hobetoxeago", + "studyBlackIsSlightlyBetter": "Beltza hobetoxeago", + "studyWhiteIsBetter": "Zuria hobeto", + "studyBlackIsBetter": "Beltza hobeto", + "studyWhiteIsWinning": "Zuria irabazten ari da", + "studyBlackIsWinning": "Beltza irabazten ari da", + "studyNovelty": "Berritasuna", + "studyDevelopment": "Garapena", + "studyInitiative": "Iniziatiba", + "studyAttack": "Erasoa", + "studyCounterplay": "Kontraerasoa", + "studyTimeTrouble": "Denbora-arazoak", + "studyWithCompensation": "Konepntsazioarekin", + "studyWithTheIdea": "Ideiarekin", + "studyNextChapter": "Hurrengo kapitulua", + "studyPrevChapter": "Aurreko kapitulua", + "studyStudyActions": "Azterlanen akzioak", + "studyTopics": "Gaiak", + "studyMyTopics": "Nire gaiak", + "studyPopularTopics": "Gai arrakastatsuak", + "studyManageTopics": "Kudeatu gaiak", + "studyBack": "Atzera joan", + "studyPlayAgain": "Jokatu berriz", + "studyWhatWouldYouPlay": "Zer jokatuko zenuke posizio honetan?", + "studyYouCompletedThisLesson": "Zorionak! Ikasgai hau osatu duzu.", + "studyPerPage": "{param} orrialde bakoitzean", + "studyNbChapters": "{count, plural, =1{Kapitulu {count}} other{{count} kapitulu}}", + "studyNbGames": "{count, plural, =1{Partida {count}} other{{count} partida}}", + "studyNbMembers": "{count, plural, =1{Kide {count}} other{{count} kide}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Itsatsi hemen zure PGNa, gehienez partida {count}} other{Itsatsi hemen zure PGNa, gehienez {count} partida}}", + "timeagoJustNow": "orain", + "timeagoRightNow": "orain", + "timeagoCompleted": "amaituta", + "timeagoInNbSeconds": "{count, plural, =1{segundo {count}en} other{{count} segundotan}}", + "timeagoInNbMinutes": "{count, plural, =1{minutu {count}en} other{{count} minututan}}", + "timeagoInNbHours": "{count, plural, =1{ordu {count}en} other{{count} ordutan}}", + "timeagoInNbDays": "{count, plural, =1{egun {count}en} other{{count} egunetan}}", + "timeagoInNbWeeks": "{count, plural, =1{aste {count}en} other{{count} egunetan}}", + "timeagoInNbMonths": "{count, plural, =1{hilabete {count}en} other{{count} hilabetetan}}", + "timeagoInNbYears": "{count, plural, =1{urte {count}en} other{{count} urtetan}}", + "timeagoNbMinutesAgo": "{count, plural, =1{orain dela minutu {count}} other{orain dela {count} minutu}}", + "timeagoNbHoursAgo": "{count, plural, =1{orain dela ordu {count}} other{orain dela {count} ordu}}", + "timeagoNbDaysAgo": "{count, plural, =1{orain dela egun {count}} other{orain dela {count} egun}}", + "timeagoNbWeeksAgo": "{count, plural, =1{orain dela aste {count}} other{orain dela {count} aste}}", + "timeagoNbMonthsAgo": "{count, plural, =1{orain dela hilabete {count}} other{orain dela {count} hilabete}}", + "timeagoNbYearsAgo": "{count, plural, =1{orain dela urte {count}} other{orain dela {count} urte}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Minutu {count} falta da} other{{count} minutu falta dira}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Ordu {count} falta da} other{{count} ordu falta dira}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fa.arb b/lib/l10n/lila_fa.arb index 535edad601..a88acf4766 100644 --- a/lib/l10n/lila_fa.arb +++ b/lib/l10n/lila_fa.arb @@ -1,70 +1,146 @@ { - "mobileHomeTab": "خانه", - "mobilePuzzlesTab": "معماها", - "mobileToolsTab": "ابزارها", - "mobileWatchTab": "تماشا", - "mobileSettingsTab": "تنظیمات", - "mobileMustBeLoggedIn": "برای دیدن این صفحه باید وارد حساب‌تان شده باشید.", - "mobileSystemColors": "رنگ‌های دستگاه", - "mobileFeedbackButton": "بازخوراند", - "mobileOkButton": "باشه", - "mobileSettingsHapticFeedback": "بازخوراند لمسی", - "mobileSettingsImmersiveMode": "حالت غوطه‌ور", - "mobileSettingsImmersiveModeSubtitle": "هنگام بازی، میانای کاربری دستگاه را پنهان کنید. اگر حرکت‌های ناوبری دستگاه در لبه‌های پرده آزارتان می‌دهد، از این استفاده کنید. برای پرده‌های بازی و معماباران (Puzzle Storm) کاربرد دارد.", - "mobileNotFollowingAnyUser": "شما هیچ کاربری را نمی‌دنبالید.", "mobileAllGames": "همه بازی‌ها", - "mobileRecentSearches": "جستجوهای اخیر", - "mobileClearButton": "پاکسازی", - "mobilePlayersMatchingSearchTerm": "بازیکنانِ «{param}»", - "mobileNoSearchResults": "بدون نتیجه", "mobileAreYouSure": "مطمئنید؟", - "mobilePuzzleStreakAbortWarning": "شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.", + "mobileBlindfoldMode": "چشم‌بسته", + "mobileCancelTakebackOffer": "رد درخواست برگرداندن", + "mobileClearButton": "پاکسازی", + "mobileCorrespondenceClearSavedMove": "پاک کردن حرکت ذخیره شده", + "mobileCustomGameJoinAGame": "به بازی بپیوندید", + "mobileFeedbackButton": "بازخورد", + "mobileGreeting": "درود، {param}", + "mobileGreetingWithoutName": "درود", + "mobileHideVariation": "پنهانیدن وَرتِش", + "mobileHomeTab": "خانه", + "mobileLiveStreamers": "بَرخَط-محتواسازان زنده", + "mobileMustBeLoggedIn": "برای دیدن این برگه باید وارد شده باشید.", + "mobileNoSearchResults": "بدون پیامد", + "mobileNotFollowingAnyUser": "شما هیچ کاربری را نمی‌دنبالید.", + "mobileOkButton": "باشه", + "mobilePlayersMatchingSearchTerm": "کاربران با پیوند «{param}»", + "mobilePrefMagnifyDraggedPiece": "بزرگ‌نمودن مهره‌ی کشیده", + "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را به پایان برسانید؟", + "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.", "mobilePuzzleStormNothingToShow": "چیزی برای نمایش نیست، چند دور معماباران بازی کنید.", - "mobileSharePuzzle": "همرسانی این معما", - "mobileShareGameURL": "همرسانی وب‌نشانی بازی", + "mobilePuzzleStormSubtitle": "هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.", + "mobilePuzzleStreakAbortWarning": "شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.", + "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.", + "mobilePuzzlesTab": "معماها", + "mobileRecentSearches": "واپسین جستجوها", + "mobileSettingsHapticFeedback": "بازخورد لمسی", + "mobileSettingsImmersiveMode": "حالت فراگیر", + "mobileSettingsImmersiveModeSubtitle": "رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.", + "mobileSettingsTab": "تنظیمات", "mobileShareGamePGN": "همرسانی PGN", + "mobileShareGameURL": "همرسانی وب‌نشانی بازی", "mobileSharePositionAsFEN": "همرسانی وضعیت، به شکل FEN", - "mobileShowVariations": "باز کردن شاخه‌ها", - "mobileHideVariation": "بستن شاخه‌ها", - "mobileShowComments": "نمایش نظرها", - "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را پایان دهید؟", - "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، لطفا پالاب‌گرها را تغییر دهید", - "mobileCancelTakebackOffer": "رد درخواست برگرداندن", - "mobileCancelDrawOffer": "رد پیشنهاد تساوی", - "mobileWaitingForOpponentToJoin": "در انتظار آمدن حریف...", - "mobileBlindfoldMode": "چشم‌بسته", - "mobileLiveStreamers": "بَرخَط-محتواسازان زنده", - "mobileCustomGameJoinAGame": "به بازی بپیوندید", - "mobileCorrespondenceClearSavedMove": "پاکیدن حرکت ذخیره‌شده", + "mobileSharePuzzle": "همرسانی این معما", + "mobileShowComments": "نمایش دیدگاه‌ها", + "mobileShowResult": "نمایش پیامد", + "mobileShowVariations": "نمایش وَرتِش", "mobileSomethingWentWrong": "مشکلی پیش آمد.", - "mobileShowResult": "نمایش نتیجه", - "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا موضوعی را برگزینید.", - "mobilePuzzleStormSubtitle": "در ۳ دقیقه، هر چندتا معما که می‌توانید، حل کنید.", - "mobileGreeting": "درود، {param}", - "mobileGreetingWithoutName": "سلام", + "mobileSystemColors": "رنگ‌های دستگاه", + "mobileTheme": "پوسته", + "mobileToolsTab": "ابزارها", + "mobileWaitingForOpponentToJoin": "در انتظار آمدن حریف...", + "mobileWatchTab": "تماشا", "activityActivity": "فعالیت", "activityHostedALiveStream": "میزبان پخش زنده بود", "activityRankedInSwissTournament": "رتبه #{param1} را در {param2} به دست آورد", - "activitySignedUp": "در لیچس ثبت نام کرد", + "activitySignedUp": "در lichess.org نام‌نوشت", "activitySupportedNbMonths": "{count, plural, =1{به عنوان {param2} برای {count} ماه از lichess.org حمایت کرد} other{به عنوان {param2} برای {count} ماه از lichess.org حمایت کرد}}", "activityPracticedNbPositions": "{count, plural, =1{{count} وضعیت تمرین‌شده در {param2}} other{{count} وضعیت تمرین‌شده در {param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{{count} معمای آموزشی را حل کرد} other{{count} مساله تاکتیکی را حل کرد}}", - "activityPlayedNbGames": "{count, plural, =1{{count} بازی {param2} را انجام داد} other{{count} بازی {param2} را انجام داد}}", - "activityPostedNbMessages": "{count, plural, =1{{count} پیام را در {param2} ارسال کرد} other{{count} پیام را در {param2} ارسال کرد}}", + "activityPlayedNbGames": "{count, plural, =1{{count} بازی {param2} کرد} other{{count} بازی {param2} کرد}}", + "activityPostedNbMessages": "{count, plural, =1{{count} پیام در {param2} فرستاد} other{{count} پیام در {param2} فرستاد}}", "activityPlayedNbMoves": "{count, plural, =1{{count} حرکت انجام داد} other{{count} حرکت انجام داد}}", "activityInNbCorrespondenceGames": "{count, plural, =1{در {count} بازی مکاتبه‌ای} other{در {count} بازی مکاتبه‌ای}}", "activityCompletedNbGames": "{count, plural, =1{{count} بازی مکاتبه‌ای را به پایان رساند} other{{count} بازی مکاتبه‌ای را به پایان رساند}}", - "activityFollowedNbPlayers": "{count, plural, =1{{count} بازیکن را دنبال کرد} other{{count} بازیکن را دنبال کرد}}", - "activityGainedNbFollowers": "{count, plural, =1{{count} دنبال کننده جدید به دست آورد} other{{count} دنبال کننده جدید به دست آورد}}", + "activityCompletedNbVariantGames": "{count, plural, =1{تکمیل {count} بازی مکاتبه‌ای {param2}} other{تکمیل {count} بازی مکاتبه‌ای {param2}}}", + "activityFollowedNbPlayers": "{count, plural, =1{شروع به دنبالیدن {count} بازیکن کرد} other{شروع به دنبالیدن {count} بازیکن کرد}}", + "activityGainedNbFollowers": "{count, plural, =1{{count} دنبال‌گر جدید به‌دست آورد} other{{count} دنبال‌گر جدید به‌دست آورد}}", "activityHostedNbSimuls": "{count, plural, =1{{count} مسابقه هم‌زمان برگزار کرد} other{{count} مسابقه هم‌زمان برگزار کرد}}", "activityJoinedNbSimuls": "{count, plural, =1{در {count} مسابقه هم‌زمان شرکت کرد} other{در {count} مسابقه هم‌زمان شرکت کرد}}", "activityCreatedNbStudies": "{count, plural, =1{{count} درس جدید ساخت} other{{count} درس جدید ساخت}}", - "activityCompetedInNbTournaments": "{count, plural, =1{در {count} مسابقه آرنا رقابت کرد} other{در {count} مسابقه آرنا رقابت کرد}}", + "activityCompetedInNbTournaments": "{count, plural, =1{در {count} مسابقهٔ راوان رقابت کرد} other{در {count} مسابقهٔ راوان رقابت کرد}}", "activityRankedInTournament": "{count, plural, =1{رتبه #{count} ({param2}% برتر) با {param3} بازی در {param4}} other{رتبه #{count} ({param2}% برتر) با {param3} بازی در {param4}}}", "activityCompetedInNbSwissTournaments": "{count, plural, =1{در {count} مسابقه سوئیسی رقابت کرد} other{در {count} مسابقه سوئیسی رقابت کرد}}", "activityJoinedNbTeams": "{count, plural, =1{به {count} تیم پیوست} other{به {count} تیم پیوست}}", "broadcastBroadcasts": "پخش همگانی", + "broadcastMyBroadcasts": "پخش همگانی من", "broadcastLiveBroadcasts": "پخش زنده مسابقات", + "broadcastBroadcastCalendar": "تقویم پخش", + "broadcastNewBroadcast": "پخش زنده جدید", + "broadcastSubscribedBroadcasts": "پخش‌های دنبالیده", + "broadcastAboutBroadcasts": "درباره پخش‌های همگانی", + "broadcastHowToUseLichessBroadcasts": "نحوه استفاده از پخش همگانی Lichess.", + "broadcastTheNewRoundHelp": "دور جدید، همان اعضا و مشارکت‌کنندگان دور قبلی را خواهد داشت.", + "broadcastAddRound": "اضافه کردن یک دور", + "broadcastOngoing": "ادامه‌دار", + "broadcastUpcoming": "آینده", + "broadcastCompleted": "کامل‌شده", + "broadcastCompletedHelp": "Lichess تکمیل دور را شناسایی می‌کند، اما می‌تواند آن را اشتباه بگیرد. از این کلید برای تنظیم دستی بهرایید.", + "broadcastRoundName": "نام دور", + "broadcastRoundNumber": "شماره دور", + "broadcastTournamentName": "نام مسابقات", + "broadcastTournamentDescription": "توضیحات کوتاه مسابقات", + "broadcastFullDescription": "توضیحات کامل مسابقات", + "broadcastFullDescriptionHelp": "توضیحات بلند و اختیاری پخش همگانی. {param1} قابل‌استفاده است. طول متن باید کمتر از {param2} نویسه باشد.", + "broadcastSourceSingleUrl": "وب‌نشانیِ PGN", + "broadcastSourceUrlHelp": "وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.", + "broadcastSourceGameIds": "تا ۶۴ شناسهٔ بازی Lichess، جداشده با فاصله.", + "broadcastStartDateTimeZone": "تاریخ آغاز در زمان-یانه محلی مسابقات: {param}", + "broadcastStartDateHelp": "اختیاری است، اگر می‌دانید چه زمانی رویداد شروع می‌شود", + "broadcastCurrentGameUrl": "نشانی بازی کنونی", + "broadcastDownloadAllRounds": "بارگیری همه دورها", + "broadcastResetRound": "ازنوکردن این دور", + "broadcastDeleteRound": "حذف این دور", + "broadcastDefinitivelyDeleteRound": "این دور و همه بازی‌هایش را به طور کامل حذف کن.", + "broadcastDeleteAllGamesOfThisRound": "همه بازی‌های این دور را حذف کن. منبع باید فعال باشد تا بتوان آنها را بازساخت.", + "broadcastEditRoundStudy": "ویرایش مطالعه دور", + "broadcastDeleteTournament": "حذف این مسابقات", + "broadcastDefinitivelyDeleteTournament": "کل مسابقات، شامل همه دورها و بازی‌هایش را به طور کامل حذف کن.", + "broadcastShowScores": "نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها", + "broadcastReplacePlayerTags": "اختیاری: عوض کردن نام، درجه‌بندی و عنوان بازیکنان", + "broadcastFideFederations": "کشورگان‌های فیده", + "broadcastTop10Rating": "ده درجه‌بندی برتر", + "broadcastFidePlayers": "بازیکنان فیده", + "broadcastFidePlayerNotFound": "بازیکن فیده پیدا نشد", + "broadcastFideProfile": "رُخ‌نمای فیده", + "broadcastFederation": "کشورگان", + "broadcastAgeThisYear": "سنِ امسال", + "broadcastUnrated": "بی‌درجه‌بندی", + "broadcastRecentTournaments": "مسابقاتِ اخیر", + "broadcastOpenLichess": "آزاد در Lichess", + "broadcastTeams": "یَران‌ها", + "broadcastBoards": "میز‌ها", + "broadcastOverview": "نمای کلی", + "broadcastSubscribeTitle": "مشترک شوید تا از آغاز هر دور باخبر شوید. می‌توانید اعلان‌های زنگی یا رانشی برای پخش‌های زنده را در تنظیمات حساب‌تان تغییر دهید.", + "broadcastUploadImage": "بارگذاری تصویر مسابقات", + "broadcastNoBoardsYet": "تاکنون هیچی. وقتی بازی‌ها بارگذاری شدند، میزها پدیدار خواهند شد.", + "broadcastBoardsCanBeLoaded": "میزها را می‌توان از یک منبع یا از راه {param} بارگذاری کرد", + "broadcastStartVerySoon": "پخش زنده به زودی آغاز خواهد شد.", + "broadcastNotYetStarted": "پخش زنده هنوز نیاغازیده است.", + "broadcastOfficialWebsite": "وبگاه رسمی", + "broadcastStandings": "رده‌بندی", + "broadcastOfficialStandings": "رده‌بندی رسمی", + "broadcastIframeHelp": "گزینه‌های بیشتر در {param}", + "broadcastWebmastersPage": "صفحهٔ وبداران", + "broadcastPgnSourceHelp": "یک منبع عمومی و بی‌درنگ PGN برای این دور. ما همچنین {param} را برای همگامِش تندتر و کارآمدتر پیشنهاد می‌دهیم.", + "broadcastEmbedThisBroadcast": "جاسازی این پخش زنده در وبگاه‌تان", + "broadcastEmbedThisRound": "جاسازی {param} در وبگاه‌تان", + "broadcastRatingDiff": "اختلاف درجه‌بندی", + "broadcastGamesThisTournament": "بازی‌های این مسابقات", + "broadcastScore": "امتیاز", + "broadcastAllTeams": "همهٔ یَران‌ها", + "broadcastTournamentFormat": "ساختار مسابقات", + "broadcastTournamentLocation": "مکان مسابقات", + "broadcastTopPlayers": "بازیکنان برتر", + "broadcastTimezone": "زمان-یانه", + "broadcastFideRatingCategory": "رسته‌بندی درجه‌بندی فیده", + "broadcastOptionalDetails": "جزئیات اختیاری", + "broadcastPastBroadcasts": "پخش‌های گذشته", + "broadcastAllBroadcastsByMonth": "دیدن پخش‌های هر ماه", + "broadcastNbBroadcasts": "{count, plural, =1{{count} پخش همگانی} other{{count} پخش همگانی}}", "challengeChallengesX": "پیشنهاد بازی: {param1}", "challengeChallengeToPlay": "پیشنهاد بازی دادن", "challengeChallengeDeclined": "پیشنهاد بازی رد شد.", @@ -74,7 +150,7 @@ "challengeYouCannotChallengeX": "شما نمی‌توانید به {param} پیشنهاد بازی دهید.", "challengeXDoesNotAcceptChallenges": "{param} پیشنهاد بازی را نپذیرفت.", "challengeYourXRatingIsTooFarFromY": "درجه‌بندی {param1} شما با {param2} اختلاف زیادی دارد.", - "challengeCannotChallengeDueToProvisionalXRating": "به‌خاطر داشتن درجه‌بندی {param} موقت، نمی‌توانید پیشنهاد بازی دهید.", + "challengeCannotChallengeDueToProvisionalXRating": "به‌خاطر درجه‌بندی {param} موقت، نمی‌توانید پیشنهاد بازی دهید.", "challengeXOnlyAcceptsChallengesFromFriends": "{param} فقط پیشنهاد بازی از دوستانش را می‌پذیرد.", "challengeDeclineGeneric": "من فعلا پیشنهاد بازی نمی‌پذیرم.", "challengeDeclineLater": "الان زمان مناسبی برای من نیست، لطفا بعدا دوباره درخواست دهید.", @@ -83,8 +159,8 @@ "challengeDeclineTimeControl": "من با این زمان‌بندی، پیشنهاد بازی را نمی‌پذیرم.", "challengeDeclineRated": "لطفا به جایش، پیشنهاد بازی رسمی بده.", "challengeDeclineCasual": "لطفا به جایش، پیشنهاد بازی نارسمی بده.", - "challengeDeclineStandard": "الان پیشنهاد بازی‌های شطرنج‌گونه را نمی‌پذیرم.", - "challengeDeclineVariant": "الان مایل نیستم این شطرنج‌گونه را بازی کنم.", + "challengeDeclineStandard": "اکنون پیشنهاد بازی‌های وَرتا را نمی‌پذیرم.", + "challengeDeclineVariant": "اکنون مایل نیستم این وَرتا را بازی کنم.", "challengeDeclineNoBot": "من پیشنهاد بازی از ربات‌ها را نمی‌پذیرم.", "challengeDeclineOnlyBot": "من فقط پیشنهاد بازی از ربات‌ها را می‌پذیرم.", "challengeInviteLichessUser": "یا یک کاربر Lichess را دعوت کنید:", @@ -92,13 +168,13 @@ "contactContactLichess": "ارتباط با Lichess", "patronDonate": "کمک مالی", "patronLichessPatron": "یاورِ Lichess", - "perfStatPerfStats": "وضعیت {param}", - "perfStatViewTheGames": "بازی ها را تماشا کنید", + "perfStatPerfStats": "آمار {param}", + "perfStatViewTheGames": "دیدن بازی‌ها", "perfStatProvisional": "موقت", "perfStatNotEnoughRatedGames": "بازی های رسمی کافی برای تعیین کردن یک درجه‌بندی قابل‌اتکا انجام نشده است.", "perfStatProgressOverLastXGames": "پیشرفت در آخرین {param} بازی ها:", "perfStatRatingDeviation": "انحراف درجه‌بندی: {param}.", - "perfStatRatingDeviationTooltip": "مقدار کمتر به این معنی است که درجه‌بندی پایدارتر است. بالاتر از {param1}، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرار گرفتن در درجه‌بندی‌ها، این مقدار باید کم‌تر از {param2} (در شطرنج استاندارد) یا {param3} (در شطرنج‌گونه‌ها) باشد.", + "perfStatRatingDeviationTooltip": "مقدار کمتر به معنای درجه‌بندی پایدارتر است. بالاتر از {param1}، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرارگیری در درجه‌بندی‌ها، این مقدار باید کم‌تر از {param2} (در شطرنج استاندارد) یا {param3} (در وَرتاها) باشد.", "perfStatTotalGames": "تمام بازی ها", "perfStatRatedGames": "بازی های رسمی", "perfStatTournamentGames": "بازی های مسابقه ای", @@ -124,13 +200,13 @@ "preferencesPreferences": "تنظیمات", "preferencesDisplay": "صفحه نمایش", "preferencesPrivacy": "امنیت و حریم شخصی", - "preferencesNotifications": "اعلانات", + "preferencesNotifications": "اعلان", "preferencesPieceAnimation": "حرکت مهره ها", "preferencesMaterialDifference": "تفاوت مُهره‌ها", "preferencesBoardHighlights": "رنگ‌نمایی صفحه (آخرین حرکت و کیش)", "preferencesPieceDestinations": "مقصد مهره(حرکت معتبر و پیش حرکت )", "preferencesBoardCoordinates": "مختصات صفحه(A-H، 1-8)", - "preferencesMoveListWhilePlaying": "لیست حرکات هنگام بازی کردن", + "preferencesMoveListWhilePlaying": "فهرست حرکت هنگام بازی کردن", "preferencesPgnPieceNotation": "نشانه‌گذاری حرکات", "preferencesChessPieceSymbol": "نماد مهره", "preferencesPgnLetter": "حرف (K, Q, R, B, N)", @@ -152,8 +228,8 @@ "preferencesClickTwoSquares": "انتخاب دو مربع مبدا و مقصد", "preferencesDragPiece": "کشیدن یک مهره", "preferencesBothClicksAndDrag": "هر دو", - "preferencesPremovesPlayingDuringOpponentTurn": "پیش حرکت (بازی در نوبت حریف)", - "preferencesTakebacksWithOpponentApproval": "پس گرفتن حرکت (با تایید حریف)", + "preferencesPremovesPlayingDuringOpponentTurn": "پیش‌حرکت (بازی در نوبت حریف)", + "preferencesTakebacksWithOpponentApproval": "برگردان (با تایید حریف)", "preferencesInCasualGamesOnly": "فقط در بازی‌های نارسمی", "preferencesPromoteToQueenAutomatically": "ارتقا خودکار به وزیر", "preferencesExplainPromoteToQueenAutomatically": " را در هنگام تبلیغ بزنید تا به طور موقت تبلیغات خودکار را غیرفعال کنید", @@ -173,23 +249,24 @@ "preferencesSnapArrowsToValidMoves": "چسبیدن پیکان‌ها به حرکت‌های ممکن", "preferencesSayGgWpAfterLosingOrDrawing": "گفتن \"بازی خوبی بود، خوب بازی کردی\" در هنگام باخت یا تساوی", "preferencesYourPreferencesHaveBeenSaved": "تغییرات شما ذخیره شده است", - "preferencesScrollOnTheBoardToReplayMoves": "اسکرول کردن روی صفحه برای مشاهده مجدد حرکت‌ها", - "preferencesCorrespondenceEmailNotification": "ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند", - "preferencesNotifyStreamStart": "استریمر شروع به فعالیت کرد", + "preferencesScrollOnTheBoardToReplayMoves": "برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید", + "preferencesCorrespondenceEmailNotification": "فهرست رایانامهٔ روزانه از بازی‌های مکاتبه‌ای‌تان", + "preferencesNotifyStreamStart": "بَرخَط-محتواساز روی پخش است", "preferencesNotifyInboxMsg": "پیام جدید", - "preferencesNotifyForumMention": "در کامنتی نام شما ذکر شده است", + "preferencesNotifyForumMention": "در انجمن از شما نام‌بُرده‌اند", "preferencesNotifyInvitedStudy": "دعوت به مطالعه", - "preferencesNotifyGameEvent": "اعلان به روزرسانی بازی", + "preferencesNotifyGameEvent": "به‌روزرسانی‌های بازی مکاتبه‌ای", "preferencesNotifyChallenge": "پیشنهاد بازی", - "preferencesNotifyTournamentSoon": "تورنمت به زودی آغاز می شود", + "preferencesNotifyTournamentSoon": "مسابقات به‌زودی می‌آغازد", "preferencesNotifyTimeAlarm": "هشدار تنگی زمان", - "preferencesNotifyBell": "زنگوله اعلانات لیچس", - "preferencesNotifyPush": "اعلانات برای زمانی که شما در لیچس نیستید", + "preferencesNotifyBell": "اعلان زنگی در Lichess", + "preferencesNotifyPush": "اعلان اَفزاره، هنگامی که در Lichess نیستید", "preferencesNotifyWeb": "مرورگر", - "preferencesNotifyDevice": "دستگاه", - "preferencesBellNotificationSound": "زنگ اعلان", + "preferencesNotifyDevice": "اَفزاره", + "preferencesBellNotificationSound": "صدای اعلان زنگی", + "preferencesBlindfold": "چشم‌بسته", "puzzlePuzzles": "معماها", - "puzzlePuzzleThemes": "موضوع معماها", + "puzzlePuzzleThemes": "موضوع معما", "puzzleRecommended": "توصیه شده", "puzzlePhases": "مرحله‌ها", "puzzleMotifs": "موضوعات", @@ -222,7 +299,7 @@ "puzzleUseFindInPage": "از گزینه «جستجو در صفحه» مرورگر استفاده کنید تا گشایش دلخواه‌تان را بیابید!", "puzzleUseCtrlF": "از Ctrl+f برای یابیدن گشایش دلخواه‌تان استفاده کنید!", "puzzleNotTheMove": "این حرکت نیست!", - "puzzleTrySomethingElse": "چیز دیگری پیدا کنید", + "puzzleTrySomethingElse": "چیز دیگری بیابید.", "puzzleRatingX": "درجه‌بندی: {param}", "puzzleHidden": "پنهان", "puzzleFromGameLink": "برگرفته از بازی {param}", @@ -237,10 +314,10 @@ "puzzleAddAnotherTheme": "افزودن موضوعی دیگر", "puzzleNextPuzzle": "معمای بعدی", "puzzleJumpToNextPuzzleImmediately": "فوراً به معمای بعدی بروید", - "puzzlePuzzleDashboard": "پیشخوان معماها", + "puzzlePuzzleDashboard": "پیشخوان معما", "puzzleImprovementAreas": "نقاط ضعف", "puzzleStrengths": "نقاط قوت", - "puzzleHistory": "پیشینه معماها", + "puzzleHistory": "پیشینهٔ معما", "puzzleSolved": "حل شده", "puzzleFailed": "شکست!", "puzzleStreakDescription": "به تدریج معماهای سخت‌تری را حل کنید و یک دنباله بُرد بسازید. محدویت زمانی وجود ندارد، پس عجله نکنید. با یک حرکت اشتباه، بازی تمام می‌شود! در هر دور، می‌توانید یک حرکت را رَد کنید.", @@ -251,13 +328,13 @@ "puzzleFromMyGames": "از بازی های من", "puzzleLookupOfPlayer": "به دنبال معماهای برگرفته از بازی‌های یک بازیکن مشخص، بگردید", "puzzleFromXGames": "معماهای برگرفته از بازی‌های {param}", - "puzzleSearchPuzzles": "جستجوی معماها", - "puzzleFromMyGamesNone": "شما هیچ معمایی در پایگاه‌داده ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بَختِتان را برای افزودن معمایی از خودتان بیفزایید!", + "puzzleSearchPuzzles": "جستجوی معما", + "puzzleFromMyGamesNone": "شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و فکری را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!", "puzzleFromXGamesFound": "{param1} معما در بازی‌های {param2} یافت شد", - "puzzlePuzzleDashboardDescription": "تمرین کن، تحلیل کن، پیشرفت کن", + "puzzlePuzzleDashboardDescription": "آموزش، واکاوی، بهبود", "puzzlePercentSolved": "{param} حل‌شده", "puzzleNoPuzzlesToShow": "چیزی برای نمایش نیست، نخست بروید و چند معما حل کنید!", - "puzzleImprovementAreasDescription": "این‌ها را تمرین کنید تا روند پیشرفت خود را بهبود ببخشید!", + "puzzleImprovementAreasDescription": "برای بهینیدن پیشرفت‌تان، این‌ها را بیاموزید!", "puzzleStrengthDescription": "شما در این زمینه‌ها بهترین عملکرد را دارید", "puzzlePlayedXTimes": "{count, plural, =1{{count} بار بازی شده است} other{{count} بار بازی شده}}", "puzzleNbPointsBelowYourPuzzleRating": "{count, plural, =1{یک امتیاز زیر درجه‌بندی معمایی‌تان} other{{count} امتیاز زیر درجه‌بندی معمایی‌تان}}", @@ -293,7 +370,7 @@ "puzzleThemeDovetailMate": "مات بوسه ای", "puzzleThemeDovetailMateDescription": "وزیری که شاه مجاور خودش را که تنها دو خانه ای که برای فرارش باقی مانده توسط مهره های خودش سد شده، مات می کند.", "puzzleThemeEquality": "برابری", - "puzzleThemeEqualityDescription": "از وضعیت باخت در‌آیید و به وضعیت تساوی یا ترازمند برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)", + "puzzleThemeEqualityDescription": "از وضعیت باخت در‌آیید و به وضعیت تساوی یا تعادل برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)", "puzzleThemeKingsideAttack": "حمله به جناح شاه", "puzzleThemeKingsideAttackDescription": "حمله به شاه حریف، پس از آنکه آنها قلعه کوچک رفتند.", "puzzleThemeClearance": "آزادسازی", @@ -375,26 +452,26 @@ "puzzleThemeSuperGMDescription": "معماهای برگرفته از بازی‌های بهترین بازیکنان جهان.", "puzzleThemeTrappedPiece": "مهره به‌دام‌افتاده", "puzzleThemeTrappedPieceDescription": "یک مهره قادر به فرار کردن از زده شدن نیست چون حرکات محدودی دارد.", - "puzzleThemeUnderPromotion": "فرو-ارتقا", + "puzzleThemeUnderPromotion": "کم‌ارتقا", "puzzleThemeUnderPromotionDescription": "ارتقا به اسب، فیل یا رخ.", "puzzleThemeVeryLong": "معمای خیلی طولانی", - "puzzleThemeVeryLongDescription": "چهار حرکت یا بیشتر برای برنده شدن.", + "puzzleThemeVeryLongDescription": "بُردن با چهار حرکت یا بیشتر.", "puzzleThemeXRayAttack": "حمله پیکانی", "puzzleThemeXRayAttackDescription": "یک مهره از طریق مهره حریف به یک خانه حمله میکند یا از آن دفاع می کند.", "puzzleThemeZugzwang": "زوگزوانگ", "puzzleThemeZugzwangDescription": "حریف در حرکت‌هایش محدود است و همه‌شان وضعیتش را بدتر می‌کند.", - "puzzleThemeHealthyMix": "ترکیب سالم", - "puzzleThemeHealthyMixDescription": "یک ذره از همه چیز. شما نمی دانید چه چیزی پیش روی شماست، بنابراین شما باید برای هر چیزی آماده باشید! دقیقا مثل بازی های واقعی.", + "puzzleThemeMix": "آمیزهٔ همگن", + "puzzleThemeMixDescription": "کمی از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.", "puzzleThemePlayerGames": "بازی‌های بازیکن", "puzzleThemePlayerGamesDescription": "دنبال معماهای ایجادشده از بازی‌های خودتان یا بازی‌های سایر بازیکنان، بگردید.", "puzzleThemePuzzleDownloadInformation": "این معماها به صورت همگانی هستند و می‌توانید از {param} بارگیریدشان.", "searchSearch": "جستجو", "settingsSettings": "تنظیمات", "settingsCloseAccount": "بستن حساب", - "settingsManagedAccountCannotBeClosed": "اکانت شما مدیریت شده است و نمی تواند بسته شود.", + "settingsManagedAccountCannotBeClosed": "حساب‌تان مدیریت می‌شود و نمی‌توان آن را بست.", "settingsClosingIsDefinitive": "بعد از بستن حسابتان دیگر نمی توانید به آن دسترسی پیدا کنید. آیا مطمئن هستید؟", "settingsCantOpenSimilarAccount": "شما نمی توانید حساب جدیدی با این نام کاربری باز کنید، حتی اگر با دستگاه دیگری وارد شوید.", - "settingsChangedMindDoNotCloseAccount": "نظرم را عوض کردم اکانتم را نمی بندم", + "settingsChangedMindDoNotCloseAccount": "نظرم عوض شد، حسابم را نبند", "settingsCloseAccountExplanation": "آیا مطمئنید که می خواهید حساب خود را ببندید؟ بستن حساب یک تصمیم دائمی است. شما هرگز نمی توانید دوباره وارد حساب خود شوید.", "settingsThisAccountIsClosed": "این حساب بسته شده است", "playWithAFriend": "بازی با دوستان", @@ -402,7 +479,7 @@ "toInviteSomeoneToPlayGiveThisUrl": "برای دعوت کردن حریف این لینک را برای او بفرستید", "gameOver": "پایان بازی", "waitingForOpponent": "انتطار برای حریف", - "orLetYourOpponentScanQrCode": "یا اجازه دهید حریف شما این QR کد را پویش کند", + "orLetYourOpponentScanQrCode": "یا اجازه دهید حریف‌تان این کد QR را بِروبینَد", "waiting": "در حال انتظار", "yourTurn": "نوبت شماست", "aiNameLevelAiLevel": "{param1} سطح {param2}", @@ -426,9 +503,9 @@ "itsYourTurn": "نوبت شماست!", "cheatDetected": "تقلب تشخیص داده شد", "kingInTheCenter": "شاه روی تپه", - "threeChecks": "سه کیش", + "threeChecks": "سه‌کیش", "raceFinished": "مسابقه تمام شد", - "variantEnding": "پایان شطرنج‌گونه", + "variantEnding": "پایان وَرتا", "newOpponent": "حریف جدید", "yourOpponentWantsToPlayANewGameWithYou": "حریف شما می خواهد که دوباره با شما بازی کند", "joinTheGame": "به بازی بپیوندید", @@ -443,8 +520,8 @@ "blackResigned": "سیاه تسلیم شد", "whiteLeftTheGame": "سفید بازی را ترک کرد", "blackLeftTheGame": "سیاه بازی را ترک کرد", - "whiteDidntMove": "سفید تکان نخورد", - "blackDidntMove": "مشکی تکان نخورد", + "whiteDidntMove": "سفید بازی نکرد", + "blackDidntMove": "سیاه بازی نکرد", "requestAComputerAnalysis": "درخواست تحلیل رایانه‌ای", "computerAnalysis": "تحليل رایانه‌ای", "computerAnalysisAvailable": "تحلیل رایانه‌ای موجود است", @@ -452,29 +529,29 @@ "analysis": "تحلیل بازی", "depthX": "عمق {param}", "usingServerAnalysis": "با استفاده از کارسازِ تحلیل", - "loadingEngine": "پردازشگر بارمی‌گذارد...", - "calculatingMoves": "در حال محاسبه حرکات...", - "engineFailed": "خطا در بارگذاری پردازشگر شطرنج", + "loadingEngine": "موتور بارمی‌گذارد...", + "calculatingMoves": "محاسبهٔ حرکت‌ها...", + "engineFailed": "خطا در بارگذاری موتور", "cloudAnalysis": "تحلیل ابری", "goDeeper": "بررسی عمیق‌تر", "showThreat": "نمایش تهدید", "inLocalBrowser": "در مرورگر محلی", "toggleLocalEvaluation": "کلید ارزیابی محلی", - "promoteVariation": "افزایش عمق شاخه اصلی", + "promoteVariation": "ارتقای وَرتِش", "makeMainLine": "خط کنونی را به خط اصلی تبدیل کنید", "deleteFromHere": "از اینجا به بعد را پاک کنید", - "collapseVariations": "بستن شاخه‌ها", - "expandVariations": "باز کردن شاخه‌ها", - "forceVariation": "نتیجه تحلیل را به عنوان یکی از تنوعهای بازی انتخاب نمایید", - "copyVariationPgn": "کپی PGN این شاخه", + "collapseVariations": "بستن وَرتِش‌ها", + "expandVariations": "گستردنِ وَرتِش‌ها", + "forceVariation": "وَرتِشِ زوری", + "copyVariationPgn": "رونوشت‌گیری PGN ِ وَرتِش", "move": "حرکت", - "variantLoss": "حرکت بازنده", - "variantWin": "حرکت برنده", + "variantLoss": "باختِ وَرتا", + "variantWin": "بُردِ وَرتا", "insufficientMaterial": "مُهره ناکافی برای مات", "pawnMove": "حرکت پیاده", "capture": "گرفتن مهره", "close": "بستن", - "winning": "حرکت پیروزی‌بخش", + "winning": "حرکت برنده", "losing": "حرکت بازنده", "drawn": "حرکت تساوی‌دهنده", "unknown": "ناشناخته", @@ -487,13 +564,13 @@ "dtzWithRounding": "DTZ50'' با گرد کردن، بر اساس تعداد حرکات نیمه تا زمان دستگیری یا حرکت پیاده بعدی", "noGameFound": "هیچ بازی یافت نشد", "maxDepthReached": "عمق به حداکثر رسیده!", - "maybeIncludeMoreGamesFromThePreferencesMenu": "ممکنه بازی‌های بیشتری با توجه به گزینگان ترجیح‌ها، وجود داشته باشد؟", + "maybeIncludeMoreGamesFromThePreferencesMenu": "شاید بازی‌های بیشتری با توجه به نام‌چین تنظیمات، وجود داشته باشد.", "openings": "گشایش‌ها", "openingExplorer": "پویشگر گشایش‌", "openingEndgameExplorer": "پویشگر گشایش/آخربازی", - "xOpeningExplorer": "جستجوگر گشایش {param}", + "xOpeningExplorer": "پویشگر گشایش {param}", "playFirstOpeningEndgameExplorerMove": "نخستین حرکت گشایش/آخربازی پویشگر را برو", - "winPreventedBy50MoveRule": "قانون پنجاه حرکت از پیروزی جلوگیری کرد", + "winPreventedBy50MoveRule": "قانون پنجاه حرکت جلوی پیروزی را گرفت", "lossSavedBy50MoveRule": "قانون ۵۰ حرکت از شکست جلوگیری کرد", "winOr50MovesByPriorMistake": "برد یا ۵٠ حرکت بعد از اشتباه قبلی", "lossOr50MovesByPriorMistake": "باخت یا ۵٠ حرکت از اشتباه قبلی", @@ -504,50 +581,48 @@ "deleteThisImportedGame": "آیا این بازیِ درونبُرده پاک شود؟", "replayMode": "حالت بازپخش", "realtimeReplay": "مشابه بازی", - "byCPL": "درنگ حین اشتباهات", - "openStudy": "گشودن مطالعه", + "byCPL": "درنگ هنگام اشتباه", "enable": "فعال سازی", - "bestMoveArrow": "فلش نشان دهنده بهترین حرکت", - "showVariationArrows": "نمایش پیکان‌های شاخه اصلی", + "bestMoveArrow": "پیکانِ بهترین حرکت", + "showVariationArrows": "نمایش پیکان‌های وَرتِش", "evaluationGauge": "میله ارزیابی", "multipleLines": "شاخه های متعدد", "cpus": "پردازنده(ها)", "memory": "حافظه", - "infiniteAnalysis": "آنالیز بی پایان", + "infiniteAnalysis": "تحلیل بی‌کران", "removesTheDepthLimit": "محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند", - "engineManager": "مدیر موتور شطرنج", "blunder": "اشتباه فاحش", "mistake": "اشتباه", "inaccuracy": "بی دقتی", "moveTimes": "مدت حركت‌ها", "flipBoard": "چرخاندن صفحه", - "threefoldRepetition": "تکرار سه گانه", + "threefoldRepetition": "تکرار سه‌گانه", "claimADraw": "ادعای تساوی", "offerDraw": "پیشنهاد مساوی", "draw": "مساوی", "drawByMutualAgreement": "تساوی با توافق طرفین", "fiftyMovesWithoutProgress": "قانون ۵۰ حرکت", - "currentGames": "بازی های در جریان", + "currentGames": "بازی‌های جاری", "viewInFullSize": "نمایش در اندازه کامل", "logOut": "خروج", "signIn": "ورود", "rememberMe": "مرا به خاطر بسپار", - "youNeedAnAccountToDoThat": "شما برای انجام این کار به یک حساب کاربری نیاز دارید.", + "youNeedAnAccountToDoThat": "برای انجام آن به یک حساب نیازمندید", "signUp": "نام نویسی", - "computersAreNotAllowedToPlay": "كامپيوتر و بازيكناني كه از كامپيوتر كمك مي گيرند،مجاز به بازی نیستند.لطفا از انجین شطرنج يا دیتابیس شطرنج و يا بازيكنان ديگر كمک نگيريد. همچنین توجه کنید که داشتن چند حساب کاربری به شدت نهی شده است. استفاده فزاینده از چند حساب منجر به مسدود شدن حساب شما خواهد شد.", + "computersAreNotAllowedToPlay": "رایانه ها و بازیکنان رایانه-یاریده، مجاز به بازی نیستند. لطفا هنگام بازی از موتورهای شطرنج، دادگان‌ها یا دیگر بازیکنان کمک نگیرید. همچنین توجه کنید که ساخت چندین حساب به شدت ممنوع است و چند حسابی افزاینده، منجر به بستن‌تان می‌شود.", "games": "بازی ها", "forum": "انجمن", - "xPostedInForumY": "{param1} در انجمن،موضوع {param2} را پست کرد.", - "latestForumPosts": "آخرین پست های انجمن", + "xPostedInForumY": "{param1} در موضوع {param2}، پیامی نوشت", + "latestForumPosts": "آخرین فرسته‌های انجمن", "players": "بازیکنان", "friends": "دوستان", "otherPlayers": "بازیکنان دیگر", "discussions": "مکالمات", "today": "امروز", "yesterday": "دیروز", - "minutesPerSide": "زمان برای هر بازیکن(به دقیقه)", - "variant": "گونه", - "variants": "گونه‌ها", + "minutesPerSide": "هر بازیکن چند دقیقه", + "variant": "وَرتا", + "variants": "وَرتا", "timeControl": "زمان", "realTime": "زمان محدود", "correspondence": "مکاتبه ای", @@ -557,28 +632,28 @@ "rating": "درجه‌بندی", "ratingStats": "آماره‌های درجه‌بندی", "username": "نام کاربری", - "usernameOrEmail": "نام کاربری یا ایمیل", + "usernameOrEmail": "نام کاربری یا رایانامه", "changeUsername": "تغییر نام کاربری", "changeUsernameNotSame": "تنها اندازه حروف میتوانند تغییر کنند. برای مثال \"johndoe\" به \"JohnDoe\".", "changeUsernameDescription": "نام کاربری خود را تغییر دهید. این تنها یک بار انجام پذیر است و شما تنها مجازید اندازه حروف نام کاربری‌تان را تغییر دهید.", "signupUsernameHint": "مطمئن شوید که یک نام کاربری مناسب انتخاب میکنید. بعداً نمی توانید آن را تغییر دهید و هر حسابی با نام کاربری نامناسب بسته می شود!", - "signupEmailHint": "ما فقط از آن برای تنظیم مجدد رمز عبور استفاده خواهیم کرد.", - "password": "رمز عبور", - "changePassword": "تغییر کلمه عبور", + "signupEmailHint": "ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.", + "password": "گذرواژه", + "changePassword": "تغییر گذرواژه", "changeEmail": "تغییر ایمیل", "email": "ایمیل", - "passwordReset": "بازیابی کلمه عبور", - "forgotPassword": "آیا کلمه عبور را فراموش کرده اید؟", - "error_weakPassword": "این رمز به شدت معمول و قابل حدس است.", - "error_namePassword": "لطفا رمز خود را متفاوت از نام کاربری خود انتخاب کنید.", - "blankedPassword": "شما از همین رمز عبور در سایت دیگری استفاده کرده اید و آن سایت در معرض خطر قرار گرفته است. برای اطمینان از ایمنی حساب لیچس خود، لازم است که شما یک رمز عبور جدید ایجاد کنید. ممنون از همکاری شما.", + "passwordReset": "بازنشانی گذرواژه", + "forgotPassword": "گذرواژه را فراموش کرده‌اید؟", + "error_weakPassword": "این گذرواژه بسیار رایج و آسان‌حدس است.", + "error_namePassword": "خواهشانه از نام کاربری‌تان برای گذرواژه‌تان استفاده نکنید.", + "blankedPassword": "شما از گذرواژهٔ یکسانی در وبگاه دیگری بهراییده‌اید و آن وبگاه به خطر افتاده است. برای اطمینان از ایمنی حساب Lichessتان، به شما نیاز داریم تا گذرواژهٔ نویی را تعیین کنید. از درک‌تان سپاسگزاریم.", "youAreLeavingLichess": "در حال ترک lichess هستید", - "neverTypeYourPassword": "هرگز رمز خود را در سایت دیگر وارد نکنید!", + "neverTypeYourPassword": "هرگز گذرواژهٔ Lichessتان را در وبگاه دیگری ننویسید!", "proceedToX": "بروید به {param}", "passwordSuggestion": "از رمز عبور پیشنهاد شده از شخص دیگر استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", "emailSuggestion": "از ایمیلی که از شخص دیگر به شما پیشنهاد داده است استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", "emailConfirmHelp": "کمک با تائید ایمیل", - "emailConfirmNotReceived": "آیا ایمیل تائید بعد از ثبت نام را دریافت نکرده اید؟", + "emailConfirmNotReceived": "آیا رایانامهٔ تاییدتان را پس از نام‌نویسی دریافت نکردید؟", "whatSignupUsername": "از چه نام کاربری برای ثبت نام استفاده کردید؟", "usernameNotFound": "ما هیچ کابری با این نام نیافتیم: {param}.", "usernameCanBeUsedForNewAccount": "شما می توانید از این نام کاربری برای ایجاد یک حساب کاربری جدید استفاده کنید", @@ -596,7 +671,8 @@ "accountRegisteredWithoutEmail": "حساب کاربری {param} بدون ایمیل ثبت نام شده بود.", "rank": "رتبه", "rankX": "رتبه:{param}", - "gamesPlayed": "تعداد بازی های انجام شده", + "gamesPlayed": "بازی‌های انجامیده", + "ok": "بسیار خب", "cancel": "لغو", "whiteTimeOut": "زمان سفید تمام شد", "blackTimeOut": "زمان سیاه تمام شد", @@ -610,8 +686,8 @@ "yourOpponentOffersADraw": "حریف شما پیشنهاد تساوی می دهد", "accept": "پذیرفتن", "decline": "رد کردن", - "playingRightNow": "بازی در حال انجام", - "eventInProgress": "بازی در حال انجام", + "playingRightNow": "هم‌اکنون بازی می‌کنند", + "eventInProgress": "اکنون بازی می‌کنند", "finished": "تمام شده", "abortGame": "انصراف از بازی", "gameAborted": "بازی لغو شد", @@ -637,7 +713,7 @@ "chatRoom": "گپ‌سرا", "loginToChat": "برای گپ زدن، وارد حساب‌تان شوید", "youHaveBeenTimedOut": "زمان شما به پایان رسید.", - "spectatorRoom": "اتاق تماشاچیان", + "spectatorRoom": "اتاق تماشاگران", "composeMessage": "نوشتن پیام", "subject": "عنوان", "send": "ارسال", @@ -647,19 +723,19 @@ "ratingRange": "محدوده درجه‌بندی", "thisAccountViolatedTos": "این حساب قوانین را نقض کرده است", "openingExplorerAndTablebase": "پویشگر گشایش و آخربازی", - "takeback": "پس گرفتن حرکت", - "proposeATakeback": "پیشنهاد پس گرفتن حرکت", - "takebackPropositionSent": "پیشنهاد پس گرفتن حرکت فرستاده شد", - "takebackPropositionDeclined": "پیشنهاد پس گرفتن حرکت رد شد", - "takebackPropositionAccepted": "پیشنهاد پس گرفتن حرکت پذیرفته شد", - "takebackPropositionCanceled": "پیشنهاد پس گرفتن حرکت لغو شد", - "yourOpponentProposesATakeback": "حریف پیشنهاد پس گرفتن حرکت می دهد", - "bookmarkThisGame": "نشان گذاری بازی", + "takeback": "برگردان", + "proposeATakeback": "پیشنهاد برگردان", + "takebackPropositionSent": "برگردان فرستاده شد", + "takebackPropositionDeclined": "برگردان رد شد", + "takebackPropositionAccepted": "برگردان پذیرفته شد", + "takebackPropositionCanceled": "برگردان لغو شد", + "yourOpponentProposesATakeback": "حریف‌تان پیشنهاد «برگرداندن» می‌دهد", + "bookmarkThisGame": "نشانک‌گذاری", "tournament": "مسابقه", "tournaments": "مسابقات", "tournamentPoints": "مجموع امتیازات مسابقات", - "viewTournament": "مشاهده مسابقات", - "backToTournament": "برگشت به مسابقه", + "viewTournament": "دیدن مسابقات", + "backToTournament": "بازگشت به مسابقات", "noDrawBeforeSwissLimit": "شما نمی‌توانید در مسابقات سوییس تا قبل از حرکت ۳۰ام بازی را مساوی کنید.", "thematic": "موضوعی", "yourPerfRatingIsProvisional": "درجه‌بندی {param} شما موقتی است", @@ -671,11 +747,11 @@ "mustBeInTeam": "باید در تیم {param} باشید", "youAreNotInTeam": "شما در تیم {param} نیستید", "backToGame": "بازگشت به بازی", - "siteDescription": "کارساز برخط و رایگان شطرنج. با میانایی روان شطرنج بازی کنید. بدون ثبت‌نام، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.", + "siteDescription": "کارساز برخط و رایگان شطرنج. با میانایی روان، شطرنج بازی کنید. بدون نام‌نویسی، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.", "xJoinedTeamY": "{param1} به تیم {param2} پیوست", "xCreatedTeamY": "{param1} تیم {param2} را ایجاد کرد", - "startedStreaming": "پخش را آغازید", - "xStartedStreaming": "{param} پخش را آغازید", + "startedStreaming": "جریان‌سازی را آغازید", + "xStartedStreaming": "{param} جریان‌سازی را آغازید", "averageElo": "میانگین درجه‌بندی", "location": "محل", "filterGames": "پالابش بازی‌ها", @@ -685,7 +761,7 @@ "leaderboard": "جدول رده‌بندی", "screenshotCurrentPosition": "نماگرفت از وضعیت فعلی", "gameAsGIF": "بارگیری GIF بازی", - "pasteTheFenStringHere": "پوزیشن دلخواه(FEN) را در این قسمت وارد کنید", + "pasteTheFenStringHere": "رشته FEN را در این قسمت قرار دهید", "pasteThePgnStringHere": "متن PGN را در این قسمت وارد کنید", "orUploadPgnFile": "یا یک فایل PGN بارگذاری کنید", "fromPosition": "از وضعیت", @@ -693,7 +769,7 @@ "toStudy": "مطالعه", "importGame": "بارگذاری بازی", "importGameExplanation": "برای دریافت بازپخش مرورپذیر، واکاوی رایانه‌ای، گپ‌های بازی، و وب‌نشانی همگانی همرسانی‌پذیر، PGN یک بازی را جای‌گذاری کنید.", - "importGameCaveat": "تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید.", + "importGameCaveat": "ورتش‌ها پاک خواهند شد. برای حفظشان، PGN را از طریق مطالعه درون‌بَرید.", "importGameDataPrivacyWarning": "این PGN برای عموم در دسترس است، برای وارد کردن یک بازی خصوصی، از *مطالعه* استفاده کنید.", "thisIsAChessCaptcha": "این یک کپچا [کد امنیتی] شطرنجی است", "clickOnTheBoardToMakeYourMove": "روی صفحه بزنید تا حرکت‌تان را بروید و اثبات کنید که انسانید.", @@ -702,19 +778,18 @@ "whiteCheckmatesInOneMove": "سفید در یک حرکت مات می‌کند", "blackCheckmatesInOneMove": "سیاه در یک حرکت مات می‌کند", "retry": "تلاش دوباره", - "reconnecting": "در حال اتصال دوباره", + "reconnecting": "بازاتصال...", "noNetwork": "بُرون‌خط", "favoriteOpponents": "رقبای مورد علاقه", - "follow": "دنبال کردن", - "following": "افرادی که دنبال می کنید", - "unfollow": "لغو دنبال کردن", - "followX": "دنبال کردن {param}", - "unfollowX": "لغو دنبال کردن {param}", + "follow": "دنبالیدن", + "following": "دنبال‌شدگان", + "unfollow": "وادنبالیدن", + "followX": "دنبالیدن {param}", + "unfollowX": "وادنبالیدن {param}", "block": "مسدود کن", "blocked": "مسدود شده", "unblock": "لغو انسداد", - "followsYou": "افرادی که شما را دنبال می کنند", - "xStartedFollowingY": "{param1} {param2} را فالو کرد", + "xStartedFollowingY": "{param1} دنبالیدن {param2} را آغازید", "more": "بیشتر", "memberSince": "عضویت از تاریخ", "lastSeenActive": "آخرین ورود {param}", @@ -725,14 +800,14 @@ "openTournaments": "باز کردن مسابقه", "duration": "مدت", "winner": "برنده", - "standing": "رتبه بندی", - "createANewTournament": "درست کردن یک مسابقه ی جدید", - "tournamentCalendar": "برنامه ی مسابقات", + "standing": "رده‌بندی", + "createANewTournament": "ایجاد یک مسابقهٔ نو", + "tournamentCalendar": "گاهشمار مسابقات", "conditionOfEntry": "شرایط ورود:", "advancedSettings": "تنظیمات پیشرفته", "safeTournamentName": "یک نام بسیار امن برای مسابقات انتخاب کنید.", "inappropriateNameWarning": "هرچیز حتی کمی نامناسب ممکن است باعث بسته شدن حساب کاربری شما بشود.", - "emptyTournamentName": "این مکان را خالی بگذارید تا به صورت تصادفی اسم یک استاد بزرگ برای مسابقات انتخاب شود.", + "emptyTournamentName": "برای نامیدن مسابقات به نام یک شطرنج‌باز برجسته، خالی بگذارید.", "makePrivateTournament": "تورنومنت را به حالت خصوصی در بیاورید و دسترسی را محدود به داشتن پسورد کنید", "join": "ملحق شدن", "withdraw": "منصرف شدن", @@ -745,20 +820,20 @@ "standByX": "حریف {param} است،آماده باشید!", "pause": "توقف", "resume": "ادامه دادن", - "youArePlaying": "شما بازی میکنید!", + "youArePlaying": "شما بازی می‌کنید!", "winRate": "درصد برد", - "berserkRate": "میزان جنون", + "berserkRate": "میزان دیوانگی", "performance": "عملکرد", "tournamentComplete": "مسابقات به پایان رسید", "movesPlayed": "حرکات انجام شده", "whiteWins": "پیروزی با مهره سفید", - "blackWins": "سیاه برنده شد", + "blackWins": "سیاه می‌برد", "drawRate": "نرخ تساوی", "draws": "مساوی", "nextXTournament": "مسابقه ی {param} بعدی:", "averageOpponent": "میانگین امتیاز حریف ها", "boardEditor": "مُهره‌چینی", - "setTheBoard": "مهره‌ها را بچینید", + "setTheBoard": "میز را بچینید", "popularOpenings": "گشایش‌های محبوب", "endgamePositions": "وضعیت‌های آخربازی", "chess960StartPosition": "وضعیت آغازین شطرنج۹۶۰: {param}", @@ -789,14 +864,14 @@ "activePlayers": "بازیکنان فعال", "bewareTheGameIsRatedButHasNoClock": "مراقب باشید،این بازی رتبه بندی میشود اما بدون ساعت!", "success": "موفق شدید", - "automaticallyProceedToNextGameAfterMoving": "حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن", + "automaticallyProceedToNextGameAfterMoving": "پس از حرکت، خودکار به بازی بعدی روید", "autoSwitch": "تعویض خودکار", - "puzzles": "معماها", + "puzzles": "معما", "onlineBots": "ربات‌های بَرخط", "name": "نام", "description": "شرح", "descPrivate": "توضیحات خصوصی", - "descPrivateHelp": "متنی که فقط اعضای تیم مشاهده خواهند کرد. در صورت تعیین، جایگزین توضیحات عمومی برای اعضای تیم خواهد شد.", + "descPrivateHelp": "متنی که فقط هم‌تیمی‌ها خواهند دید. در صورت تعیین، جایگزین وصف همگانی برای هم‌تیمی‌ها می‌شود خواهد شد.", "no": "نه", "yes": "بله", "website": "وبگاه", @@ -804,8 +879,8 @@ "help": "راهنما:", "createANewTopic": "ایجاد یک موضوع جدید", "topics": "مباحث", - "posts": "پست ها", - "lastPost": "آخرین ارسال", + "posts": "فرسته‌ها", + "lastPost": "آخرین فرسته", "views": "نمایش ها", "replies": "پاسخ ها", "replyToThisTopic": "پاسخ به این موضوع", @@ -818,31 +893,33 @@ "whatIsIheMatter": "موضوع", "cheat": "تقلب", "troll": "وِزُل", - "other": "موضوعات دیگر", - "reportDescriptionHelp": "لینک بازی های این کاربر را قرار دهید و توضیع دهید خطای رفتار این بازیکن چه بوده است", + "other": "دیگر", + "reportCheatBoostHelp": "پیوند بازی(ها) را جای‌گذارید و بشرحید که چه رفتاری از این کاربر مشکل دارد. فقط نگویید «آنها تقلب‌کارند»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید.", + "reportUsernameHelp": "بشرحید چه چیز این نام‌کاربری آزارنده است. فقط نگویید «آزارنده/نامناسب است»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید، به‌ویژه اگر توهین: گنگ است، انگلیسی نیست، کوچه‌بازاری است، یا یک ارجاع تاریخی/فرهنگی است.", + "reportProcessedFasterInEnglish": "اگر انگلیسی بنویسید، زودتر به گزارش‌تان رسیدگی خواهد شد.", "error_provideOneCheatedGameLink": "لطفآ حداقل یک نمونه تقلب در بازی را مطرح کنید.", - "by": "توسط {param}", + "by": "به‌دستِ {param}", "importedByX": "{param} آن را وارد کرده", "thisTopicIsNowClosed": "این موضوع بسته شده است", - "blog": "بلاگ", - "notes": "یادداشت ها", - "typePrivateNotesHere": "یادداشت خصوصی را اینجا وارد کنید", + "blog": "وبنوشت", + "notes": "یادداشت‌ها", + "typePrivateNotesHere": "یادداشت‌های خصوصی را اینجا بنویسید", "writeAPrivateNoteAboutThisUser": "یک یادداشت خصوصی درباره این کاربر بنویسید", - "noNoteYet": "تا الان، بدون یادداشت", - "invalidUsernameOrPassword": "نام کاربری یا رمز عبور نادرست است", - "incorrectPassword": "رمزعبور اشتباه", - "invalidAuthenticationCode": "کد اصالت سنجی نامعتبر", + "noNoteYet": "تاکنون، بدون یادداشت", + "invalidUsernameOrPassword": "نام کاربری یا گذرواژهٔ نامعتبر", + "incorrectPassword": "گذرواژهٔ نادرست", + "invalidAuthenticationCode": "کد راستین‌آزمایی نامعتبر", "emailMeALink": "یک لینک به من ایمیل کنید", - "currentPassword": "رمز جاری", - "newPassword": "رمز جدید", - "newPasswordAgain": "(رمز جدید(برای دومین بار", - "newPasswordsDontMatch": "کلمه‌های عبور وارد شده مطابقت ندارند", - "newPasswordStrength": "استحکام کلمه عبور", + "currentPassword": "گذرواژهٔ جاری", + "newPassword": "گذرواژهٔ نو", + "newPasswordAgain": "گذرواژهٔ نو (دوباره)", + "newPasswordsDontMatch": "گذرواژه‌های نو هم‌جور نیستند", + "newPasswordStrength": "نیرومندی گذرواژه", "clockInitialTime": "مقدار زمان اولیه", "clockIncrement": "مقدار زمان اضافی به ازای هر حرکت", "privacy": "حریم شخصی", "privacyPolicy": "سیاست حریم شخصی", - "letOtherPlayersFollowYou": "بقیه بازیکنان شما را دنبال کنند", + "letOtherPlayersFollowYou": "اجازه دهید دیگر بازیکنان شما را بدنبالند", "letOtherPlayersChallengeYou": "اجازه دهید بازیکنان دیگر به شما پیشنهاد بازی دهند", "letOtherPlayersInviteYouToStudy": "بگذارید دیگر بازیکنان، شما را به مطالعه دعوت کنند", "sound": "صدا", @@ -863,7 +940,7 @@ "defeatVsYInZ": "{param1} vs {param2} in {param3}", "drawVsYInZ": "{param1} vs {param2} in {param3}", "timeline": "جدول زمانی", - "starting": "شروع", + "starting": "آغاز:", "allInformationIsPublicAndOptional": "تمامی اطلاعات عمومی و اختیاری است.", "biographyDescription": "درباره ی خودتان بگویید - به چه چیزی در شطرنج علاقه داریدو گشایش ها - بازی ها و بازیکنان مورد علاقه تان…", "listBlockedPlayers": "فهرست بازیکنانی که مسدود کرده‌اید", @@ -873,9 +950,9 @@ "clock": "ساعت", "opponent": "حریف", "learnMenu": "یادگیری", - "studyMenu": "مطالعه‌ها", - "practice": "تمرین کردن", - "community": "انجمن", + "studyMenu": "مطالعه", + "practice": "تمرین", + "community": "همدارگان", "tools": "ابزارها", "increment": "افزایش زمان", "error_unknown": "مقدار نامعتبر", @@ -885,23 +962,23 @@ "error_email_unique": "آدرس ایمیل نامعتبر است یا قبلا در سیستم ثبت شده است", "error_email_different": "اکنون، این نشانی رایانامه‌تان شما است", "error_minLength": "باید حداقل {param} حرف داشته باشد", - "error_maxLength": "باید حداقل دارای {param} حرف باشد", + "error_maxLength": "باید حداکثر {param} نویسه داشته باشد", "error_min": "باید حداقل {param} باشد", "error_max": "باید حداکثر {param} باشد", "ifRatingIsPlusMinusX": "اگر درجه‌بندی‌شان {param}± است", "ifRegistered": "اگر نام‌نویسی‌کرده", "onlyExistingConversations": "تنها مکالمات موجود", "onlyFriends": "فقط دوستان", - "menu": "فهرست", + "menu": "نام‌چین", "castling": "قلعه‌روی", "whiteCastlingKingside": "O-O سفید", "blackCastlingKingside": "O-O سیاه", "tpTimeSpentPlaying": "زمان بازی کردن: {param}", - "watchGames": "تماشای بازی ها", - "tpTimeSpentOnTV": "مدت گذرانده در تلویزیون: {param}", - "watch": "نگاه کردن", - "videoLibrary": "فیلم ها", - "streamersMenu": "بَرخَط-محتواسازها", + "watchGames": "تماشای بازی‌ها", + "tpTimeSpentOnTV": "مدت آرنگیده در تلویزیون: {param}", + "watch": "تماشا", + "videoLibrary": "فیلم‌ها", + "streamersMenu": "بَرخَط-محتواسازان", "mobileApp": "گوشی‌افزار", "webmasters": "وبداران", "about": "درباره ما", @@ -910,7 +987,7 @@ "really": "واقعاً", "contribute": "مشارکت", "termsOfService": "قوانین", - "sourceCode": "منبع کد لایچس", + "sourceCode": "کد منبع", "simultaneousExhibitions": "نمایش هم زمان", "host": "میزبان", "hostColorX": "رنگ میزبان: {param}", @@ -925,42 +1002,42 @@ "aboutSimulImage": "از ۵۰ بازی فیشر موفق به کسب ۴۷ برد و ۲ تساوی و یک باخت شد.", "aboutSimulRealLife": "این مفهوم از رویدادهای واقعی الهام گرفته شده است. در آن جا میزبان میز به میز برای انجام حرکت خود، حرکت می کند.", "aboutSimulRules": "وقتی نمایش همزمان شروع شود، هر بازیکن یک بازی را با میزبان که با مهره سفید بازی میکند آغاز میکند. نمایش وقتی تمام می شود که تمام بازی ها تمام شده باشند.", - "aboutSimulSettings": "نمایش های همزمان همیشه غیر رسمی هستند. بازی دوباره، پس گرفتن حرکت و اضافه کردن زمان غیرفعال شده اند.", + "aboutSimulSettings": "نمایشگاه همزمان همیشه نارسمی است. بازرویارویی، برگرداندن و زمان افزاینده نافعال شده‌اند.", "create": "ساختن", "whenCreateSimul": "وقتی یک نمایش همزمان ایجاد میکنید باید با چند نفر همزمان بازی کنید.", - "simulVariantsHint": "اگر چندین گونه را انتخاب کنید، هر بازیکن می‌تواند انتخاب کند که کدام یک را بازی کند.", + "simulVariantsHint": "اگر چندین وَرتا را برگزینید، هر بازیکن می‌تواند انتخاب کند که کدام‌یک را بازی کند.", "simulClockHint": "تنظیم ساعت فیشر. هرچه از بازیکنان بیشتری برنده شوید، زمان بیشتری نیاز دارید", "simulAddExtraTime": "برای کمک به شما میتوانید برای خود زمان اضافی در نظر بگیرید.", "simulHostExtraTime": "زمان اضافی میزبان", "simulAddExtraTimePerPlayer": "به ازای پیوستن هر بازیکن، به زمان اولیه خود اضافه کنید.", "simulHostExtraTimePerPlayer": "زمان اضافه میزبان به ازای بازیکن", - "lichessTournaments": "مسابقات لی چس", - "tournamentFAQ": "سوالات متداول مسابقات", + "lichessTournaments": "مسابقات Lichess", + "tournamentFAQ": "پرسش‌های پربسامد مسابقات راوان", "timeBeforeTournamentStarts": "زمان باقی مانده به شروع مسابقه", "averageCentipawnLoss": "میانگین سرباز از دست داده", "accuracy": "دقت", - "keyboardShortcuts": "میانبر های صفحه کلید", + "keyboardShortcuts": "میانبرهای صفحه‌کلید", "keyMoveBackwardOrForward": "حرکت به عقب/جلو", "keyGoToStartOrEnd": "رفتن به آغاز/پایان", - "keyCycleSelectedVariation": "چرخه شاخه اصلی انتخاب‌شده", - "keyShowOrHideComments": "نمایش/ پنهان کردن نظرات", - "keyEnterOrExitVariation": "ورود / خروج به شاخه", + "keyCycleSelectedVariation": "چرخاندن وَرتِش گزیده", + "keyShowOrHideComments": "نمایش/پنهان کردن نظرها", + "keyEnterOrExitVariation": "ورود/خروج به وَرتِش", "keyRequestComputerAnalysis": "درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید", - "keyNextLearnFromYourMistakes": "بعدی (از اشتباهات خود درس بگیرید)", + "keyNextLearnFromYourMistakes": "بعدی (از اشتباه‌های‌تان بیاموزید)", "keyNextBlunder": "اشتباه فاحش بعدی", "keyNextMistake": "اشتباه بعدی", "keyNextInaccuracy": "بی‌دقتی بعدی", "keyPreviousBranch": "شاخه پیشین", "keyNextBranch": "شاخه بعدی", - "toggleVariationArrows": "کلید پیکان‌های شاخه اصلی", - "cyclePreviousOrNextVariation": "چرخه پیشین/پسین شاخه اصلی", + "toggleVariationArrows": "کلید پیکان‌های وَرتِش", + "cyclePreviousOrNextVariation": "چرخاندن پیشین/پسین وَرتِش", "toggleGlyphAnnotations": "کلید علائم حرکت‌نویسی", "togglePositionAnnotations": "تغییر حرکت‌نویسی وضعیت", - "variationArrowsInfo": "پیکان های شاخه اصلی به شما امکان می‌دهد بدون استفاده از فهرست حرکت، پیمایش کنید.", + "variationArrowsInfo": "پیکان های وَرتِش به شما امکان ناوِش بدون استفاده از فهرستِ حرکت را می‌دهد.", "playSelectedMove": "حرکت انتخاب شده را بازی کن", "newTournament": "مسابقه جدید", - "tournamentHomeTitle": "مسابقات شطرنج با گونه‌ها و زمان‌بندی‌های مختلف", - "tournamentHomeDescription": "هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، مرسوم، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.", + "tournamentHomeTitle": "مسابقات شطرنج با وَرتاها و زمان‌بندی‌های گوناگون", + "tournamentHomeDescription": "هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، فکری، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.", "tournamentNotFound": "مسابقات یافت نشد", "tournamentDoesNotExist": "این مسابقات وجود ندارد", "tournamentMayHaveBeenCanceled": "ممکن است مسابقه لغو شده باشد,شاید همه ی بازیکنان مسابقه را قبل از شروع ترک کرده باشند", @@ -968,8 +1045,8 @@ "weeklyPerfTypeRatingDistribution": "توزیع درجه‌بندی {param} هفتگی", "yourPerfTypeRatingIsRating": "درجه‌بندی {param1} شما {param2} است.", "youAreBetterThanPercentOfPerfTypePlayers": "شما بهتر از {param1} بازیکن ها در {param2} هستید.", - "userIsBetterThanPercentOfPerfTypePlayers": "{param1} بهتر از {param2} از بازیکنان {param3} میباشد.", - "betterThanPercentPlayers": "بهتر از {param1} بازیکنان در {param2}", + "userIsBetterThanPercentOfPerfTypePlayers": "{param1} بهتر از {param2} بازیکنان {param3} است.", + "betterThanPercentPlayers": "بهتر از {param1} بازیکنان {param2}", "youDoNotHaveAnEstablishedPerfTypeRating": "شما درجه‌بندی {param} تثبیت‌شده‌ای ندارید.", "yourRating": "درجه‌بندی شما", "cumulative": "تجمعی", @@ -977,33 +1054,33 @@ "checkYourEmail": "به رایانامه‌تان سر زنید", "weHaveSentYouAnEmailClickTheLink": "ما به شما ایمیل فرستادیم. روی لینکی که در ایمیل است کلیک کنید", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "اگر رایانامه را نمی‌بینید، مکان‌های دیگری مانند پوشه‌های ناخواسته، هرزنامه، اجتماعی یا سایر موردها را بررسی کنید.", - "weHaveSentYouAnEmailTo": "ایمیل ارسال شد.بر روی لینک داخل ایمیل کلیک کنید تا پسورد شما ریست شود {param} به آدرس", + "weHaveSentYouAnEmailTo": "ما یک رایانامه به {param} فرستاده‌ایم. برای بازنشانی گذرواژه‌تان، روی پیوند موجود در رایانامه بزنید.", "byRegisteringYouAgreeToBeBoundByOur": "با ثبت‌نام، با {param} موافقت می‌کنید.", "readAboutOur": "درباره {param} ما بخوانید.", - "networkLagBetweenYouAndLichess": "تاخیر شبکه بین شما و Lichess", + "networkLagBetweenYouAndLichess": "تاخیر شبکه میان شما و Lichess", "timeToProcessAMoveOnLichessServer": "زمان سپری شده برای پردازش یک حرکت", "downloadAnnotated": "بارگیری حرکت‌نویسی", "downloadRaw": "بارگیری خام", "downloadImported": "بارگیری درونبُرد", "crosstable": "رودررو", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "شما می توانید برای حرکت در بازی از صفحه استفاده کنید", - "scrollOverComputerVariationsToPreviewThem": "برای مشاهده آن ها اسکرول کنید.", - "analysisShapesHowTo": "و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "برای حرکت، روی صفحه بازی بِنَوَردید.", + "scrollOverComputerVariationsToPreviewThem": "برای پیش‌نمایش آن‌ها، روی وَرتِش‌های رایانه‌ای بِغَرالید.", + "analysisShapesHowTo": "برای رسم دایره و پیکان روی تخته، shift+click یا راست-تِلیک را بفشارید.", "letOtherPlayersMessageYou": "ارسال پیام توسط بقیه به شما", - "receiveForumNotifications": "دریافت اعلان در هنگام ذکر شدن در انجمن", + "receiveForumNotifications": "دریافت اعلان هنگام نام‌بَری در انجمن", "shareYourInsightsData": "اشتراک گذاشتن داده های شما", "withNobody": "هیچکس", "withFriends": "با دوستان", "withEverybody": "با همه", - "kidMode": "حالت کودکان", + "kidMode": "حالت کودک", "kidModeIsEnabled": "حالت کودک فعال است.", - "kidModeExplanation": "این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید.", - "inKidModeTheLichessLogoGetsIconX": "در حالت کودکانه،به نماد لیچس،یک {param} اضافه می شود تا شما از فعال بودن آن مطلع شوید.", - "askYourChessTeacherAboutLiftingKidMode": "اکانت شما مدیریت شده است. برای برداشتن حالت کودک از معلم شطرنج خود درخواست کنید.", - "enableKidMode": "فعال کردن حالت کودکانه", - "disableKidMode": "غیر فعال کردن حالت کودکانه", + "kidModeExplanation": "این دربارهٔ ایمنی است. در حالت کودک، همهٔ ارتباط‌های وبگاه نافعال است. این را برای فرزندان و شطرنج‌آموزان مدرسه خود فعال کنید تا از آنها در برابر دیگر کاربران اینترنت حفاظت کنید.", + "inKidModeTheLichessLogoGetsIconX": "در حالت کودک، نماد Lichess نقشک {param} را می‌گیرد، بنابراین می‌دانید کودکان‌تان در امانند.", + "askYourChessTeacherAboutLiftingKidMode": "حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.", + "enableKidMode": "فعال‌سازی حالت کودک", + "disableKidMode": "ازکاراندازی حالت کودک", "security": "امنیت", - "sessions": "جلسات", + "sessions": "جلسه", "revokeAllSessions": "باطل کردن تمامی موارد", "playChessEverywhere": "همه جا شطرنج بازی کنید", "asFreeAsLichess": "کاملا رایگان", @@ -1011,16 +1088,16 @@ "everybodyGetsAllFeaturesForFree": "همگی از مزایا بصورت رایگان استفاده می کنند", "zeroAdvertisement": "بدون تبلیغات", "fullFeatured": "با تمامی امکانات", - "phoneAndTablet": "موبایل و تبلت", - "bulletBlitzClassical": "گلوله‌ای، برق‌آسا، مرسوم", + "phoneAndTablet": "گوشی و رایانک", + "bulletBlitzClassical": "گلوله‌ای، برق‌آسا، فکری", "correspondenceChess": "شطرنج مکاتبه ای", "onlineAndOfflinePlay": "بازی بَرخط و بُرون‌خط", "viewTheSolution": "دیدن راه‌حل", - "followAndChallengeFriends": "دنبال کردن و پیشنهاد بازی دادن به دوستان", + "followAndChallengeFriends": "دنبالیدن و پیشنهاد بازی دادن به دوستان", "gameAnalysis": "تجزیه و تحلیلِ بازی", - "xHostsY": "{param1} میزبان ها {param2}", - "xJoinsY": "{param1} وارد می شود {param2}", - "xLikesY": "{param1} می پسندد {param2}", + "xHostsY": "{param1} میزبان {param2} است", + "xJoinsY": "{param1} به {param2} می‌پیوندد", + "xLikesY": "{param1}، {param2} را می‌پسندد", "quickPairing": "رویارویی سریع", "lobby": "سَرسَرا", "anonymous": "ناشناس", @@ -1030,7 +1107,7 @@ "light": "روشن", "dark": "تیره", "transparent": "شفاف", - "deviceTheme": "طرح زمینه دستگاه", + "deviceTheme": "پوستهٔ اَفزاره", "backgroundImageUrl": "وب‌نشانی تصویر پس‌زمینه:", "board": "صفحه شطرنج", "size": "اندازه", @@ -1044,31 +1121,31 @@ "usernamePrefixInvalid": "نام کاربری باید با حرف شروع شود.", "usernameSuffixInvalid": "نام کاربری باید با حرف یا شماره خاتمه یابد.", "usernameCharsInvalid": "نام کاربری فقط می تواند شامل حروف،اعداد،خط فاصله یا زیر خط(under line) باشد.", - "usernameUnacceptable": "این نام کاربری قابل قبول نیست.", - "playChessInStyle": "استایل شطرنج باز داشته باشید!", - "chessBasics": "اصول شطرنج", - "coaches": "مربی ها", - "invalidPgn": "فایل PGN نامعتبر است", + "usernameUnacceptable": "این نام کاربری پذیرفتنی نیست.", + "playChessInStyle": "شطرنج‌بازیِ نوگارانه", + "chessBasics": "پایه‌های شطرنج", + "coaches": "مربیان", + "invalidPgn": "PGN ِ نامعتبر", "invalidFen": "وضعیت نامعتبر", "custom": "دلخواه", - "notifications": "گزارش", - "notificationsX": "هشدار: {param1}", + "notifications": "اعلان", + "notificationsX": "اعلان: {param1}", "perfRatingX": "درجه‌بندی: {param}", "practiceWithComputer": "تمرین با رایانه", "anotherWasX": "حرکت مناسب دیگر {param} بود", "bestWasX": "بهترین حرکت {param} بود", - "youBrowsedAway": "پوزیشن را به هم زدید!", - "resumePractice": "ادامه تمرین", + "youBrowsedAway": "دور شُدید", + "resumePractice": "از سرگیری تمرین", "drawByFiftyMoves": "بازی با قانون پنجاه حرکت مساوی شده است.", "theGameIsADraw": "بازی مساوی است.", - "computerThinking": "محاسبه رایانه‌ای ...", - "seeBestMove": "مشاهده بهترین حرکت", + "computerThinking": "محاسبهٔ رایانه‌ای...", + "seeBestMove": "دیدن بهترین حرکت", "hideBestMove": "پنهان کردن بهترین حرکت", "getAHint": "راهنمایی", - "evaluatingYourMove": "در حال بررسی حرکت شما...", - "whiteWinsGame": "سفید برنده شد", - "blackWinsGame": "سیاه برنده شد", - "learnFromYourMistakes": "از اشتباهات خود درس بگیرید", + "evaluatingYourMove": "حرکت‌تان را می‌ارزیابد...", + "whiteWinsGame": "سفید می‌برد", + "blackWinsGame": "سیاه می‌برد", + "learnFromYourMistakes": "از اشتباه‌های‌تان بیاموزید", "learnFromThisMistake": "از این اشتباه درس بگیرید", "skipThisMove": "رد کردن این حرکت", "next": "بعدی", @@ -1077,12 +1154,12 @@ "findBetterMoveForBlack": "حرکت بهتری برای سیاه بیابید", "resumeLearning": "ادامه یادگیری", "youCanDoBetter": "می‌توانید بهتر انجامش دهید", - "tryAnotherMoveForWhite": "برای سفید،حرکت دیگری را امتحان کنید", - "tryAnotherMoveForBlack": "برای سیاه،حرکت دیگری را امتحان کنید", + "tryAnotherMoveForWhite": "حرکت دیگری را برای سفید بیابید", + "tryAnotherMoveForBlack": "حرکت دیگری را برای سیاه بیابید", "solution": "راه‌حل", - "waitingForAnalysis": "در انتظار برای آنالیز", - "noMistakesFoundForWhite": "هیچ اشتباهی برای سفید مشاهده نشد", - "noMistakesFoundForBlack": "هیچ اشتباهی برای سیاه مشاهده نشد", + "waitingForAnalysis": "در انتظار تحلیل", + "noMistakesFoundForWhite": "هیچی اشتباهی از سفید یافت نشد", + "noMistakesFoundForBlack": "هیچی اشتباهی از سیاه یافت نشد", "doneReviewingWhiteMistakes": "اشتباهات سفید بررسی شد", "doneReviewingBlackMistakes": "اشتباهات سیاه بررسی شد.", "doItAgain": "دوباره", @@ -1093,8 +1170,8 @@ "middlegame": "وسط بازی", "endgame": "آخربازی", "conditionalPremoves": "پیش‌حرکت‌های شرطی", - "addCurrentVariation": "اضافه کردن این نوع حرکات", - "playVariationToCreateConditionalPremoves": "یک نوع حرکات را بازی کنید تا پیش حرکت های شرطی را بسازید", + "addCurrentVariation": "افزودن وَرتِش جاری", + "playVariationToCreateConditionalPremoves": "بازی کردن یک وَرتِش، برای ایجاد پیش‌حرکت‌های شرطی", "noConditionalPremoves": "بدون پیش‌حرکت‌های شرطی", "playX": "{param} را انجام دهید", "showUnreadLichessMessage": "شما یک پیام خصوصی از Lichess دریافت کرده‌اید.", @@ -1105,7 +1182,7 @@ "pleasantChessExperience": "هدف ما مهیا ساختن تجربه لذت بخش شطرنج به همه افراد است.", "goodPractice": "به همین منظور، ما باید اطمینان حاصل کنیم که تمام بازیکنان تمرین خوب را دنبال میکنند.", "potentialProblem": "زمانی که مشکلی احتمالی شناسایی شد ، این پیام را نمایش می دهیم.", - "howToAvoidThis": "چگونه از این امر جلوگیری کنیم؟", + "howToAvoidThis": "چگونه از آن بپرهیزیم؟", "playEveryGame": "هر بازی‌ای که آغازیدید را، بازی کنید.", "tryToWin": "در هر بازی برای پیروزی (یا حداقل تساوی) تلاش کنید.", "resignLostGames": "بازی های از دست رفته را انصراف دهید(نگذارید زمان تمام شود).", @@ -1114,26 +1191,26 @@ "thankYouForReading": "از اینکه متن را خواندید متشکریم!", "lifetimeScore": "امتیاز کل", "currentMatchScore": "امتیاز بازی فعلی", - "agreementAssistance": "من تضمین میکنم که در حین بازی ها کمک نگیرم ( از انجین ، کتاب ، پایگاه داده یا شخصی دیگر)", - "agreementNice": "من تضمین میکنم که همیشه به بازیکن های دیگر احترام بگذارم.", - "agreementMultipleAccounts": "من موافقت می‌کنم که چندین اکانت برای خودم ایجاد نکنم(به جز دلایلی که در {param} اشاره شده).", - "agreementPolicy": "من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم .", + "agreementAssistance": "من موافقم که در طول بازی‌هایم هیچگاه کمکی نخواهم گرفت (از یک رایانه شطرنج، کتاب، دادگان یا شخص دیگری).", + "agreementNice": "می‌پذیرم که همواره به بازیکنان دیگر احترام گزارم.", + "agreementMultipleAccounts": "موافقم که چندین حساب نخواهم ساخت (جز به دلیل‌های ذکر شده در {param}).", + "agreementPolicy": "با پیروی از همهٔ خط‌مشی‌های Lichess، موافقم.", "searchOrStartNewDiscussion": "جستجو یا شروع کردن مکالمه جدید", "edit": "ویرایش", "bullet": "گلوله‌ای", "blitz": "برق‌آسا", "rapid": "سریع", - "classical": "کلاسیک", + "classical": "فکری", "ultraBulletDesc": "بازی‌های سرعتی دیوانه‌وار: کمتر از ۳۰ ثانیه", "bulletDesc": "بازی‌های خیلی سرعتی: کمتر از ۳ دقیقه", "blitzDesc": "بازی های سرعتی: ۳ تا ۸ دقیقه", "rapidDesc": "بازی های سریع: ۸ تا ۲۵ دقیقه", - "classicalDesc": "بازی های کلاسیک : 25 دقیقه یا بیشتر", + "classicalDesc": "بازی های فکری: ۲۵ دقیقه یا بیشتر", "correspondenceDesc": "بازی های مکاتبه ای : یک یا چند روز برای هر حرکت", "puzzleDesc": "تمرین تاکتیک های شطرنج", "important": "مهم!", "yourQuestionMayHaveBeenAnswered": "سوال شما ممکن است که از قبل پاسخی داشته باشد {param1}", - "inTheFAQ": "در سوالات متداول باشد.", + "inTheFAQ": "در پرسش‌های پُربسامد", "toReportSomeoneForCheatingOrBadBehavior": "برای گزارش دادن یک کاربر به علت تقلب یا بدرفتاری، {param1}", "useTheReportForm": "از فرم گزارش استفاده کنید.", "toRequestSupport": "جهت درخواست پشتیبانی، {param1}", @@ -1141,28 +1218,28 @@ "makeSureToRead": "حتما {param1} را مطالعه کنید", "theForumEtiquette": "آداب انجمن", "thisTopicIsArchived": "این موضوع بایگانی شده است و دیگر نمی توان به آن پاسخ داد.", - "joinTheTeamXToPost": "به {param1} ملحق شوید تا در این انجمن پست بگذارید", + "joinTheTeamXToPost": "برای فرسته گذاشتن در این انجمن، به {param1} بپیوندید", "teamNamedX": "تیم {param1}", - "youCannotPostYetPlaySomeGames": "شما هنوز قادر به پست گذاشتن در انجمن نیستید. چند بازی انجام دهید!", + "youCannotPostYetPlaySomeGames": "هنوز نمی‌توانید در انجمن‌ها فرسته گذارید. چند بازی کنید!", "subscribe": "مشترک شدن", "unsubscribe": "لغو اشتراک", - "mentionedYouInX": "از شما در {param1} نام برده شد.", - "xMentionedYouInY": "{param1} اسم شما را در \"{param2}\" ذکر کرده است.", + "mentionedYouInX": "در «{param1}» از شما نام‌برده شد.", + "xMentionedYouInY": "{param1} از شما در \"{param2}\" نام برد.", "invitedYouToX": "به «{param1}» دعوت شده‌اید.", "xInvitedYouToY": "{param1} شما را به «{param2}» دعوت کرده است.", "youAreNowPartOfTeam": "شما در حال حاضر عضوی از تیم هستید.", - "youHaveJoinedTeamX": "شما به \"{param1}\" پیوستید.", + "youHaveJoinedTeamX": "شما به «{param1}» پیوسته‌اید.", "someoneYouReportedWasBanned": "شخصی که گزارش کردید مسدود شد", - "congratsYouWon": "تبریک، شما برنده شدید!", + "congratsYouWon": "شادباش، شما بُردید!", "gameVsX": "بازی در برابر {param1}", "resVsX": "{param1} در برابر {param2}", - "lostAgainstTOSViolator": "شما در برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید", + "lostAgainstTOSViolator": "شما برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید", "refundXpointsTimeControlY": "پس‌دادن: {param1} امتیاز به درجه‌بندی {param2}.", - "timeAlmostUp": "زمان تقریباً تمام شده است!", - "clickToRevealEmailAddress": "[برای آشکارسازی نشانی رایانامه بتلیکید]", + "timeAlmostUp": "زمان نزدیک به پایان است!", + "clickToRevealEmailAddress": "[برای آشکارسازی نشانیِ رایانامه بتِلیکید]", "download": "بارگیری", "coachManager": "تنظیمات مربی", - "streamerManager": "مدیریت پخش", + "streamerManager": "مدیریت جریان‌سازی", "cancelTournament": "لغو مسابقه", "tournDescription": "توضیحات مسابقه", "tournDescriptionHelp": "نکته خاصی را می‌خواهید به شرکت‌کنندگان گویید؟ بکوشید کوتاه باشد. پیوندهای فرونشان موجودند:\n[name](https://url)", @@ -1172,11 +1249,11 @@ "minimumRatedGames": "حداقل بازی های ریتد", "minimumRating": "حداقل درجه‌بندی", "maximumWeeklyRating": "حداکثر درجه‌بندی هفتگی", - "positionInputHelp": "برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با شطرنج‌گونه‌ها.\nمی‌توانید از {param} برای آزانیدن وضعیت FEN استفاده کنید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.", + "positionInputHelp": "برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با وَرتاها.\nمی‌توانید از {param} برای آزانیدن وضعیت FEN بهرایید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.", "cancelSimul": "بازی هم‌زمان (سیمولتانه) را لغو نمایید", "simulHostcolor": "رنگ مربوط به نمایش‌دهنده یا میزبان برای هر بازی", "estimatedStart": "زمان تقریبی شروع بازی", - "simulFeatured": "نمایش در {param}", + "simulFeatured": "آرنگیدن در {param}", "simulFeaturedHelp": "بازی هم‌زمان خود را برای همه بر روی لینک {param} نشان بدهید. برای دسترسی خصوصی آن را غیرفعال نمایید.", "simulDescription": "توصیف بازی هم‌زمان", "simulDescriptionHelp": "آیا می‌خواهید مطلبی را به شرکت‌کنندگان بگویید؟", @@ -1184,7 +1261,7 @@ "embedsAvailable": "وب‌نشانی بازی یا وب‌نشانی بخشی از مطالعه را، برای جاسازی آن، جایگذاری کنید.", "inYourLocalTimezone": "ذر منطقه زمانی شما", "tournChat": "چت مسابقه", - "noChat": "بدون چت", + "noChat": "بدون گپ", "onlyTeamLeaders": "تنها مسئولان تیم", "onlyTeamMembers": "تنها اعضای تیم", "navigateMoveTree": "ناویدن فهرست حرکت‌ها", @@ -1217,15 +1294,16 @@ "showMeEverything": "همه چیز را به من نشان بده", "lichessPatronInfo": "لایچس یک خیریه و کاملا رایگان و نرم افزاری متن باز است. تمام هزینه های اجرا، توسعه و محتوا تنها بر پایه هدایای کاربران بنا شده است.", "nothingToSeeHere": "فعلا هیچی اینجا نیست.", + "stats": "آمار", "opponentLeftCounter": "{count, plural, =1{رقیب شما بازی را ترک کرده است. شما میتوانید بعد از {count} ثانیه اعلام پیروزی کنید.} other{رقیب شما بازی را ترک کرده است. شما میتوانید بعد از {count} ثانیه اعلام پیروزی کنید.}}", "mateInXHalfMoves": "{count, plural, =1{در {count} نیم‌حرکت مات می‌شود} other{در {count} نیم‌حرکت مات می‌شود}}", "nbBlunders": "{count, plural, =1{{count} اشتباه بزرگ} other{{count} اشتباه بزرگ}}", "nbMistakes": "{count, plural, =1{{count} اشتباه} other{{count} اشتباه}}", - "nbInaccuracies": "{count, plural, =1{{count} غیردقیق} other{{count} غیردقیق}}", + "nbInaccuracies": "{count, plural, =1{{count} نادقیق} other{{count} نادقیق}}", "nbPlayers": "{count, plural, =1{{count} بازیکن} other{{count} بازیکن}}", "nbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", - "ratingXOverYGames": "{count, plural, =1{درجه‌بندی {count} در {param2} بازی} other{{count} ریتینگ در {param2} بازی}}", - "nbBookmarks": "{count, plural, =1{{count} بازی مورد علاقه} other{{count} بازی مورد علاقه}}", + "ratingXOverYGames": "{count, plural, =1{درجه‌بندی {count} در {param2} بازی} other{درجه‌بندی {count} در {param2} بازی}}", + "nbBookmarks": "{count, plural, =1{{count} نشانک} other{{count} نشانک}}", "nbDays": "{count, plural, =1{{count} روز} other{{count} روز}}", "nbHours": "{count, plural, =1{{count} ساعت} other{{count} ساعت}}", "nbMinutes": "{count, plural, =1{{count} دقیقه} other{{count} دقیقه}}", @@ -1236,25 +1314,25 @@ "nbWins": "{count, plural, =1{{count} برد} other{{count} برد}}", "nbLosses": "{count, plural, =1{{count} باخت} other{{count} باخت}}", "nbDraws": "{count, plural, =1{{count} مساوی} other{{count} مساوی}}", - "nbPlaying": "{count, plural, =1{{count} بازی در حال انجام} other{{count} بازی در حال انجام}}", + "nbPlaying": "{count, plural, =1{{count} بازیِ اکنونی} other{{count} بازیِ اکنونی}}", "giveNbSeconds": "{count, plural, =1{{count} ثانیه اضافه کن} other{{count} ثانیه اضافه کن}}", "nbTournamentPoints": "{count, plural, =1{مجموع امتیازات مسابقات:{count}} other{مجموع امتیازات مسابقات:{count}}}", "nbStudies": "{count, plural, =1{{count} مطالعه} other{{count} مطالعه}}", "nbSimuls": "{count, plural, =1{{count} سیمولتانه} other{{count} سیمولتانه}}", "moreThanNbRatedGames": "{count, plural, =1{بیشتر از {count} بازی رسمی} other{بیشتر از {count} بازی رسمی}}", "moreThanNbPerfRatedGames": "{count, plural, =1{بیشتر از {count} بازی رسمی {param2}} other{بیشتر از {count} بازی رسمی {param2}}}", - "needNbMorePerfGames": "{count, plural, =1{شما باید{count} بازی رسمی{param2} انجام دهید.} other{شما باید{count} بازی رسمی{param2} انجام دهید.}}", + "needNbMorePerfGames": "{count, plural, =1{شما باید {count} بازی رسمی {param2} دیگر کنید} other{شما باید {count} بازی رسمی {param2} دیگر کنید}}", "needNbMoreGames": "{count, plural, =1{شما باید{count} بازی رسمی دیگر انجام دهید.} other{شما باید{count} بازی رسمی دیگر انجام دهید.}}", "nbImportedGames": "{count, plural, =1{{count} بارگذاری شده} other{{count} بارگذاری شده}}", "nbFriendsOnline": "{count, plural, =1{{count} دوست بَرخط} other{{count} دوست بَرخط}}", - "nbFollowers": "{count, plural, =1{{count} دنبال کننده‌} other{{count} دنبال کننده‌}}", - "nbFollowing": "{count, plural, =1{{count} دنبال می کند} other{{count} دنبال میکند}}", + "nbFollowers": "{count, plural, =1{{count} دنبال‌گر} other{{count} دنبال‌گر}}", + "nbFollowing": "{count, plural, =1{{count} دنبالیده} other{{count} دنبال‌شده}}", "lessThanNbMinutes": "{count, plural, =1{کمتر از {count} دقیقه} other{کمتر از {count} دقیقه}}", "nbGamesInPlay": "{count, plural, =1{{count} بازی در حال انجام است} other{{count} بازی در حال انجام است}}", "maximumNbCharacters": "{count, plural, =1{حداکثر: {count} حرف} other{حداکثر: {count} حرف}}", "blocks": "{count, plural, =1{{count} مسدود شده} other{{count} مسدود شده}}", - "nbForumPosts": "{count, plural, =1{{count} وبنوشته در انجمن} other{{count} وبنوشته در انجمن}}", - "nbPerfTypePlayersThisWeek": "{count, plural, =1{{count} بازیکن {param2} این هفته فعالیت داشته‌ است.} other{{count} بازیکن {param2} این هفته فعالیت داشته‌اند.}}", + "nbForumPosts": "{count, plural, =1{{count} فرسته در انجمن} other{{count} فرسته در انجمن}}", + "nbPerfTypePlayersThisWeek": "{count, plural, =1{این هفته، {count} بازیکن {param2}.} other{این هفته، {count} بازیکن {param2}.}}", "availableInNbLanguages": "{count, plural, =1{در {count} زبان موجود است!} other{در {count} زبان موجود است!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{{count} ثانیه برای شروع اولین حرکت} other{{count} ثانیه برای شروع اولین حرکت}}", "nbSeconds": "{count, plural, =1{{count} ثانیه} other{{count} ثانیه}}", @@ -1281,11 +1359,11 @@ "stormNewRun": "دور جدید (میانبر: Space)", "stormEndRun": "پایان‌دهی دور (میانبر: Enter)", "stormHighscores": "بالاترین امتیازها", - "stormViewBestRuns": "مشاهده بهترین دورها", + "stormViewBestRuns": "دیدن بهترین دورها", "stormBestRunOfDay": "بهترین دور روز", "stormRuns": "دورها", "stormGetReady": "آماده شوید!", - "stormWaitingForMorePlayers": "در حال انتظار برای پیوستن بازیکنان بیشتر...", + "stormWaitingForMorePlayers": "در انتظارِ پیوستن بازیکنان بیشتر...", "stormRaceComplete": "مسابقه تمام شد!", "stormSpectating": "در حال تماشا", "stormJoinTheRace": "به مسابقه بپیوندید!", @@ -1313,6 +1391,178 @@ "stormXRuns": "{count, plural, =1{یک دور} other{{count} دور}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{یک دور از {param2} بازی شد} other{{count} دور از {param2} بازی شد}}", "streamerLichessStreamers": "بَرخَط-محتواسازان Lichess", + "studyPrivate": "خصوصی", + "studyMyStudies": "مطالعه‌های من", + "studyStudiesIContributeTo": "مطالعه‌هایی که در آن شرکت دارم", + "studyMyPublicStudies": "مطالعه‌های همگانی من", + "studyMyPrivateStudies": "مطالعه‌های خصوصی من", + "studyMyFavoriteStudies": "مطالعه‌های دلخواه من", + "studyWhatAreStudies": "مطالعه‌ها چه هستند؟", + "studyAllStudies": "همه مطالعه‌ها", + "studyStudiesCreatedByX": "مطالعه‌هایی که {param} ساخته است", + "studyNoneYet": "هنوز، هیچ.", + "studyHot": "رواجیده", + "studyDateAddedNewest": "تاریخ افزوده شدن (نوترین)", + "studyDateAddedOldest": "تاریخ افزوده شدن (کهنه‌ترین)", + "studyRecentlyUpdated": "تازگی به‌روزشده", + "studyMostPopular": "محبوب‌ترین‌", + "studyAlphabetical": "براساس حروف الفبا", + "studyAddNewChapter": "افزودن بخش جدید", + "studyAddMembers": "افزودن اعضا", + "studyInviteToTheStudy": "دعوت به این مطالعه", + "studyPleaseOnlyInvitePeopleYouKnow": "لطفا تنها کسانی را دعوت کنید که شما را می‌شناسند و کنشگرانه می‌خواهند به این مطالعه بپیوندند.", + "studySearchByUsername": "جستجو بر اساس نام کاربری", + "studySpectator": "تماشاگر", + "studyContributor": "مشارکت کننده", + "studyKick": "اخراج", + "studyLeaveTheStudy": "ترک مطالعه", + "studyYouAreNowAContributor": "شما یک مشارکت کننده جدید هستید", + "studyYouAreNowASpectator": "شما اکنون یک تماشاگرید", + "studyPgnTags": "نشان های PGN", + "studyLike": "پسندیدن", + "studyUnlike": "نمی‌پسندم", + "studyNewTag": "برچسب جدید", + "studyCommentThisPosition": "یادداشت‌نویسی برای این وضعیت", + "studyCommentThisMove": "یادداشت‌نویسی برای این حرکت", + "studyAnnotateWithGlyphs": "حرکت‌نویسی به‌همراه علامت‌ها", + "studyTheChapterIsTooShortToBeAnalysed": "این بخش برای تحلیل، بسیار کوتاه است.", + "studyOnlyContributorsCanRequestAnalysis": "تنها مشارکت‌گران این مطالعه، می‌توانند درخواست تحلیل رایانه‌ای دهند.", + "studyGetAFullComputerAnalysis": "یک تحلیل کامل رایانه‌ای کارساز-سو از شاخه اصلی بگیرید.", + "studyMakeSureTheChapterIsComplete": "مطمئن شوید که بخش کامل است. شما فقط یک بار می‌توانید درخواست تحلیل دهید.", + "studyAllSyncMembersRemainOnTheSamePosition": "همه‌ی عضوهای همگام در وضعیت یکسانی باقی می‌مانند", + "studyShareChanges": "تغییرها را در کارساز ذخیره کنید و با تماشاگران به اشتراک گذارید", + "studyPlaying": "در حال انجام", + "studyShowEvalBar": "نوار ارزیابی", + "studyFirst": "اولین", + "studyPrevious": "پیشین", + "studyNext": "بعدی", + "studyLast": "آخرین", "studyShareAndExport": "همرسانی و برون‏بُرد", - "studyStart": "آغاز" + "studyCloneStudy": "همسانیدن", + "studyStudyPgn": "PGN مطالعه", + "studyDownloadAllGames": "بارگیری تمام بازی ها", + "studyChapterPgn": "PGN ِ بخش", + "studyCopyChapterPgn": "رونوشت‌گیری PGN", + "studyDownloadGame": "بارگیری بازی", + "studyStudyUrl": "وب‌نشانی مطالعه", + "studyCurrentChapterUrl": "وب‌نشانی بخش جاری", + "studyYouCanPasteThisInTheForumToEmbed": "می‌توانید این را در انجمن یا وبنوشت Lichessتان برای جاسازی قرار دهید", + "studyStartAtInitialPosition": "در وضعیت نخستین بیاغازید", + "studyStartAtX": "آغاز از {param}", + "studyEmbedInYourWebsite": "در وبگاهتان قرار دهید", + "studyReadMoreAboutEmbedding": "درباره قرار دادن (در سایت) بیشتر بخوانید", + "studyOnlyPublicStudiesCanBeEmbedded": "فقط مطالعه‌های همگانی می‌توانند جایگذاری شوند!", + "studyOpen": "بگشایید", + "studyXBroughtToYouByY": "{param1}، به دست {param2} برای شما آورده شده است", + "studyStudyNotFound": "مطالعه یافت نشد", + "studyEditChapter": "ویرایش بخش", + "studyNewChapter": "بخش نو", + "studyImportFromChapterX": "درونبُرد از {param}", + "studyOrientation": "جهت", + "studyAnalysisMode": "حالت تجزیه تحلیل", + "studyPinnedChapterComment": "یادداشت سنجاقیده‌ٔ بخش", + "studySaveChapter": "ذخیره بخش", + "studyClearAnnotations": "پاک کردن حرکت‌نویسی", + "studyClearVariations": "پاکیدن وَرتِش‌ها", + "studyDeleteChapter": "حذف بخش", + "studyDeleteThisChapter": "حذف این بخش. بازگشت وجود ندارد!", + "studyClearAllCommentsInThisChapter": "همه دیدگاه‌ها، نمادها و شکل‌های ترسیم شده در این بخش، پاک شوند", + "studyRightUnderTheBoard": "درست زیر صفحهٔ بازی", + "studyNoPinnedComment": "هیچ", + "studyNormalAnalysis": "تحلیل ساده", + "studyHideNextMoves": "پنهان کردن حرکت بعدی", + "studyInteractiveLesson": "درس میان‌کنشی", + "studyChapterX": "بخش {param}", + "studyEmpty": "خالی", + "studyStartFromInitialPosition": "از وضعیت نخستین بیاغازید", + "studyEditor": "ویرایشگر", + "studyStartFromCustomPosition": "از وضعیت دلخواه بیاغازید", + "studyLoadAGameByUrl": "بارگذاری بازی از وب‌نشانی‌ها", + "studyLoadAPositionFromFen": "بار کردن وضعیت از FEN", + "studyLoadAGameFromPgn": "باگذاری بازی با استفاده از فایل PGN", + "studyAutomatic": "خودکار", + "studyUrlOfTheGame": "وب‌نشانی بازی‌ها، یکی در هر خط", + "studyLoadAGameFromXOrY": "بازی‌ها را از {param1} یا {param2} بارگذاری نمایید", + "studyCreateChapter": "ساخت بخش", + "studyCreateStudy": "ساخت مطالعه", + "studyEditStudy": "ویرایش مطالعه", + "studyVisibility": "دیدگی", + "studyPublic": "همگانی", + "studyUnlisted": "فهرست‌نشده", + "studyInviteOnly": "فقط توسط دعوتنامه", + "studyAllowCloning": "اجازه همسانِش", + "studyNobody": "هیچ کس", + "studyOnlyMe": "تنها من", + "studyContributors": "مشارکت‌کنندگان", + "studyMembers": "اعضا", + "studyEveryone": "همه", + "studyEnableSync": "فعال کردن همگام سازی", + "studyYesKeepEveryoneOnTheSamePosition": "بله: همه را در وضعیت یکسانی نگه دار", + "studyNoLetPeopleBrowseFreely": "خیر: به مردم اجازه جستجوی آزادانه بده", + "studyPinnedStudyComment": "یادداشت سنجاقیده به مطالعه", + "studyStart": "آغاز", + "studySave": "ذخیره", + "studyClearChat": "پاک کردن گفتگو", + "studyDeleteTheStudyChatHistory": "پیشینه گپِ مطالعه پاک شود؟ بازگشت وجود ندارد!", + "studyDeleteStudy": "پاکیدن مطالعه", + "studyConfirmDeleteStudy": "کل مطالعه پاک شود؟ بازگشت وجود ندارد! برای تایید، نام مطالعه را بنویسید: {param}", + "studyWhereDoYouWantToStudyThat": "کجا می‌خواهید آنرا مطالعه کنید؟", + "studyGoodMove": "حرکت خوب", + "studyMistake": "اشتباه", + "studyBrilliantMove": "حرکت درخشان", + "studyBlunder": "اشتباه فاحش", + "studyInterestingMove": "حرکت جالب", + "studyDubiousMove": "حرکت مشکوک", + "studyOnlyMove": "تک‌حرکت", + "studyZugzwang": "اکراهی", + "studyEqualPosition": "وضعیت برابر", + "studyUnclearPosition": "وضعیت ناروشن", + "studyWhiteIsSlightlyBetter": "سفید کمی بهتر است", + "studyBlackIsSlightlyBetter": "سیاه کمی بهتر است", + "studyWhiteIsBetter": "سفید بهتر است", + "studyBlackIsBetter": "سیاه بهتر است", + "studyWhiteIsWinning": "سفید می‌برد", + "studyBlackIsWinning": "سیاه می‌برد", + "studyNovelty": "روش و ایده‌ای نو در شروع بازی", + "studyDevelopment": "گسترش", + "studyInitiative": "ابتکار عمل", + "studyAttack": "حمله", + "studyCounterplay": "بازی‌متقابل", + "studyTimeTrouble": "تنگی زمان", + "studyWithCompensation": "دارای مزیت و برتری", + "studyWithTheIdea": "با طرح", + "studyNextChapter": "بخش بعدی", + "studyPrevChapter": "بخش پیشین", + "studyStudyActions": "عملگرهای مطالعه", + "studyTopics": "موضوع‌ها", + "studyMyTopics": "موضوع‌های من", + "studyPopularTopics": "موضوع‌های محبوب", + "studyManageTopics": "مدیریت موضوع‌ها", + "studyBack": "بازگشت", + "studyPlayAgain": "دوباره بازی کنید", + "studyWhatWouldYouPlay": "در این وضعیت چطور بازی می‌کنید؟", + "studyYouCompletedThisLesson": "تبریک! شما این درس را کامل کردید.", + "studyPerPage": "{param} میز", + "studyNbChapters": "{count, plural, =1{{count} بخش} other{{count} بخش}}", + "studyNbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", + "studyNbMembers": "{count, plural, =1{{count} عضو} other{{count} عضو}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{متن PGN خود را در اینجا بچسبانید، تا {count} بازی} other{متن PGN خود را در اینجا بچسبانید، تا {count} بازی}}", + "timeagoJustNow": "چند لحظه پیش", + "timeagoRightNow": "هم‌اکنون", + "timeagoCompleted": "کامل شده", + "timeagoInNbSeconds": "{count, plural, =1{تا {count} ثانیهٔ دیگر} other{تا {count} ثانیهٔ دیگر}}", + "timeagoInNbMinutes": "{count, plural, =1{تا {count} دقیقه دیگر} other{تا {count} دقیقه دیگر}}", + "timeagoInNbHours": "{count, plural, =1{تا {count} ساعت دیگر} other{تا {count} ساعت دیگر}}", + "timeagoInNbDays": "{count, plural, =1{تا {count} روز دیگر} other{تا {count} روز دیگر}}", + "timeagoInNbWeeks": "{count, plural, =1{تا {count} هفته دیگر} other{تا {count} هفته دیگر}}", + "timeagoInNbMonths": "{count, plural, =1{تا {count} ماه دیگر} other{تا {count} ماه دیگر}}", + "timeagoInNbYears": "{count, plural, =1{تا {count} سال دیگر} other{تا {count} سال دیگر}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} دقیقه پیش} other{{count} دقیقه پیش}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ساعت پیش} other{{count} ساعت پیش}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} روز پیش} other{{count} روز پیش}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} هفته پیش} other{{count} هفته پیش}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ماه پیش} other{{count} ماه پیش}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} سال پیش} other{{count} سال پیش}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} دقیقه باقی مانده} other{{count} دقیقه باقی مانده}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ساعت باقی مانده} other{{count} ساعت باقی مانده}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fi.arb b/lib/l10n/lila_fi.arb index 4ddb845b0a..0e7ea3eda2 100644 --- a/lib/l10n/lila_fi.arb +++ b/lib/l10n/lila_fi.arb @@ -1,45 +1,48 @@ { + "mobileAllGames": "Kaikki pelit", + "mobileAreYouSure": "Oletko varma?", + "mobileBlindfoldMode": "Sokko", + "mobileCancelTakebackOffer": "Peruuta siirron peruutuspyyntö", + "mobileClearButton": "Tyhjennä", + "mobileCorrespondenceClearSavedMove": "Poista tallennettu siirto", + "mobileCustomGameJoinAGame": "Liity peliin", + "mobileFeedbackButton": "Palaute", + "mobileGreeting": "Hei {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Piilota muunnelma", "mobileHomeTab": "Etusivu", - "mobilePuzzlesTab": "Tehtävät", - "mobileToolsTab": "Työkalut", - "mobileWatchTab": "Seuraa", - "mobileSettingsTab": "Asetukset", + "mobileLiveStreamers": "Live-striimaajat", "mobileMustBeLoggedIn": "Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.", - "mobileSystemColors": "Järjestelmän värit", - "mobileFeedbackButton": "Palaute", + "mobileNoSearchResults": "Ei hakutuloksia", + "mobileNotFollowingAnyUser": "Et seuraa yhtäkään käyttäjää.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Pelaajat, joiden tunnuksesta löytyy \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Suurenna vedettävä nappula", + "mobilePuzzleStormConfirmEndRun": "Haluatko lopettaa tämän sarjan?", + "mobilePuzzleStormFilterNothingToShow": "Ei näytettävää, muuta suodatusehtoja", + "mobilePuzzleStormNothingToShow": "Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.", + "mobilePuzzleStormSubtitle": "Ratkaise mahdollisimman monta tehtävää 3 minuutissa.", + "mobilePuzzleStreakAbortWarning": "Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.", + "mobilePuzzleThemesSubtitle": "Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.", + "mobilePuzzlesTab": "Tehtävät", + "mobileRecentSearches": "Viimeisimmät haut", "mobileSettingsHapticFeedback": "Kosketuspalaute", "mobileSettingsImmersiveMode": "Kokoruututila", "mobileSettingsImmersiveModeSubtitle": "Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.", - "mobileNotFollowingAnyUser": "Et seuraa yhtäkään käyttäjää.", - "mobileAllGames": "Kaikki pelit", - "mobileRecentSearches": "Viimeisimmät haut", - "mobileClearButton": "Tyhjennä", - "mobileNoSearchResults": "Ei hakutuloksia", - "mobileAreYouSure": "Oletko varma?", - "mobilePuzzleStreakAbortWarning": "Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.", - "mobilePuzzleStormNothingToShow": "Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.", - "mobileSharePuzzle": "Jaa tämä tehtävä", - "mobileShareGameURL": "Jaa pelin URL", + "mobileSettingsTab": "Asetukset", "mobileShareGamePGN": "Jaa PGN", + "mobileShareGameURL": "Jaa pelin URL", "mobileSharePositionAsFEN": "Jaa asema FEN:nä", - "mobileShowVariations": "Näytä muunnelmat", - "mobileHideVariation": "Piilota muunnelma", + "mobileSharePuzzle": "Jaa tämä tehtävä", "mobileShowComments": "Näytä kommentit", - "mobilePuzzleStormConfirmEndRun": "Haluatko lopettaa tämän sarjan?", - "mobilePuzzleStormFilterNothingToShow": "Ei näytettävää, muuta suodatusehtoja", - "mobileCancelTakebackOffer": "Peruuta siirron peruutuspyyntö", - "mobileCancelDrawOffer": "Peruuta tasapeliehdotus", - "mobileWaitingForOpponentToJoin": "Odotetaan vastustajan löytymistä...", - "mobileBlindfoldMode": "Sokko", - "mobileCustomGameJoinAGame": "Liity peliin", - "mobileCorrespondenceClearSavedMove": "Poista tallennettu siirto", - "mobileSomethingWentWrong": "Jokin meni vikaan.", "mobileShowResult": "Näytä lopputulos", - "mobilePuzzleThemesSubtitle": "Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.", - "mobilePuzzleStormSubtitle": "Ratkaise mahdollisimman monta tehtävää 3 minuutissa.", - "mobileGreeting": "Hei {param}", - "mobileGreetingWithoutName": "Hei", + "mobileShowVariations": "Näytä muunnelmat", + "mobileSomethingWentWrong": "Jokin meni vikaan.", + "mobileSystemColors": "Järjestelmän värit", + "mobileTheme": "Teema", + "mobileToolsTab": "Työkalut", + "mobileWaitingForOpponentToJoin": "Odotetaan vastustajan löytymistä...", + "mobileWatchTab": "Seuraa", "activityActivity": "Toiminta", "activityHostedALiveStream": "Piti livestreamin", "activityRankedInSwissTournament": "Tuli {param1}. sijalle turnauksessa {param2}", @@ -52,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Pelasi {count} siirtoa} other{Pelasi {count} siirtoa}}", "activityInNbCorrespondenceGames": "{count, plural, =1{{count} kirjeshakkipelissä} other{{count} kirjeshakkipelissä}}", "activityCompletedNbGames": "{count, plural, =1{Pelasi {count} kirjeshakkipelin} other{Pelasi {count} kirjeshakkipeliä}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Pelasi {count} {param2}-kirjeshakkipelin} other{Pelasi {count} {param2}-kirjeshakkipeliä}}", "activityFollowedNbPlayers": "{count, plural, =1{Alkoi seuraamaan {count} pelaajaa} other{Alkoi seuraamaan {count} pelaajaa}}", "activityGainedNbFollowers": "{count, plural, =1{Sai {count} uuden seuraajan} other{Sai {count} uutta seuraajaa}}", "activityHostedNbSimuls": "{count, plural, =1{Piti {count} simultaanin} other{Piti {count} simultaania}}", @@ -62,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Osallistui {count} sveitsiläiseen turnaukseen} other{Osallistui {count} sveitsiläiseen turnaukseen}}", "activityJoinedNbTeams": "{count, plural, =1{Liittyi {count} joukkueeseen} other{Liittyi {count} joukkueeseen}}", "broadcastBroadcasts": "Lähetykset", + "broadcastMyBroadcasts": "Omat lähetykset", "broadcastLiveBroadcasts": "Suorat lähetykset turnauksista", + "broadcastBroadcastCalendar": "Lähetyskalenteri", + "broadcastNewBroadcast": "Uusi livelähetys", + "broadcastSubscribedBroadcasts": "Tilatut lähetykset", + "broadcastAboutBroadcasts": "Lähetyksistä", + "broadcastHowToUseLichessBroadcasts": "Kuinka Lichess-lähetyksiä käytetään.", + "broadcastTheNewRoundHelp": "Uudella kierroksella on samat jäsenet ja osallistujat kuin edellisellä.", + "broadcastAddRound": "Lisää kierros", + "broadcastOngoing": "Käynnissä", + "broadcastUpcoming": "Tulossa", + "broadcastCompleted": "Päättyneet", + "broadcastCompletedHelp": "Lichess tunnistaa lähteenä olevista peleistä, milloin kierros on viety päätökseen. Lähteen puuttuessa voit käyttää tätä asetusta.", + "broadcastRoundName": "Kierroksen nimi", + "broadcastRoundNumber": "Kierroksen numero", + "broadcastTournamentName": "Turnauksen nimi", + "broadcastTournamentDescription": "Turnauksen lyhyt kuvaus", + "broadcastFullDescription": "Täysimittainen kuvaus tapahtumasta", + "broadcastFullDescriptionHelp": "Ei-pakollinen pitkä kuvaus lähetyksestä. {param1}-muotoiluja voi käyttää. Pituus voi olla enintään {param2} merkkiä.", + "broadcastSourceSingleUrl": "PGN:n lähde-URL", + "broadcastSourceUrlHelp": "URL, josta Lichess hakee PGN-päivitykset. Sen täytyy olla julkisesti saatavilla internetissä.", + "broadcastSourceGameIds": "Korkeintaan 64 Lichess-pelin tunnistenumeroa välilyönneillä eroteltuna.", + "broadcastStartDateTimeZone": "Alkamisajankohta turnauksen paikallisella aikavyöhykkeellä: {param}", + "broadcastStartDateHelp": "Ei-pakollinen, laita jos tiedät milloin tapahtuma alkaa", + "broadcastCurrentGameUrl": "Tämän pelin URL", + "broadcastDownloadAllRounds": "Lataa kaikki kierrokset", + "broadcastResetRound": "Nollaa tämä kierros", + "broadcastDeleteRound": "Poista tämä kierros", + "broadcastDefinitivelyDeleteRound": "Poista kierros ja sen pelit lopullisesti.", + "broadcastDeleteAllGamesOfThisRound": "Poista kaikki tämän kierroksen pelit. Lähteen on oltava aktiivinen, jotta pelit voidaan luoda uudelleen.", + "broadcastEditRoundStudy": "Kierrostutkielman muokkaus", + "broadcastDeleteTournament": "Poista tämä turnaus", + "broadcastDefinitivelyDeleteTournament": "Poista lopullisesti koko turnaus, sen kaikki kierrokset ja kaikki pelit.", + "broadcastShowScores": "Näytä pelaajien pisteet pelien tulosten pohjalta", + "broadcastReplacePlayerTags": "Valinnainen: korvaa pelaajien nimet, vahvuusluvut ja arvonimet", + "broadcastFideFederations": "FIDEn liitot", + "broadcastTop10Rating": "Top 10 -vahvuuslukulista", + "broadcastFidePlayers": "FIDE-pelaajat", + "broadcastFidePlayerNotFound": "FIDE-pelaajaa ei löytynyt", + "broadcastFideProfile": "FIDE-profiili", + "broadcastFederation": "Kansallinen liitto", + "broadcastAgeThisYear": "Ikä tänä vuonna", + "broadcastUnrated": "Pisteyttämätön", + "broadcastRecentTournaments": "Viimeisimmät turnaukset", + "broadcastOpenLichess": "Avaa Lichessissä", + "broadcastTeams": "Joukkueet", + "broadcastBoards": "Laudat", + "broadcastOverview": "Pääsivu", + "broadcastSubscribeTitle": "Tilaa ilmoitukset kunkin kierroksen alkamisesta. Käyttäjätunnuksesi asetuksista voit kytkeä ääni- ja puskuilmoitukset päälle tai pois.", + "broadcastUploadImage": "Lisää turnauksen kuva", + "broadcastNoBoardsYet": "Pelilautoja ei vielä ole. Ne tulevat näkyviin sitä mukaa, kun pelit ladataan tänne.", + "broadcastBoardsCanBeLoaded": "Laudat voidaan ladata lähteen kautta tai {param} kautta", + "broadcastStartsAfter": "Alkaa {param}:n jälkeen", + "broadcastStartVerySoon": "Lähetys alkaa aivan pian.", + "broadcastNotYetStarted": "Lähetys ei ole vielä alkanut.", + "broadcastOfficialWebsite": "Virallinen verkkosivu", + "broadcastStandings": "Tulostaulu", + "broadcastOfficialStandings": "Virallinen tulostaulu", + "broadcastIframeHelp": "Lisäasetuksia löytyy {param}", + "broadcastWebmastersPage": "webmasterin sivulta", + "broadcastPgnSourceHelp": "Tämän kierroksen julkinen ja reaaliaikainen PGN-tiedosto. Nopeampaan ja tehokkaampaan synkronisointiin on tarjolla myös {param}.", + "broadcastEmbedThisBroadcast": "Upota tämä lähetys sivustoosi", + "broadcastEmbedThisRound": "Upota {param} sivustoosi", + "broadcastRatingDiff": "Vahvuuslukujen erotus", + "broadcastGamesThisTournament": "Pelit tässä turnauksessa", + "broadcastScore": "Pisteet", + "broadcastAllTeams": "Kaikki joukkueet", + "broadcastTournamentFormat": "Turnauksen laji", + "broadcastTournamentLocation": "Turnauksen sijainti", + "broadcastTopPlayers": "Parhaat pelaajat", + "broadcastTimezone": "Aikavyöhyke", + "broadcastFideRatingCategory": "Kategoria (FIDE-vahvuuslukujen mukaan)", + "broadcastOptionalDetails": "Mahdolliset lisätiedot", + "broadcastPastBroadcasts": "Menneet lähetykset", + "broadcastAllBroadcastsByMonth": "Näytä kaikki lähetykset kuukausikohtaisesti", + "broadcastNbBroadcasts": "{count, plural, =1{{count} lähetys} other{{count} lähetystä}}", "challengeChallengesX": "Haasteet: {param1}", "challengeChallengeToPlay": "Haasta peliin", "challengeChallengeDeclined": "Haasteesta kieltäydyttiin", @@ -186,6 +265,7 @@ "preferencesNotifyWeb": "Selain", "preferencesNotifyDevice": "Laite", "preferencesBellNotificationSound": "Ilmoitusten kilahdusääni", + "preferencesBlindfold": "Sokko", "puzzlePuzzles": "Tehtävät", "puzzlePuzzleThemes": "Tehtävien aiheet", "puzzleRecommended": "Suosittelemme", @@ -381,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Nappula uhkaa tai puolustaa ruutua vastustajan nappulan läpi.", "puzzleThemeZugzwang": "Siirtopakko", "puzzleThemeZugzwangDescription": "Vastustajalla on rajoitettu määrä mahdollisia siirtoja, ja niistä kaikki heikentävät hänen asemaansa.", - "puzzleThemeHealthyMix": "Terve sekoitus", - "puzzleThemeHealthyMixDescription": "Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.", + "puzzleThemeMix": "Terve sekoitus", + "puzzleThemeMixDescription": "Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.", "puzzleThemePlayerGames": "Pelaajan peleistä", "puzzleThemePlayerGamesDescription": "Tehtäviä sinun tai jonkun toisen yksittäisen pelaajan peleistä.", "puzzleThemePuzzleDownloadInformation": "Nämä tehtävät ovat vapaasti käytettävissä ja ladattavissa osoitteesta {param}.", @@ -503,7 +583,6 @@ "replayMode": "Toistotapa", "realtimeReplay": "Reaaliaik.", "byCPL": "Virheet", - "openStudy": "Avaa tutkielma", "enable": "Käytössä", "bestMoveArrow": "Parhaan siirron nuoli", "showVariationArrows": "Näytä muunnelman nuolet", @@ -513,7 +592,6 @@ "memory": "Muistia", "infiniteAnalysis": "Loputon analyysi", "removesTheDepthLimit": "Poistaa syvyysrajoituksen ja pitää koneesi lämpöisenä", - "engineManager": "Moottorin hallinta", "blunder": "Vakava virhe", "mistake": "Virhe", "inaccuracy": "Epätarkkuus", @@ -595,6 +673,7 @@ "rank": "Sijoitus", "rankX": "Sijoitus: {param}", "gamesPlayed": "Pelattuja pelejä", + "ok": "OK", "cancel": "Peruuta", "whiteTimeOut": "Valkealta loppui aika", "blackTimeOut": "Mustalta loppui aika", @@ -711,7 +790,6 @@ "block": "Estä", "blocked": "Estetty", "unblock": "Poista esto", - "followsYou": "Seuraa sinua", "xStartedFollowingY": "{param1} alkoi seurata {param2}", "more": "Lisää", "memberSince": "Liittynyt", @@ -817,7 +895,9 @@ "cheat": "Huijaus", "troll": "Trolli", "other": "Muu", - "reportDescriptionHelp": "Liitä linkki peliin/peleihin ja kerro, mikä on pielessä tämän käyttäjän käytöksessä. Älä vain sano että \"hän huijaa\", vaan kerro meille miksi ajattelet näin. Raporttisi käydään läpi nopeammin, jos se on kirjoitettu englanniksi.", + "reportCheatBoostHelp": "Liitä linkki peliin/peleihin ja kerro, mikä tämän käyttäjän toiminnassa on pielessä. Älä vain sano hänen huijaavan, vaan kerro meille, miksi olet päätellyt niin.", + "reportUsernameHelp": "Selitä, mikä tässä käyttäjätunnuksessa on loukkaavaa. Älä vain sano sen olevan loukkaava tai sopimaton, vaan kerro meille, mihin näkemyksesi perustuu, varsinkin jos loukkaus on epäsuora, muun kuin englanninkielinen, slangia, tai jos siinä viitataan kulttuuriin tai historiaan.", + "reportProcessedFasterInEnglish": "Ilmoituksesi käsitellään nopeammin, jos se on kirjoitettu englanniksi.", "error_provideOneCheatedGameLink": "Anna ainakin yksi linkki peliin, jossa epäilet huijaamista.", "by": "{param}", "importedByX": "Käyttäjän {param} tuoma", @@ -1215,6 +1295,7 @@ "showMeEverything": "Näytä kaikki", "lichessPatronInfo": "Lichess on hyväntekeväisyysjärjestö ja täysin ilmainen avoimen lähdekoodin ohjelmisto.\nKaikki toimintakustannukset, kehitystyö ja sisältö rahoitetaan yksinomaan käyttäjien lahjoituksilla.", "nothingToSeeHere": "Täällä ei ole tällä hetkellä mitään nähtävää.", + "stats": "Tilastot", "opponentLeftCounter": "{count, plural, =1{Vastustajasi on poistunut pelistä. Voit julistautua voittajaksi {count} sekunnin kuluttua.} other{Vastustajasi on poistunut pelistä. Voit julistautua voittajaksi {count} sekunnin kuluttua.}}", "mateInXHalfMoves": "{count, plural, =1{Matti {count} puolisiirrolla} other{Matti {count} puolisiirrolla}}", "nbBlunders": "{count, plural, =1{{count} vakava virhe} other{{count} vakavaa virhettä}}", @@ -1311,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 sarja} other{{count} sarjaa}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Pelasi yhden sarjan {param2}ia} other{Pelasi {count} sarjaa {param2}ia}}", "streamerLichessStreamers": "Lichess-striimaajat", + "studyPrivate": "Yksityinen", + "studyMyStudies": "Tutkielmani", + "studyStudiesIContributeTo": "Tutkielmat joihin olen osallisena", + "studyMyPublicStudies": "Julkiset tutkielmani", + "studyMyPrivateStudies": "Yksityiset tutkielmani", + "studyMyFavoriteStudies": "Suosikkitutkielmani", + "studyWhatAreStudies": "Mitä ovat tutkielmat?", + "studyAllStudies": "Kaikki tutkielmat", + "studyStudiesCreatedByX": "{param} luomat tutkielmat", + "studyNoneYet": "Ei mitään.", + "studyHot": "Suositut juuri nyt", + "studyDateAddedNewest": "Julkaisupäivä (uusimmat)", + "studyDateAddedOldest": "Julkaisupäivä (vanhimmat)", + "studyRecentlyUpdated": "Viimeksi päivitetyt", + "studyMostPopular": "Suosituimmat", + "studyAlphabetical": "Aakkosjärjestyksessä", + "studyAddNewChapter": "Lisää uusi luku", + "studyAddMembers": "Lisää jäseniä", + "studyInviteToTheStudy": "Kutsu tutkielmaan", + "studyPleaseOnlyInvitePeopleYouKnow": "Kutsu vain ihmisiä, jotka tunnet ja jotka haluavat osallistua aktiivisesti.", + "studySearchByUsername": "Hae käyttäjätunnuksella", + "studySpectator": "Katsoja", + "studyContributor": "Osallistuja", + "studyKick": "Poista", + "studyLeaveTheStudy": "Jätä tutkielma", + "studyYouAreNowAContributor": "Olet nyt osallistuja", + "studyYouAreNowASpectator": "Olet nyt katsoja", + "studyPgnTags": "PGN-tunnisteet", + "studyLike": "Tykkää", + "studyUnlike": "Poista tykkäys", + "studyNewTag": "Uusi tunniste", + "studyCommentThisPosition": "Kommentoi asemaa", + "studyCommentThisMove": "Kommentoi siirtoa", + "studyAnnotateWithGlyphs": "Arvioi symbolein", + "studyTheChapterIsTooShortToBeAnalysed": "Luku on liian lyhyt analysoitavaksi.", + "studyOnlyContributorsCanRequestAnalysis": "Vain tutkielman osallistujat voivat pyytää tietokoneanalyysin.", + "studyGetAFullComputerAnalysis": "Hanki palvelimelta täysi tietokoneanalyysi päälinjasta.", + "studyMakeSureTheChapterIsComplete": "Varmista, että luku on valmis. Voit pyytää analyysiä vain kerran.", + "studyAllSyncMembersRemainOnTheSamePosition": "Kaikki SYNC-jäsenet pysyvät samassa asemassa", + "studyShareChanges": "Anna katsojien nähdä muutokset ja tallenna ne palvelimelle", + "studyPlaying": "Meneillään", + "studyShowEvalBar": "Arviopalkit", + "studyFirst": "Alkuun", + "studyPrevious": "Edellinen", + "studyNext": "Seuraava", + "studyLast": "Loppuun", "studyShareAndExport": "Jaa & vie", - "studyStart": "Aloita" + "studyCloneStudy": "Kloonaa", + "studyStudyPgn": "Tutkielman PGN", + "studyDownloadAllGames": "Lataa kaikki pelit", + "studyChapterPgn": "Luvun PGN", + "studyCopyChapterPgn": "Kopioi PGN", + "studyDownloadGame": "Lataa peli", + "studyStudyUrl": "Tutkielman URL", + "studyCurrentChapterUrl": "Tämän luvun URL", + "studyYouCanPasteThisInTheForumToEmbed": "Voit upottaa tämän foorumiin liittämällä", + "studyStartAtInitialPosition": "Aloita alkuperäisestä asemasta", + "studyStartAtX": "Aloita siirrosta {param}", + "studyEmbedInYourWebsite": "Upota sivustoosi tai blogiisi", + "studyReadMoreAboutEmbedding": "Lue lisää upottamisesta", + "studyOnlyPublicStudiesCanBeEmbedded": "Vain julkiset tutkielmat voidaan upottaa!", + "studyOpen": "Avaa", + "studyXBroughtToYouByY": "{param1}, sivustolta {param2}", + "studyStudyNotFound": "Tutkielmaa ei löydy", + "studyEditChapter": "Muokkaa lukua", + "studyNewChapter": "Uusi luku", + "studyImportFromChapterX": "Tuo luvusta {param}", + "studyOrientation": "Suunta", + "studyAnalysisMode": "Analyysitila", + "studyPinnedChapterComment": "Kiinnitetty lukukommentti", + "studySaveChapter": "Tallenna luku", + "studyClearAnnotations": "Poista kommentit", + "studyClearVariations": "Tyhjennä muunnelmat", + "studyDeleteChapter": "Poista luku", + "studyDeleteThisChapter": "Poistetaanko tämä luku? Et voi palauttaa sitä enää!", + "studyClearAllCommentsInThisChapter": "Poista kaikki kommentit, symbolit ja piirtokuviot tästä luvusta?", + "studyRightUnderTheBoard": "Heti laudan alla", + "studyNoPinnedComment": "Ei", + "studyNormalAnalysis": "Tavallinen analyysi", + "studyHideNextMoves": "Piilota tulevat siirrot", + "studyInteractiveLesson": "Interaktiivinen oppitunti", + "studyChapterX": "Luku {param}", + "studyEmpty": "Tyhjä", + "studyStartFromInitialPosition": "Aloita alkuasemasta", + "studyEditor": "Editori", + "studyStartFromCustomPosition": "Aloita haluamastasi asemasta", + "studyLoadAGameByUrl": "Lataa peli URL:stä", + "studyLoadAPositionFromFen": "Lataa asema FEN:istä", + "studyLoadAGameFromPgn": "Ota peli PGN:stä", + "studyAutomatic": "Automaattinen", + "studyUrlOfTheGame": "URL peliin", + "studyLoadAGameFromXOrY": "Lataa peli lähteestä {param1} tai {param2}", + "studyCreateChapter": "Aloita luku", + "studyCreateStudy": "Luo tutkielma", + "studyEditStudy": "Muokkaa tutkielmaa", + "studyVisibility": "Näkyvyys", + "studyPublic": "Julkinen", + "studyUnlisted": "Listaamaton", + "studyInviteOnly": "Vain kutsutut", + "studyAllowCloning": "Salli kloonaus", + "studyNobody": "Ei kukaan", + "studyOnlyMe": "Vain minä", + "studyContributors": "Osallistujat", + "studyMembers": "Jäsenet", + "studyEveryone": "Kaikki", + "studyEnableSync": "Synkronointi käyttöön", + "studyYesKeepEveryoneOnTheSamePosition": "Kyllä: pidä kaikki samassa asemassa", + "studyNoLetPeopleBrowseFreely": "Ei: anna ihmisten selata vapaasti", + "studyPinnedStudyComment": "Kiinnitetty tutkielmakommentti", + "studyStart": "Aloita", + "studySave": "Tallenna", + "studyClearChat": "Tyhjennä keskustelu", + "studyDeleteTheStudyChatHistory": "Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää!", + "studyDeleteStudy": "Poista tutkielma", + "studyConfirmDeleteStudy": "Poistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: {param}", + "studyWhereDoYouWantToStudyThat": "Missä haluat tutkia tätä?", + "studyGoodMove": "Hyvä siirto", + "studyMistake": "Virhe", + "studyBrilliantMove": "Loistava siirto", + "studyBlunder": "Vakava virhe", + "studyInterestingMove": "Mielenkiintoinen siirto", + "studyDubiousMove": "Kyseenalainen siirto", + "studyOnlyMove": "Ainoa siirto", + "studyZugzwang": "Siirtopakko", + "studyEqualPosition": "Tasainen asema", + "studyUnclearPosition": "Epäselvä asema", + "studyWhiteIsSlightlyBetter": "Valkealla on pieni etu", + "studyBlackIsSlightlyBetter": "Mustalla on pieni etu", + "studyWhiteIsBetter": "Valkealla on etu", + "studyBlackIsBetter": "Mustalla on etu", + "studyWhiteIsWinning": "Valkea on voitolla", + "studyBlackIsWinning": "Musta on voitolla", + "studyNovelty": "Uutuus", + "studyDevelopment": "Kehitys", + "studyInitiative": "Aloite", + "studyAttack": "Hyökkäys", + "studyCounterplay": "Vastapeli", + "studyTimeTrouble": "Aikapula", + "studyWithCompensation": "Kompensaatio", + "studyWithTheIdea": "Ideana", + "studyNextChapter": "Seuraava luku", + "studyPrevChapter": "Edellinen luku", + "studyStudyActions": "Tutkielmatoiminnot", + "studyTopics": "Aiheet", + "studyMyTopics": "Omat aiheeni", + "studyPopularTopics": "Suositut aiheet", + "studyManageTopics": "Aiheiden hallinta", + "studyBack": "Takaisin", + "studyPlayAgain": "Pelaa uudelleen", + "studyWhatWouldYouPlay": "Mitä pelaisit tässä asemassa?", + "studyYouCompletedThisLesson": "Onnittelut! Olet suorittanut tämän oppiaiheen.", + "studyPerPage": "{param} per sivu", + "studyNbChapters": "{count, plural, =1{{count} luku} other{{count} lukua}}", + "studyNbGames": "{count, plural, =1{{count} peli} other{{count} peliä}}", + "studyNbMembers": "{count, plural, =1{{count} jäsen} other{{count} jäsentä}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Liitä PGN tähän, enintään {count} peli} other{Liitä PGN tähän, enintään {count} peliä}}", + "timeagoJustNow": "juuri äsken", + "timeagoRightNow": "juuri nyt", + "timeagoCompleted": "suoritettu", + "timeagoInNbSeconds": "{count, plural, =1{{count} sekunnin kuluttua} other{{count} sekunnin kuluttua}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} minuutin kuluttua} other{{count} minuutin kuluttua}}", + "timeagoInNbHours": "{count, plural, =1{{count} tunnin kuluttua} other{{count} tunnin kuluttua}}", + "timeagoInNbDays": "{count, plural, =1{{count} päivän kuluttua} other{{count} päivän kuluttua}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} viikon kuluttua} other{{count} viikon kuluttua}}", + "timeagoInNbMonths": "{count, plural, =1{{count} kuukauden kuluttua} other{{count} kuukauden kuluttua}}", + "timeagoInNbYears": "{count, plural, =1{{count} vuoden kuluttua} other{{count} vuoden kuluttua}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuutti sitten} other{{count} minuuttia sitten}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} tunti sitten} other{{count} tuntia sitten}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} päivä sitten} other{{count} päivää sitten}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} viikko sitten} other{{count} viikkoa sitten}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} kuukausi sitten} other{{count} kuukautta sitten}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} vuosi sitten} other{{count} vuotta sitten}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuutti jäljellä} other{{count} minuuttia jäljellä}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} tunti jäljellä} other{{count} tuntia jäljellä}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fo.arb b/lib/l10n/lila_fo.arb index 68a210975a..28cd5b0f21 100644 --- a/lib/l10n/lila_fo.arb +++ b/lib/l10n/lila_fo.arb @@ -22,6 +22,15 @@ "activityJoinedNbTeams": "{count, plural, =1{Fór upp í {count} lið} other{Fór upp í {count} lið}}", "broadcastBroadcasts": "Sendingar", "broadcastLiveBroadcasts": "Beinleiðis sendingar frá kappingum", + "broadcastNewBroadcast": "Nýggj beinleiðis sending", + "broadcastOngoing": "Í gongd", + "broadcastUpcoming": "Komandi", + "broadcastCompleted": "Liðug sending", + "broadcastRoundNumber": "Nummar á umfari", + "broadcastFullDescription": "Fullfíggjað lýsing av tiltaki", + "broadcastFullDescriptionHelp": "Valfrí long lýsing av sending. {param1} er tøkt. Longdin má vera styttri enn {param2} bókstavir.", + "broadcastSourceUrlHelp": "URL-leinki, ið Lichess fer at kanna til tess at fáa PGN dagføringar. Leinkið nýtist at vera alment atkomiligt á alnetinum.", + "broadcastStartDateHelp": "Valfrítt, um tú veitst, nær tiltakið byrjar", "challengeChallengeToPlay": "Bjóða av at telva", "challengeChallengeDeclined": "Avbjóðing avvíst", "challengeChallengeAccepted": "Avbjóðing góðtikin!", @@ -255,8 +264,8 @@ "puzzleThemeXRayAttackDescription": "Eitt fólk loypur á ella verjir ein punt gjøgnum eitt mótstøðufólk.", "puzzleThemeZugzwang": "Leiktvingsil", "puzzleThemeZugzwangDescription": "Mótleikarin hevur avmarkaðar møguleikar at flyta, og allir leikir gera støðu hansara verri.", - "puzzleThemeHealthyMix": "Sunt bland", - "puzzleThemeHealthyMixDescription": "Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.", + "puzzleThemeMix": "Sunt bland", + "puzzleThemeMixDescription": "Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.", "searchSearch": "Leita", "settingsSettings": "Stillingar", "settingsCloseAccount": "Lat kontu aftur", @@ -361,7 +370,6 @@ "replayMode": "Endurspælsháttur", "realtimeReplay": "Verulig tíð", "byCPL": "Við CPL", - "openStudy": "Lat rannsókn upp", "enable": "Loyv", "bestMoveArrow": "Pílur fyri besta leik", "evaluationGauge": "Eftirmetingarmát", @@ -531,7 +539,6 @@ "block": "Forða", "blocked": "Forðaður", "unblock": "Forða ikki", - "followsYou": "Fylgir tær", "xStartedFollowingY": "{param1} byrjaði at fylgja {param2}", "more": "Meira", "memberSince": "Limur síðani", @@ -624,7 +631,6 @@ "cheat": "Snýt", "troll": "Trøll", "other": "Annað", - "reportDescriptionHelp": "Flyt leinkið til talvið ella talvini higar, og greið frá, hvat bagir atburðinum hjá brúkaranum. Skriva ikki bert \"hann snýtir\", men sig okkum, hvussu tú komst til hesa niðurstøðu. Fráboðan tín verður skjótari viðgjørd, um hon verður skrivað á enskum.", "error_provideOneCheatedGameLink": "Útvega leinki til í minsta lagi eitt talv, har snýtt varð.", "by": "eftir {param}", "importedByX": "{param} las inn", @@ -1002,6 +1008,126 @@ "stormXRuns": "{count, plural, =1{1 umfar} other{{count} umfør}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Telvaði eitt umfar av {param2}} other{Telvaði {count} umfør av {param2}}}", "streamerLichessStreamers": "Lichess stroymarar", + "studyPrivate": "Egin (privat)", + "studyMyStudies": "Mínar rannsóknir", + "studyStudiesIContributeTo": "Rannsóknir, eg gevi mítt íkast til", + "studyMyPublicStudies": "Mínar almennu rannsóknir", + "studyMyPrivateStudies": "Mínar egnu rannsóknir", + "studyMyFavoriteStudies": "Mínar yndisrannsóknir", + "studyWhatAreStudies": "Hvat eru rannsóknir?", + "studyAllStudies": "Allar rannsóknir", + "studyStudiesCreatedByX": "{param} stovnaði hesar rannsóknir", + "studyNoneYet": "Ongar enn.", + "studyHot": "Heitar", + "studyDateAddedNewest": "Eftir dagfesting (nýggjastu)", + "studyDateAddedOldest": "Eftir dagfesting (eldstu)", + "studyRecentlyUpdated": "Nýliga dagførdar", + "studyMostPopular": "Best dámdu", + "studyAddNewChapter": "Skoyt nýggjan kapittul upp í", + "studyAddMembers": "Legg limir aftrat", + "studyInviteToTheStudy": "Bjóða uppí rannsóknina", + "studyPleaseOnlyInvitePeopleYouKnow": "Bjóða vinaliga bert fólki, tú kennir, og sum vilja taka virknan lut í rannsóknini.", + "studySearchByUsername": "Leita eftir brúkaranavni", + "studySpectator": "Áskoðari", + "studyContributor": "Gevur íkast", + "studyKick": "Koyr úr", + "studyLeaveTheStudy": "Far úr rannsóknini", + "studyYouAreNowAContributor": "Tú ert nú ein, ið leggur aftrat rannsóknini", + "studyYouAreNowASpectator": "Tú ert nú áskoðari", + "studyPgnTags": "PGN-frámerki", + "studyLike": "Dáma", + "studyNewTag": "Nýtt frámerki", + "studyCommentThisPosition": "Viðmerk hesa støðuna", + "studyCommentThisMove": "Viðmerk henda leikin", + "studyAnnotateWithGlyphs": "Skriva við teknum", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittulin er ov stuttur til at verða greinaður.", + "studyOnlyContributorsCanRequestAnalysis": "Bert tey, ið geva sítt íkast til rannsóknina, kunnu biðja um eina teldugreining.", + "studyGetAFullComputerAnalysis": "Fá eina fullfíggjaða teldugreining av høvuðsbrigdinum frá ambætaranum.", + "studyMakeSureTheChapterIsComplete": "Tryggja tær, at kapittulin er fullfíggjaður. Tú kanst bert biðja um greining eina ferð.", + "studyAllSyncMembersRemainOnTheSamePosition": "Allir SYNC-limir verða verandi í somu støðu", + "studyShareChanges": "Deil broytingar við áskoðarar, og goym tær á ambætaranum", + "studyPlaying": "Í gongd", + "studyFirst": "Fyrsta", + "studyPrevious": "Undanfarna", + "studyNext": "Næsta", + "studyLast": "Síðsta", "studyShareAndExport": "Deil & flyt út", - "studyStart": "Byrja" + "studyCloneStudy": "Klona", + "studyStudyPgn": "PGN rannsókn", + "studyDownloadAllGames": "Tak øll talv niður", + "studyChapterPgn": "PGN kapittul", + "studyDownloadGame": "Tak talv niður", + "studyStudyUrl": "URL rannsókn", + "studyCurrentChapterUrl": "Núverandi URL partur", + "studyYouCanPasteThisInTheForumToEmbed": "Tú kanst seta hetta inn í torgið at sýna tað har", + "studyStartAtInitialPosition": "Byrja við byrjanarstøðuni", + "studyStartAtX": "Byrja við {param}", + "studyEmbedInYourWebsite": "Fell inn í heimasíðu tína ella blogg tín", + "studyReadMoreAboutEmbedding": "Les meira um at fella inn í", + "studyOnlyPublicStudiesCanBeEmbedded": "Bert almennar rannsóknir kunnu verða feldar inn í!", + "studyOpen": "Lat upp", + "studyXBroughtToYouByY": "{param2} fekk tær {param1} til vegar", + "studyStudyNotFound": "Rannsókn ikki funnin", + "studyEditChapter": "Broyt kapittul", + "studyNewChapter": "Nýggjur kapittul", + "studyOrientation": "Helling", + "studyAnalysisMode": "Greiningarstøða", + "studyPinnedChapterComment": "Føst viðmerking til kapittulin", + "studySaveChapter": "Goym kapittulin", + "studyClearAnnotations": "Strika viðmerkingar", + "studyDeleteChapter": "Strika kapittul", + "studyDeleteThisChapter": "Strika henda kapittulin? Til ber ikki at angra!", + "studyClearAllCommentsInThisChapter": "Skulu allar viðmerkingar, øll tekn og teknað skap strikast úr hesum kapitli?", + "studyRightUnderTheBoard": "Beint undir talvborðinum", + "studyNoPinnedComment": "Einki", + "studyNormalAnalysis": "Vanlig greining", + "studyHideNextMoves": "Fjal næstu leikirnar", + "studyInteractiveLesson": "Samvirkin frálæra", + "studyChapterX": "Kapittul {param}", + "studyEmpty": "Tómur", + "studyStartFromInitialPosition": "Byrja við byrjanarstøðuni", + "studyEditor": "Ritstjóri", + "studyStartFromCustomPosition": "Byrja við støðu, ið brúkari ger av", + "studyLoadAGameByUrl": "Les inn talv frá URL", + "studyLoadAPositionFromFen": "Les inn talvstøðu frá FEN", + "studyLoadAGameFromPgn": "Les inn talv frá PGN", + "studyAutomatic": "Sjálvvirkið", + "studyUrlOfTheGame": "URL fyri talvini", + "studyLoadAGameFromXOrY": "Les talv inn frá {param1} ella {param2}", + "studyCreateChapter": "Stovna kapittul", + "studyCreateStudy": "Stovna rannsókn", + "studyEditStudy": "Ritstjórna rannsókn", + "studyVisibility": "Sýni", + "studyPublic": "Almen", + "studyUnlisted": "Ikki skrásett", + "studyInviteOnly": "Bert innboðin", + "studyAllowCloning": "Loyv kloning", + "studyNobody": "Eingin", + "studyOnlyMe": "Bert eg", + "studyContributors": "Luttakarar", + "studyMembers": "Limir", + "studyEveryone": "Øll", + "studyEnableSync": "Samstilling møgulig", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: varðveit øll í somu støðu", + "studyNoLetPeopleBrowseFreely": "Nei: lat fólk kaga frítt", + "studyPinnedStudyComment": "Føst rannsóknarviðmerking", + "studyStart": "Byrja", + "studySave": "Goym", + "studyClearChat": "Rudda kjatt", + "studyDeleteTheStudyChatHistory": "Skal kjattsøgan í rannsóknini strikast? Til ber ikki at angra!", + "studyDeleteStudy": "Burturbein rannsókn", + "studyWhereDoYouWantToStudyThat": "Hvar vilt tú rannsaka hatta?", + "studyGoodMove": "Góður leikur", + "studyMistake": "Mistak", + "studyBrilliantMove": "Framúrskarandi leikur", + "studyBlunder": "Bukkur", + "studyInterestingMove": "Áhugaverdur leikur", + "studyDubiousMove": "Ivasamur leikur", + "studyOnlyMove": "Einasti leikur", + "studyWhiteIsWinning": "Hvítur stendur til at vinna", + "studyBlackIsWinning": "Svartur stendur til at vinna", + "studyNbChapters": "{count, plural, =1{{count} kapittul} other{{count} kapitlar}}", + "studyNbGames": "{count, plural, =1{{count} talv} other{{count} talv}}", + "studyNbMembers": "{count, plural, =1{{count} limur} other{{count} limir}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Set PGN tekstin hjá tær inn her, upp til {count} talv} other{Set PGN tekstin hjá tær inn her, upp til {count} talv}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fr.arb b/lib/l10n/lila_fr.arb index c68f898a26..117a364366 100644 --- a/lib/l10n/lila_fr.arb +++ b/lib/l10n/lila_fr.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Toutes les parties", + "mobileAreYouSure": "Êtes-vous sûr(e) ?", + "mobileBlindfoldMode": "Partie à l'aveugle", + "mobileCancelTakebackOffer": "Annuler la proposition de reprise du coup", + "mobileClearButton": "Effacer", + "mobileCorrespondenceClearSavedMove": "Effacer les coups enregistrés", + "mobileCustomGameJoinAGame": "Joindre une partie", + "mobileFeedbackButton": "Commentaires", + "mobileGreeting": "Bonjour {param}", + "mobileGreetingWithoutName": "Bonjour", + "mobileHideVariation": "Masquer les variantes", "mobileHomeTab": "Accueil", - "mobilePuzzlesTab": "Problèmes", - "mobileToolsTab": "Outils", - "mobileWatchTab": "Regarder", - "mobileSettingsTab": "Paramètres", + "mobileLiveStreamers": "Diffuseurs en direct", "mobileMustBeLoggedIn": "Vous devez être connecté pour voir cette page.", - "mobileSystemColors": "Couleurs du système", - "mobileFeedbackButton": "Commentaires", + "mobileNoSearchResults": "Aucun résultat", + "mobileNotFollowingAnyUser": "Vous ne suivez aucun utilisateur.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Joueurs – \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Grossir la pièce déplacée", + "mobilePuzzleStormConfirmEndRun": "Voulez-vous mettre fin à cette série?", + "mobilePuzzleStormFilterNothingToShow": "Rien à afficher. Veuillez changer les filtres.", + "mobilePuzzleStormNothingToShow": "Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).", + "mobilePuzzleStormSubtitle": "Faites un maximum de problèmes en 3 minutes.", + "mobilePuzzleStreakAbortWarning": "Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.", + "mobilePuzzleThemesSubtitle": "Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.", + "mobilePuzzlesTab": "Problèmes", + "mobileRecentSearches": "Recherches récentes", "mobileSettingsHapticFeedback": "Mode vibration", "mobileSettingsImmersiveMode": "Mode plein écran", "mobileSettingsImmersiveModeSubtitle": "Masquer l'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l'interface système sur les bords de l'écran vous gênent. S'applique aux écrans de la partie et des problèmes (Puzzle Storm).", - "mobileNotFollowingAnyUser": "Vous ne suivez aucun utilisateur.", - "mobileAllGames": "Toutes les parties", - "mobileRecentSearches": "Recherches récentes", - "mobileClearButton": "Effacer", - "mobilePlayersMatchingSearchTerm": "Joueurs – \"{param}\"", - "mobileNoSearchResults": "Aucun résultat", - "mobileAreYouSure": "Êtes-vous sûr(e) ?", - "mobilePuzzleStreakAbortWarning": "Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.", - "mobilePuzzleStormNothingToShow": "Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).", - "mobileSharePuzzle": "Partager ce problème", - "mobileShareGameURL": "Partager l'URL de la partie", + "mobileSettingsTab": "Paramètres", "mobileShareGamePGN": "Partager le PGN", + "mobileShareGameURL": "Partager l'URL de la partie", "mobileSharePositionAsFEN": "Partager la position FEN", - "mobileShowVariations": "Afficher les variantes", - "mobileHideVariation": "Masquer les variantes", + "mobileSharePuzzle": "Partager ce problème", "mobileShowComments": "Afficher les commentaires", - "mobilePuzzleStormConfirmEndRun": "Voulez-vous mettre fin à cette série?", - "mobilePuzzleStormFilterNothingToShow": "Rien à afficher. Veuillez changer les filtres.", - "mobileCancelTakebackOffer": "Annuler la proposition de reprise du coup", - "mobileCancelDrawOffer": "Annuler la proposition de nulle", - "mobileWaitingForOpponentToJoin": "En attente d'un adversaire...", - "mobileBlindfoldMode": "Partie à l'aveugle", - "mobileLiveStreamers": "Diffuseurs en direct", - "mobileCustomGameJoinAGame": "Joindre une partie", - "mobileCorrespondenceClearSavedMove": "Effacer les coups enregistrés", - "mobileSomethingWentWrong": "Une erreur s'est produite.", "mobileShowResult": "Afficher le résultat", - "mobilePuzzleThemesSubtitle": "Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.", - "mobilePuzzleStormSubtitle": "Faites un maximum de problèmes en 3 minutes.", - "mobileGreeting": "Bonjour {param}", - "mobileGreetingWithoutName": "Bonjour", + "mobileShowVariations": "Afficher les variantes", + "mobileSomethingWentWrong": "Une erreur s'est produite.", + "mobileSystemColors": "Couleurs du système", + "mobileTheme": "Thème", + "mobileToolsTab": "Outils", + "mobileWaitingForOpponentToJoin": "En attente d'un adversaire...", + "mobileWatchTab": "Regarder", "activityActivity": "Activité", "activityHostedALiveStream": "A hébergé une diffusion en direct", "activityRankedInSwissTournament": "Classé {param1} dans le tournoi {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{A joué {count} coup} other{A joué {count} coups}}", "activityInNbCorrespondenceGames": "{count, plural, =1{dans {count} partie par correspondance} other{dans {count} parties par correspondance}}", "activityCompletedNbGames": "{count, plural, =1{{count} partie par correspondance terminée} other{{count} parties par correspondance terminées}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} partie {param2} par correspondance terminée} other{{count} parties {param2} par correspondance terminées}}", "activityFollowedNbPlayers": "{count, plural, =1{A commencé à suivre {count} joueur} other{A commencé à suivre {count} joueurs}}", "activityGainedNbFollowers": "{count, plural, =1{A gagné {count} nouveau suiveur} other{A gagné {count} nouveaux suiveurs}}", "activityHostedNbSimuls": "{count, plural, =1{A hébergé {count} simultanée} other{A hébergé {count} simultanées}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{A participé à {count} tournoi(s) suisse(s)} other{A participé à {count} tournois suisses}}", "activityJoinedNbTeams": "{count, plural, =1{A rejoint {count} équipe} other{A rejoint {count} équipes}}", "broadcastBroadcasts": "Diffusions", + "broadcastMyBroadcasts": "Ma diffusion", "broadcastLiveBroadcasts": "Diffusions de tournois en direct", + "broadcastBroadcastCalendar": "Calendrier des diffusions", + "broadcastNewBroadcast": "Nouvelle diffusion en direct", + "broadcastSubscribedBroadcasts": "Diffusions suivies", + "broadcastAboutBroadcasts": "À propos des diffusions", + "broadcastHowToUseLichessBroadcasts": "Comment utiliser les diffusions dans Lichess.", + "broadcastTheNewRoundHelp": "La nouvelle ronde aura les mêmes participants et contributeurs que la précédente.", + "broadcastAddRound": "Ajouter une ronde", + "broadcastOngoing": "En cours", + "broadcastUpcoming": "À venir", + "broadcastCompleted": "Terminé", + "broadcastCompletedHelp": "Lichess détecte la fin des rondes en fonction des parties sources. Utilisez cette option s'il n'y a pas de source.", + "broadcastRoundName": "Nom de la ronde", + "broadcastRoundNumber": "Numéro de la ronde", + "broadcastTournamentName": "Nom du tournoi", + "broadcastTournamentDescription": "Brève description du tournoi", + "broadcastFullDescription": "Description complète de l'événement", + "broadcastFullDescriptionHelp": "Description détaillée et optionnelle de la diffusion. {param1} est disponible. La longueur doit être inférieure à {param2} caractères.", + "broadcastSourceSingleUrl": "URL source de la partie en PGN", + "broadcastSourceUrlHelp": "URL que Lichess interrogera pour obtenir les mises à jour du PGN. Elle doit être accessible publiquement depuis Internet.", + "broadcastSourceGameIds": "Jusqu'à 64 ID de partie Lichess séparés par des espaces.", + "broadcastStartDateTimeZone": "Date de début du tournoi (fuseau horaire local) : {param}", + "broadcastStartDateHelp": "Facultatif, si vous savez quand l'événement commence", + "broadcastCurrentGameUrl": "URL de la partie en cours", + "broadcastDownloadAllRounds": "Télécharger toutes les rondes", + "broadcastResetRound": "Réinitialiser cette ronde", + "broadcastDeleteRound": "Supprimer cette ronde", + "broadcastDefinitivelyDeleteRound": "Supprimer définitivement la ronde et ses parties.", + "broadcastDeleteAllGamesOfThisRound": "Supprimer toutes les parties de la ronde. La source doit être active pour recréer les parties.", + "broadcastEditRoundStudy": "Modifier l'étude de la ronde", + "broadcastDeleteTournament": "Supprimer ce tournoi", + "broadcastDefinitivelyDeleteTournament": "Supprimer définitivement le tournoi, toutes ses rondes et toutes ses parties.", + "broadcastShowScores": "Afficher les résultats des joueurs en fonction des résultats des parties", + "broadcastReplacePlayerTags": "Facultatif : remplacer les noms des joueurs, les classements et les titres", + "broadcastFideFederations": "Fédérations FIDE", + "broadcastTop10Rating": "10 plus hauts classements", + "broadcastFidePlayers": "Joueurs FIDE", + "broadcastFidePlayerNotFound": "Joueur FIDE introuvable", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Fédération", + "broadcastAgeThisYear": "Âge cette année", + "broadcastUnrated": "Non classé", + "broadcastRecentTournaments": "Tournois récents", + "broadcastOpenLichess": "Ouvrir dans Lichess", + "broadcastTeams": "Équipes", + "broadcastBoards": "Échiquiers", + "broadcastOverview": "Survol", + "broadcastSubscribeTitle": "Abonnez-vous pour être averti du début de chaque ronde. Vous pouvez basculer entre une sonnerie ou une notification poussée pour les diffusions dans les préférences de votre compte.", + "broadcastUploadImage": "Téléverser une image pour le tournoi", + "broadcastNoBoardsYet": "Pas d'échiquiers pour le moment. Ils s'afficheront lorsque les parties seront téléversées.", + "broadcastBoardsCanBeLoaded": "Les échiquiers sont chargés à partir d'une source ou de l'{param}.", + "broadcastStartsAfter": "Commence après la {param}", + "broadcastStartVerySoon": "La diffusion commencera très bientôt.", + "broadcastNotYetStarted": "La diffusion n'a pas encore commencé.", + "broadcastOfficialWebsite": "Site Web officiel", + "broadcastStandings": "Classement", + "broadcastOfficialStandings": "Résultats officiels", + "broadcastIframeHelp": "Plus d'options sur la {param}", + "broadcastWebmastersPage": "page des webmestres", + "broadcastPgnSourceHelp": "Source PGN publique en temps réel pour cette ronde. Nous offrons également un {param} pour permettre une synchronisation rapide et efficace.", + "broadcastEmbedThisBroadcast": "Intégrer cette diffusion dans votre site Web", + "broadcastEmbedThisRound": "Intégrer la {param} dans votre site Web", + "broadcastRatingDiff": "Différence de cote", + "broadcastGamesThisTournament": "Partie de ce tournoi", + "broadcastScore": "Résultat", + "broadcastAllTeams": "Toutes les équipes", + "broadcastTournamentFormat": "Format du tournoi", + "broadcastTournamentLocation": "Lieu du tournoi", + "broadcastTopPlayers": "Meilleurs joueurs", + "broadcastTimezone": "Fuseau horaire", + "broadcastFideRatingCategory": "Catégorie FIDE", + "broadcastOptionalDetails": "Informations facultatives", + "broadcastPastBroadcasts": "Diffusions passées", + "broadcastAllBroadcastsByMonth": "Voir les diffusions par mois", + "broadcastNbBroadcasts": "{count, plural, =1{{count} diffusion} other{{count} diffusions}}", "challengeChallengesX": "Défis : {param1}", "challengeChallengeToPlay": "Défier ce joueur", "challengeChallengeDeclined": "Défi refusé", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Navigateur", "preferencesNotifyDevice": "Appareil", "preferencesBellNotificationSound": "Son de notification", + "preferencesBlindfold": "Partie à l'aveugle", "puzzlePuzzles": "Problèmes", "puzzlePuzzleThemes": "Thèmes des problèmes", "puzzleRecommended": "Recommandé", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Une pièce attaque ou défend une case, à travers une pièce ennemie.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "L'adversaire est limité dans les mouvements qu'il peut effectuer, et tous les coups aggravent sa position.", - "puzzleThemeHealthyMix": "Divers", - "puzzleThemeHealthyMixDescription": "Un peu de tout. Vous ne savez pas à quoi vous attendre ! Comme dans une vraie partie.", + "puzzleThemeMix": "Problèmes variés", + "puzzleThemeMixDescription": "Un peu de tout. Vous ne savez pas à quoi vous attendre! Comme dans une vraie partie.", "puzzleThemePlayerGames": "Parties de joueurs", "puzzleThemePlayerGamesDescription": "Problèmes tirés de vos parties ou de celles d'autres joueurs.", "puzzleThemePuzzleDownloadInformation": "Ces problèmes sont du domaine public et peuvent être téléchargés sur {param}.", @@ -505,7 +583,6 @@ "replayMode": "Rejouer la partie", "realtimeReplay": "Temps réel", "byCPL": "Par erreurs", - "openStudy": "Ouvrir l'analyse", "enable": "Activée", "bestMoveArrow": "Flèche du meilleur coup", "showVariationArrows": "Afficher les flèches de variantes", @@ -515,7 +592,6 @@ "memory": "Mémoire", "infiniteAnalysis": "Analyse infinie", "removesTheDepthLimit": "Désactive la profondeur limitée et fait chauffer votre ordinateur", - "engineManager": "Gestionnaire de moteur d'analyse", "blunder": "Gaffe", "mistake": "Erreur", "inaccuracy": "Imprécision", @@ -597,6 +673,7 @@ "rank": "Rang", "rankX": "Classement : {param}", "gamesPlayed": "Parties jouées", + "ok": "OK", "cancel": "Annuler", "whiteTimeOut": "Temps blanc écoulé", "blackTimeOut": "Temps noir écoulé", @@ -713,7 +790,6 @@ "block": "Bloquer", "blocked": "Bloqué", "unblock": "Débloquer", - "followsYou": "Vous suit", "xStartedFollowingY": "{param1} a suivi {param2}", "more": "Plus", "memberSince": "Membre depuis", @@ -819,7 +895,9 @@ "cheat": "Triche", "troll": "Troll", "other": "Autre", - "reportDescriptionHelp": "Copiez le(s) lien(s) vers les parties et expliquez en quoi le comportement de cet utilisateur est inapproprié. Ne dites pas juste \"il triche\", mais expliquez comment vous êtes arrivé à cette conclusion. Votre rapport sera traité plus vite s'il est écrit en anglais.", + "reportCheatBoostHelp": "Collez le lien vers la ou les parties et expliquez pourquoi le comportement de l'utilisateur est inapproprié. Ne dites pas juste « il triche »; expliquez comment vous êtes arrivé à cette conclusion.", + "reportUsernameHelp": "Expliquez pourquoi ce nom d'utilisateur est offensant. Ne dites pas simplement qu'il est choquant ou inapproprié; expliquez comment vous êtes arrivé à cette conclusion, surtout si l'insulte n'est pas claire, n'est pas en anglais, est en argot ou a une connotation historique ou culturelle.", + "reportProcessedFasterInEnglish": "Votre rapport sera traité plus rapidement s'il est rédigé en anglais.", "error_provideOneCheatedGameLink": "Merci de fournir au moins un lien vers une partie où il y a eu triche.", "by": "par {param}", "importedByX": "Importée par {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Tout afficher", "lichessPatronInfo": "Lichess est une association à but non lucratif et un logiciel open source entièrement libre.\nTous les coûts d'exploitation, le développement et le contenu sont financés uniquement par les dons des utilisateurs.", "nothingToSeeHere": "Rien à voir ici pour le moment.", + "stats": "Statistiques", "opponentLeftCounter": "{count, plural, =1{Votre adversaire a quitté la partie. Vous pourrez revendiquer la victoire dans {count} seconde.} other{Votre adversaire a quitté la partie. Vous pourrez revendiquer la victoire dans {count} secondes.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} demi-coup} other{Mate en {count} demi-coups}}", "nbBlunders": "{count, plural, =1{{count} gaffe} other{{count} gaffes}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 essai} other{{count} essais}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{A fait un essai de {param2}} other{A fait {count} essais de {param2}}}", "streamerLichessStreamers": "Streamers sur Lichess", + "studyPrivate": "Étude(s) privée(s)", + "studyMyStudies": "Mes études", + "studyStudiesIContributeTo": "Études auxquelles je participe", + "studyMyPublicStudies": "Mes études publiques", + "studyMyPrivateStudies": "Mes études privées", + "studyMyFavoriteStudies": "Mes études favorites", + "studyWhatAreStudies": "Qu'est-ce qu'une étude ?", + "studyAllStudies": "Toutes les études", + "studyStudiesCreatedByX": "Études créées par {param}", + "studyNoneYet": "Aucune étude.", + "studyHot": "Populaire(s)", + "studyDateAddedNewest": "Date d'ajout (dernier ajout)", + "studyDateAddedOldest": "Date d'ajout (premier ajout)", + "studyRecentlyUpdated": "Récemment mis à jour", + "studyMostPopular": "Études les plus populaires", + "studyAlphabetical": "Alphabétique", + "studyAddNewChapter": "Ajouter un nouveau chapitre", + "studyAddMembers": "Ajouter des membres", + "studyInviteToTheStudy": "Inviter à l'étude", + "studyPleaseOnlyInvitePeopleYouKnow": "Veuillez n'inviter que des personnes qui vous connaissent et qui souhaitent activement participer à cette étude.", + "studySearchByUsername": "Rechercher par nom d'utilisateur", + "studySpectator": "Spectateur", + "studyContributor": "Contributeur", + "studyKick": "Éjecter", + "studyLeaveTheStudy": "Quitter l'étude", + "studyYouAreNowAContributor": "Vous êtes maintenant un contributeur", + "studyYouAreNowASpectator": "Vous êtes maintenant un spectateur", + "studyPgnTags": "Étiquettes PGN", + "studyLike": "Aimer", + "studyUnlike": "Je n’aime pas", + "studyNewTag": "Nouvelle étiquette", + "studyCommentThisPosition": "Commenter la position", + "studyCommentThisMove": "Commenter ce coup", + "studyAnnotateWithGlyphs": "Annoter avec des symboles", + "studyTheChapterIsTooShortToBeAnalysed": "Le chapitre est trop court pour être analysé.", + "studyOnlyContributorsCanRequestAnalysis": "Seuls les contributeurs de l'étude peuvent demander une analyse informatique.", + "studyGetAFullComputerAnalysis": "Obtenez une analyse en ligne complète de la ligne principale.", + "studyMakeSureTheChapterIsComplete": "Assurez-vous que le chapitre est terminé. Vous ne pouvez demander l'analyse qu'une seule fois.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tous les membres SYNC demeurent sur la même position", + "studyShareChanges": "Partager les changements avec les spectateurs et les enregistrer sur le serveur", + "studyPlaying": "En cours", + "studyShowEvalBar": "Barre d’évaluation", + "studyFirst": "Premier", + "studyPrevious": "Précédent", + "studyNext": "Suivant", + "studyLast": "Dernier", "studyShareAndExport": "Partager & exporter", - "studyStart": "Commencer" + "studyCloneStudy": "Dupliquer", + "studyStudyPgn": "PGN de l'étude", + "studyDownloadAllGames": "Télécharger toutes les parties", + "studyChapterPgn": "PGN du chapitre", + "studyCopyChapterPgn": "Copier le fichier PGN", + "studyDownloadGame": "Télécharger la partie", + "studyStudyUrl": "URL de l'étude", + "studyCurrentChapterUrl": "URL du chapitre actuel", + "studyYouCanPasteThisInTheForumToEmbed": "Vous pouvez collez ce lien dans le forum afin de l’insérer", + "studyStartAtInitialPosition": "Commencer à partir du début", + "studyStartAtX": "Débuter à {param}", + "studyEmbedInYourWebsite": "Intégrer dans votre site ou blog", + "studyReadMoreAboutEmbedding": "En savoir plus sur l'intégration", + "studyOnlyPublicStudiesCanBeEmbedded": "Seules les études publiques peuvent être intégrées !", + "studyOpen": "Ouvrir", + "studyXBroughtToYouByY": "{param1} vous est apporté par {param2}", + "studyStudyNotFound": "Étude introuvable", + "studyEditChapter": "Modifier le chapitre", + "studyNewChapter": "Nouveau chapitre", + "studyImportFromChapterX": "Importer depuis {param}", + "studyOrientation": "Orientation", + "studyAnalysisMode": "Mode analyse", + "studyPinnedChapterComment": "Commentaire du chapitre épinglé", + "studySaveChapter": "Enregistrer le chapitre", + "studyClearAnnotations": "Effacer les annotations", + "studyClearVariations": "Supprimer les variantes", + "studyDeleteChapter": "Supprimer le chapitre", + "studyDeleteThisChapter": "Supprimer ce chapitre ? Cette action est irréversible !", + "studyClearAllCommentsInThisChapter": "Effacer tous les commentaires et annotations dans ce chapitre ?", + "studyRightUnderTheBoard": "Juste sous l'échiquier", + "studyNoPinnedComment": "Aucun", + "studyNormalAnalysis": "Analyse normale", + "studyHideNextMoves": "Cacher les coups suivants", + "studyInteractiveLesson": "Leçon interactive", + "studyChapterX": "Chapitre : {param}", + "studyEmpty": "Par défaut", + "studyStartFromInitialPosition": "Commencer à partir du début", + "studyEditor": "Editeur", + "studyStartFromCustomPosition": "Commencer à partir d'une position personnalisée", + "studyLoadAGameByUrl": "Charger des parties à partir d'une URL", + "studyLoadAPositionFromFen": "Charger une position par FEN", + "studyLoadAGameFromPgn": "Charger des parties par PGN", + "studyAutomatic": "Automatique", + "studyUrlOfTheGame": "URL des parties, une par ligne", + "studyLoadAGameFromXOrY": "Charger des parties de {param1} ou {param2}", + "studyCreateChapter": "Créer un chapitre", + "studyCreateStudy": "Créer une étude", + "studyEditStudy": "Modifier l'étude", + "studyVisibility": "Visibilité", + "studyPublic": "Publique", + "studyUnlisted": "Non répertorié", + "studyInviteOnly": "Sur invitation seulement", + "studyAllowCloning": "Autoriser la duplication", + "studyNobody": "Personne", + "studyOnlyMe": "Seulement moi", + "studyContributors": "Contributeurs", + "studyMembers": "Membres", + "studyEveryone": "Tout le monde", + "studyEnableSync": "Activer la synchronisation", + "studyYesKeepEveryoneOnTheSamePosition": "Oui : garder tout le monde sur la même position", + "studyNoLetPeopleBrowseFreely": "Non : laisser les gens naviguer librement", + "studyPinnedStudyComment": "Commentaire d'étude épinglé", + "studyStart": "Commencer", + "studySave": "Enregistrer", + "studyClearChat": "Effacer le tchat", + "studyDeleteTheStudyChatHistory": "Supprimer l'historique du tchat de l'étude ? Cette action est irréversible !", + "studyDeleteStudy": "Supprimer l'étude", + "studyConfirmDeleteStudy": "Supprimer toute l’étude? Aucun retour en arrière possible! Taper le nom de l’étude pour confirmer : {param}", + "studyWhereDoYouWantToStudyThat": "Où voulez-vous étudier cela ?", + "studyGoodMove": "Bon coup", + "studyMistake": "Erreur", + "studyBrilliantMove": "Excellent coup", + "studyBlunder": "Gaffe", + "studyInterestingMove": "Coup intéressant", + "studyDubiousMove": "Coup douteux", + "studyOnlyMove": "Seul coup", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Position égale", + "studyUnclearPosition": "Position incertaine", + "studyWhiteIsSlightlyBetter": "Les Blancs sont un peu mieux", + "studyBlackIsSlightlyBetter": "Les Noirs sont un peu mieux", + "studyWhiteIsBetter": "Les Blancs sont mieux", + "studyBlackIsBetter": "Les Noirs sont mieux", + "studyWhiteIsWinning": "Les Blancs gagnent", + "studyBlackIsWinning": "Les Noirs gagnent", + "studyNovelty": "Nouveauté", + "studyDevelopment": "Développement", + "studyInitiative": "Initiative", + "studyAttack": "Attaque", + "studyCounterplay": "Contre-jeu", + "studyTimeTrouble": "Pression de temps", + "studyWithCompensation": "Avec compensation", + "studyWithTheIdea": "Avec l'idée", + "studyNextChapter": "Chapitre suivant", + "studyPrevChapter": "Chapitre précédent", + "studyStudyActions": "Options pour les études", + "studyTopics": "Thèmes", + "studyMyTopics": "Mes thèmes", + "studyPopularTopics": "Thèmes populaires", + "studyManageTopics": "Gérer les thèmes", + "studyBack": "Retour", + "studyPlayAgain": "Jouer à nouveau", + "studyWhatWouldYouPlay": "Que joueriez-vous dans cette position ?", + "studyYouCompletedThisLesson": "Félicitations ! Vous avez terminé ce cours.", + "studyPerPage": "{param} par page", + "studyNbChapters": "{count, plural, =1{{count} chapitre} other{{count} chapitres}}", + "studyNbGames": "{count, plural, =1{{count} partie} other{{count} parties}}", + "studyNbMembers": "{count, plural, =1{{count} membre} other{{count} membres}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Collez votre texte PGN ici, jusqu'à {count} partie} other{Collez votre texte PGN ici, jusqu'à {count} parties}}", + "timeagoJustNow": "Maintenant", + "timeagoRightNow": "à l'instant", + "timeagoCompleted": "terminé", + "timeagoInNbSeconds": "{count, plural, =1{dans {count} seconde} other{dans {count} secondes}}", + "timeagoInNbMinutes": "{count, plural, =1{dans {count} minute} other{dans {count} minutes}}", + "timeagoInNbHours": "{count, plural, =1{dans {count} heure} other{dans {count} heures}}", + "timeagoInNbDays": "{count, plural, =1{dans {count} jour} other{dans {count} jours}}", + "timeagoInNbWeeks": "{count, plural, =1{dans {count} semaine} other{dans {count} semaines}}", + "timeagoInNbMonths": "{count, plural, =1{dans {count} mois} other{dans {count} mois}}", + "timeagoInNbYears": "{count, plural, =1{dans {count} an} other{dans {count} ans}}", + "timeagoNbMinutesAgo": "{count, plural, =1{il y a {count} minute} other{il y a {count} minutes}}", + "timeagoNbHoursAgo": "{count, plural, =1{il y a {count} heure} other{il y a {count} heures}}", + "timeagoNbDaysAgo": "{count, plural, =1{il y a {count} jour} other{il y a {count} jours}}", + "timeagoNbWeeksAgo": "{count, plural, =1{il y a {count} semaine} other{il y a {count} semaines}}", + "timeagoNbMonthsAgo": "{count, plural, =1{il y a {count} mois} other{il y a {count} mois}}", + "timeagoNbYearsAgo": "{count, plural, =1{il y a {count} an} other{il y a {count} ans}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute restante} other{{count} minutes restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} heure restante} other{{count} heures restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ga.arb b/lib/l10n/lila_ga.arb index 4ec0db6c6b..3c6678e295 100644 --- a/lib/l10n/lila_ga.arb +++ b/lib/l10n/lila_ga.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =1{Isteach i bhfoireann amháin} =2{Isteach i {count} fhoireann} few{Isteach i {count} bhfoireann} many{Isteach i {count} foireann} other{Isteach i {count} foireann}}", "broadcastBroadcasts": "Craoltaí", "broadcastLiveBroadcasts": "Craoltaí beo comórtais", + "broadcastNewBroadcast": "Craoladh beo nua", + "broadcastAddRound": "Cuir babhta leis", + "broadcastOngoing": "Leanúnach", + "broadcastUpcoming": "Le teacht", + "broadcastCompleted": "Críochnaithe", + "broadcastRoundName": "Ainm babhta", + "broadcastRoundNumber": "Uimhir bhabhta", + "broadcastTournamentName": "Ainm comórtas", + "broadcastTournamentDescription": "Cur síos gairid ar an gcomórtas", + "broadcastFullDescription": "Cur síos iomlán ar an ócáid", + "broadcastFullDescriptionHelp": "Cur síos fada roghnach ar an craoladh. Tá {param1} ar fáil. Caithfidh an fad a bheith níos lú ná {param2} carachtar.", + "broadcastSourceUrlHelp": "URL a seiceálfaidh Lichess chun PGN nuashonruithe a fháil. Caithfidh sé a bheith le féiceáil go poiblí ón Idirlíon.", + "broadcastStartDateHelp": "Roghnach, má tá a fhios agat cathain a thosóidh an ócáid", + "broadcastCurrentGameUrl": "URL cluiche reatha", + "broadcastDownloadAllRounds": "Íoslódáil gach babhta", + "broadcastResetRound": "Athshocraigh an babhta seo", + "broadcastDeleteRound": "Scrios an babhta seo", + "broadcastDefinitivelyDeleteRound": "Scrios go cinntitheach an babhta agus a chuid cluichí.", + "broadcastDeleteAllGamesOfThisRound": "Scrios gach cluiche den bhabhta seo. Caithfidh an fhoinse a bheith gníomhach chun iad a athchruthú.", "challengeChallengeToPlay": "Dúshlán cluiche", "challengeChallengeDeclined": "Dúshlán diúltaithe", "challengeChallengeAccepted": "Dúshlán glactha!", @@ -328,8 +347,8 @@ "puzzleThemeXRayAttackDescription": "Déanann píosa ionsaí nó cosaint ar chearnóg, trí phíosa namhaid.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Ciallaíonn Zugzwang gur gá le himreoir a s(h) eans a \nthógáil cé nár mhaith leis nó léi toisc gur laige a bheith a s(h) uíomh cibé beart a dhéanfaidh sé/sí. Ba mhaith leis / léi \"háram\" a rá ach níl sé sin ceadaithe.", - "puzzleThemeHealthyMix": "Meascán sláintiúil", - "puzzleThemeHealthyMixDescription": "Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.", + "puzzleThemeMix": "Meascán sláintiúil", + "puzzleThemeMixDescription": "Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.", "puzzleThemePlayerGames": "Cluichí imreoir", "puzzleThemePlayerGamesDescription": "Cuardaigh fadhbanna a ghintear ó do chluichí, nó ó chluichí imreoir eile.", "puzzleThemePuzzleDownloadInformation": "Tá na fadhbanna seo i mbéal an phobail, agus is féidir iad a íoslódáil ó {param}.", @@ -445,7 +464,6 @@ "replayMode": "Modh athimeartha", "realtimeReplay": "Fíor-am", "byCPL": "De réir CPL", - "openStudy": "Oscail staidéar", "enable": "Cumasaigh", "bestMoveArrow": "Saighead don bheart is fearr", "evaluationGauge": "Tomhsaire measúnachta", @@ -647,7 +665,6 @@ "block": "Blocáil", "blocked": "Blocáilte", "unblock": "Bain bac de", - "followsYou": "Do leanúint", "xStartedFollowingY": "Thosaigh {param1} ag leanúint {param2}", "more": "Tuilleadh", "memberSince": "Ball ó", @@ -744,7 +761,6 @@ "cheat": "Caimiléir", "troll": "Troll", "other": "Eile", - "reportDescriptionHelp": "Greamaigh an nasc chuig an gcluiche/na cluichí agus mínigh cad atá cearr le hiompar an úsáideora. Ná habair go díreach go mbíonn \"caimiléireacht\" ar bun acu, ach inis dúinn faoin dóigh a fuair tú amach faoi. Faraor, déanfar do thuairisc a phróiseáil níos tapúla más i mBéarla atá sé.", "error_provideOneCheatedGameLink": "Cuir nasc ar fáil chuig cluiche amháin ar a laghad ar tharla caimiléireacht ann le do thoil.", "by": "ó{param}", "importedByX": "Iompórtáilte ag {param}", @@ -1203,6 +1219,173 @@ "stormXRuns": "{count, plural, =1{1 stríocáin} =2{{count} stríocáin} few{{count} stríocáin} many{{count} stríocáin} other{{count} stríocáin}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{D'imir stríocáin amháin de {param2}} =2{D'imir {count} stríocáin de {param2}} few{D'imir {count} stríocáin de {param2}} many{D'imir {count} stríocáin de {param2}} other{D'imir {count} stríocáin de {param2}}}", "streamerLichessStreamers": "Sruthaithe Lichess", + "studyPrivate": "Cé na daoine! Tá an leathanach seo príobháideach, ní féidir leat é a rochtain", + "studyMyStudies": "Mo chuid staidéir", + "studyStudiesIContributeTo": "Staidéir atá á n-iarraidh agam", + "studyMyPublicStudies": "Mo chuid staidéir phoiblí", + "studyMyPrivateStudies": "Mo chuid staidéir phríobháideacha", + "studyMyFavoriteStudies": "Na staidéir is fearr liom", + "studyWhatAreStudies": "Cad is staidéir ann?", + "studyAllStudies": "Gach staidéar", + "studyStudiesCreatedByX": "Staidéir a chruthaigh {param}", + "studyNoneYet": "Níl aon cheann fós.", + "studyHot": "Te", + "studyDateAddedNewest": "Dáta curtha leis (dáta is déanaí)", + "studyDateAddedOldest": "Dáta curtha leis (dáta is sinne)", + "studyRecentlyUpdated": "Faisnéis nuashonraithe le déanaí", + "studyMostPopular": "Móréilimh", + "studyAlphabetical": "Aibítre", + "studyAddNewChapter": "Cuir caibidil nua leis", + "studyAddMembers": "Cuir baill leis", + "studyInviteToTheStudy": "Tabhair cuireadh don staidéar", + "studyPleaseOnlyInvitePeopleYouKnow": "Ná tabhair cuireadh ach do dhaoine a bhfuil aithne agat orthu, agus ar mian leo go gníomhach a bheith páirteach sa staidéar seo.", + "studySearchByUsername": "Cuardaigh de réir ainm úsáideora", + "studySpectator": "Breathnóir", + "studyContributor": "Rannpháirtí", + "studyKick": "Ciceáil", + "studyLeaveTheStudy": "Fág an staidéar", + "studyYouAreNowAContributor": "Is ranníocóir anois tú", + "studyYouAreNowASpectator": "Is lucht féachana anois tú", + "studyPgnTags": "Clibeanna PGN", + "studyLike": "Is maith liom", + "studyUnlike": "Díthogh", + "studyNewTag": "Clib nua", + "studyCommentThisPosition": "Déan trácht ar an suíomh seo", + "studyCommentThisMove": "Déan trácht ar an mbeart seo", + "studyAnnotateWithGlyphs": "Nodaireacht le glifeanna", + "studyTheChapterIsTooShortToBeAnalysed": "Tá an chaibidil ró-ghearr le hanailís a dhéanamh uirthi.", + "studyOnlyContributorsCanRequestAnalysis": "Ní féidir ach le rannpháirtithe an staidéir anailís ríomhaire a iarraidh.", + "studyGetAFullComputerAnalysis": "Faigh anailís ríomhaire iomlán ón freastalaí ar an bpríomhlíne.", + "studyMakeSureTheChapterIsComplete": "Bí cinnte go bhfuil an chaibidil críochnaithe. Ní féidir leat iarr ar anailís ach uair amháin.", + "studyAllSyncMembersRemainOnTheSamePosition": "Fanann gach ball SYNC sa suíomh céanna", + "studyShareChanges": "Roinn athruithe le lucht féachana agus sábháil iad ar an freastalaí", + "studyPlaying": "Ag imirt", + "studyFirst": "Céad", + "studyPrevious": "Roimhe", + "studyNext": "Ar aghaidh", + "studyLast": "Deiridh", "studyShareAndExport": "Comhroinn & easpórtáil", - "studyStart": "Tosú" + "studyCloneStudy": "Déan cóip", + "studyStudyPgn": "Déan staidéar ar PGN", + "studyDownloadAllGames": "Íoslódáil gach cluiche", + "studyChapterPgn": "PGN caibidle", + "studyCopyChapterPgn": "Cóipeáil PGN", + "studyDownloadGame": "Íoslódáil cluiche", + "studyStudyUrl": "URL an staidéir", + "studyCurrentChapterUrl": "URL caibidil reatha", + "studyYouCanPasteThisInTheForumToEmbed": "Is féidir é seo a ghreamú san fhóram chun leabú", + "studyStartAtInitialPosition": "Tosaigh ag an suíomh tosaigh", + "studyStartAtX": "Tosú ag {param}", + "studyEmbedInYourWebsite": "Leabaithe i do shuíomh Gréasáin nó i do bhlag", + "studyReadMoreAboutEmbedding": "Léigh tuilleadh faoi leabú", + "studyOnlyPublicStudiesCanBeEmbedded": "Ní féidir ach staidéir phoiblí a leabú!", + "studyOpen": "Oscailte", + "studyXBroughtToYouByY": "{param1}, a thugann {param2} chugat", + "studyStudyNotFound": "Níor aimsíodh staidéar", + "studyEditChapter": "Cuir caibidil in eagar", + "studyNewChapter": "Caibidil nua", + "studyImportFromChapterX": "Iompórtáil ó {param}", + "studyOrientation": "Treoshuíomh", + "studyAnalysisMode": "Modh anailíse", + "studyPinnedChapterComment": "Trácht caibidil greamaithe", + "studySaveChapter": "Sábháil caibidil", + "studyClearAnnotations": "Glan anótála", + "studyClearVariations": "Glan éagsúlachtaí", + "studyDeleteChapter": "Scrios caibidil", + "studyDeleteThisChapter": "Scrios an chaibidil seo? Níl aon dul ar ais!", + "studyClearAllCommentsInThisChapter": "Glan gach trácht, glif agus cruthanna tarraingthe sa chaibidil seo?", + "studyRightUnderTheBoard": "Díreach faoin gclár", + "studyNoPinnedComment": "Faic", + "studyNormalAnalysis": "Gnáth-anailís", + "studyHideNextMoves": "Folaigh na bearta ina dhiaidh seo", + "studyInteractiveLesson": "Ceacht idirghníomhach", + "studyChapterX": "Caibidil {param}", + "studyEmpty": "Folamh", + "studyStartFromInitialPosition": "Tosaigh ón suíomh tosaigh", + "studyEditor": "Eagarthóir", + "studyStartFromCustomPosition": "Tosaigh ón suíomh saincheaptha", + "studyLoadAGameByUrl": "Lód cluichí le URLanna", + "studyLoadAPositionFromFen": "Luchtaigh suíomh ó FEN", + "studyLoadAGameFromPgn": "Lódáil cluichí ó PGN", + "studyAutomatic": "Uathoibríoch", + "studyUrlOfTheGame": "URL na gcluichí, ceann amháin an líne", + "studyLoadAGameFromXOrY": "Lódáil cluichí ó {param1} nó {param2}", + "studyCreateChapter": "Cruthaigh caibidil", + "studyCreateStudy": "Cruthaigh staidéar", + "studyEditStudy": "Cuir staidéar in eagar", + "studyVisibility": "Infheictheacht", + "studyPublic": "Poiblí", + "studyUnlisted": "Neamhliostaithe", + "studyInviteOnly": "Tabhair cuireadh amháin", + "studyAllowCloning": "Lig clónáil", + "studyNobody": "Níl einne", + "studyOnlyMe": "Mise amháin", + "studyContributors": "Rannpháirtithe", + "studyMembers": "Baill", + "studyEveryone": "Gach duine", + "studyEnableSync": "Cuir sinc ar chumas", + "studyYesKeepEveryoneOnTheSamePosition": "Cinnte: coinnigh gach duine ar an suíomh céanna", + "studyNoLetPeopleBrowseFreely": "Na déan: lig do dhaoine brabhsáil go saor", + "studyPinnedStudyComment": "Trácht staidéir greamaithe", + "studyStart": "Tosú", + "studySave": "Sábháil", + "studyClearChat": "Glan comhrá", + "studyDeleteTheStudyChatHistory": "Scrios an stair comhrá staidéir? Níl aon dul ar ais!", + "studyDeleteStudy": "Scrios an staidéar", + "studyConfirmDeleteStudy": "Scrios an staidéar iomlán? Níl aon dul ar ais! Clóscríobh ainm an staidéar le deimhniú: {param}", + "studyWhereDoYouWantToStudyThat": "Cá háit ar mhaith leat staidéar a dhéanamh air sin?", + "studyGoodMove": "Beart maith", + "studyMistake": "Botún", + "studyBrilliantMove": "Beart iontach", + "studyBlunder": "Botún", + "studyInterestingMove": "Beart suimiúil", + "studyDubiousMove": "Beart amhrasach", + "studyOnlyMove": "Beart dleathach", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Suíomh cothrom", + "studyUnclearPosition": "Suíomh doiléir", + "studyWhiteIsSlightlyBetter": "Tá bán píosa beag níos fearr", + "studyBlackIsSlightlyBetter": "Tá dubh píosa beag níos fearr", + "studyWhiteIsBetter": "Tá bán níos fearr", + "studyBlackIsBetter": "Tá dubh níos fearr", + "studyWhiteIsWinning": "Bán ag bua", + "studyBlackIsWinning": "Dubh ag bua", + "studyNovelty": "Nuaga", + "studyDevelopment": "Forbairt", + "studyInitiative": "Tionscnamh", + "studyAttack": "Ionsaí", + "studyCounterplay": "Frithimirt", + "studyTimeTrouble": "Trioblóid ama", + "studyWithCompensation": "Le cúiteamh", + "studyWithTheIdea": "Le smaoineamh", + "studyNextChapter": "Céad chaibidil eile", + "studyPrevChapter": "Caibidil roimhe seo", + "studyStudyActions": "Déan staidéar ar ghníomhartha", + "studyTopics": "Topaicí", + "studyMyTopics": "Mo thopaicí", + "studyPopularTopics": "Topaicí choitianta", + "studyManageTopics": "Bainistigh topaicí", + "studyBack": "Siar", + "studyPlayAgain": "Imir arís", + "studyWhatWouldYouPlay": "Cad a dhéanfá sa suíomh seo?", + "studyYouCompletedThisLesson": "Comhghairdeas! Chríochnaigh tú an ceacht seo.", + "studyNbChapters": "{count, plural, =1{{count} Caibidil} =2{{count} Chaibidil} few{{count} gCaibidil} many{{count} Caibidil} other{{count} Caibidil}}", + "studyNbGames": "{count, plural, =1{{count} Cluiche} =2{{count} Chluiche} few{{count} gCluiche} many{{count} Cluiche} other{{count} Cluiche}}", + "studyNbMembers": "{count, plural, =1{{count} Comhalta} =2{{count} Chomhalta} few{{count} gComhalta} many{{count} Comhalta} other{{count} Comhalta}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Greamaigh do théacs PGN anseo, suas le {count} cluiche} =2{Greamaigh do théacs PGN anseo, suas le {count} chluiche} few{Greamaigh do théacs PGN anseo, suas le {count} gcluiche} many{Greamaigh do théacs PGN anseo, suas le {count} cluiche} other{Greamaigh do théacs PGN anseo, suas le {count} cluiche}}", + "timeagoJustNow": "díreach anois", + "timeagoRightNow": "anois", + "timeagoInNbSeconds": "{count, plural, =1{i soicind amháín} =2{i {count} soicind} few{i {count} soicind} many{i {count} soicind} other{i {count} soicind}}", + "timeagoInNbMinutes": "{count, plural, =1{i nóiméad amháin} =2{i {count} nóiméad} few{i {count} nóiméad} many{i {count} nóiméad} other{i {count} nóiméad}}", + "timeagoInNbHours": "{count, plural, =1{in uair amháin} =2{i {count} uair} few{i {count} uair} many{i {count} uair} other{i {count} uair}}", + "timeagoInNbDays": "{count, plural, =1{i lá amháín} =2{i {count} lá} few{i {count} lá} many{i {count} lá} other{i {count} lá}}", + "timeagoInNbWeeks": "{count, plural, =1{i seachtain amháin} =2{i {count} sheachtain} few{i {count} seachtain} many{i {count} seachtain} other{i {count} seachtain}}", + "timeagoInNbMonths": "{count, plural, =1{i gceann míosa} =2{i {count} mhí} few{i {count} mhí} many{i {count} mí} other{i {count} mí}}", + "timeagoInNbYears": "{count, plural, =1{i gceann bliana} =2{i gceann {count} bhliain} few{i gceann {count} bhliain} many{i gceann {count} mbliain} other{i gceann {count} bliain}}", + "timeagoNbMinutesAgo": "{count, plural, =1{nóiméad amháin ó shin} =2{{count} nóiméad ó shin} few{{count} nóiméad ó shin} many{{count} nóiméad ó shin} other{{count} nóiméad ó shin}}", + "timeagoNbHoursAgo": "{count, plural, =1{uair amháin ó shin} =2{{count} uair ó shin} few{{count} uair ó shin} many{{count} uair ó shin} other{{count} uair ó shin}}", + "timeagoNbDaysAgo": "{count, plural, =1{lá amháin ó shin} =2{{count} lá ó shin} few{{count} lá ó shin} many{{count} lá ó shin} other{{count} lá ó shin}}", + "timeagoNbWeeksAgo": "{count, plural, =1{seachtain amháin ó shin} =2{{count} sheachtain ó shin} few{{count} sheachtain ó shin} many{{count} seachtain ó shin} other{{count} seachtain ó shin}}", + "timeagoNbMonthsAgo": "{count, plural, =1{mí amháin ó shin} =2{{count} mhí ó shin} few{{count} mí ó shin} many{{count} mí ó shin} other{{count} mí ó shin}}", + "timeagoNbYearsAgo": "{count, plural, =1{Bliain amháin ó shin} =2{{count} bhliain ó shin} few{{count} mbliain ó shin} many{{count} mbliain ó shin} other{{count} bliain ó shin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gl.arb b/lib/l10n/lila_gl.arb index fd63ec9811..8ddee708e8 100644 --- a/lib/l10n/lila_gl.arb +++ b/lib/l10n/lila_gl.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todas as partidas", + "mobileAreYouSure": "Estás seguro?", + "mobileBlindfoldMode": "Ás cegas", + "mobileCancelTakebackOffer": "Cancelar a proposta de cambio", + "mobileClearButton": "Borrar", + "mobileCorrespondenceClearSavedMove": "Borrar a xogada gardada", + "mobileCustomGameJoinAGame": "Unirse a unha partida", + "mobileFeedbackButton": "Comentarios", + "mobileGreeting": "Ola, {param}", + "mobileGreetingWithoutName": "Ola", + "mobileHideVariation": "Ocultar variantes", "mobileHomeTab": "Inicio", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Ferramentas", - "mobileWatchTab": "Ver", - "mobileSettingsTab": "Axustes", + "mobileLiveStreamers": "Presentadores en directo", "mobileMustBeLoggedIn": "Debes iniciar sesión para ver esta páxina.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Comentarios", + "mobileNoSearchResults": "Sen resultados", + "mobileNotFollowingAnyUser": "Non estás a seguir a ningún usuario.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "O nome de usuario contén \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ampliar a peza arrastrada", + "mobilePuzzleStormConfirmEndRun": "Queres rematar esta quenda?", + "mobilePuzzleStormFilterNothingToShow": "Non aparece nada. Por favor, cambia os filtros", + "mobilePuzzleStormNothingToShow": "Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resolve tantos crebacabezas como sexa posible en 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderás a túa secuencia actual e o teu resultado gardarase.", + "mobilePuzzleThemesSubtitle": "Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Procuras recentes", "mobileSettingsHapticFeedback": "Vibración ó mover", "mobileSettingsImmersiveMode": "Pantalla completa", "mobileSettingsImmersiveModeSubtitle": "Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.", - "mobileNotFollowingAnyUser": "Non estás a seguir a ningún usuario.", - "mobileAllGames": "Todas as partidas", - "mobileRecentSearches": "Procuras recentes", - "mobileClearButton": "Borrar", - "mobilePlayersMatchingSearchTerm": "Xogadores con \"{param}\"", - "mobileNoSearchResults": "Sen resultados", - "mobileAreYouSure": "Estás seguro?", - "mobilePuzzleStreakAbortWarning": "Perderás a túa secuencia actual e o teu resultado gardarase.", - "mobilePuzzleStormNothingToShow": "Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.", - "mobileSharePuzzle": "Compartir este crebacabezas", - "mobileShareGameURL": "Compartir a URL da partida", + "mobileSettingsTab": "Axustes", "mobileShareGamePGN": "Compartir PGN", + "mobileShareGameURL": "Compartir a URL da partida", "mobileSharePositionAsFEN": "Compartir a posición coma FEN", - "mobileShowVariations": "Amosar variantes", - "mobileHideVariation": "Ocultar variantes", + "mobileSharePuzzle": "Compartir este crebacabezas", "mobileShowComments": "Amosar comentarios", - "mobilePuzzleStormConfirmEndRun": "Queres rematar esta quenda?", - "mobilePuzzleStormFilterNothingToShow": "Non aparece nada. Por favor, cambia os filtros", - "mobileCancelTakebackOffer": "Cancelar a proposta de cambio", - "mobileCancelDrawOffer": "Cancelar a oferta de táboas", - "mobileWaitingForOpponentToJoin": "Agardando un rival...", - "mobileBlindfoldMode": "Á cega", - "mobileLiveStreamers": "Presentadores en directo", - "mobileCustomGameJoinAGame": "Unirse a unha partida", - "mobileCorrespondenceClearSavedMove": "Borrar a xogada gardada", - "mobileSomethingWentWrong": "Algo foi mal.", "mobileShowResult": "Amosar o resultado", - "mobilePuzzleThemesSubtitle": "Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.", - "mobilePuzzleStormSubtitle": "Resolve tantos crebacabezas como sexa posible en 3 minutos.", - "mobileGreeting": "Ola, {param}", - "mobileGreetingWithoutName": "Ola", + "mobileShowVariations": "Amosar variantes", + "mobileSomethingWentWrong": "Algo foi mal.", + "mobileSystemColors": "Cores do sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Ferrament.", + "mobileWaitingForOpponentToJoin": "Agardando un rival...", + "mobileWatchTab": "Ver", "activityActivity": "Actividade", "activityHostedALiveStream": "Emitiu en directo", "activityRankedInSwissTournament": "{param1}º na clasificación de {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Xogou {count} movemento} other{Xogou {count} movementos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{en {count} partida por correspondencia} other{en {count} partidas por correspondencia}}", "activityCompletedNbGames": "{count, plural, =1{Xogou {count} partida por correspondencia} other{Xogou {count} partidas por correspondencia}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Finalizou {count} partida {param2} por correspondencia} other{Finalizou {count} partidas {param2} por correspondencia}}", "activityFollowedNbPlayers": "{count, plural, =1{Comezou a seguir a {count} xogador} other{Comezou a seguir a {count} xogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Gañou {count} novo seguidor} other{Gañou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Ofreceu {count} exhibición simultánea} other{Ofreceu {count} exhibicións simultáneas}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu en {count} torneo suízo} other{Competiu en {count} torneos suízos}}", "activityJoinedNbTeams": "{count, plural, =1{Uniuse a {count} equipo} other{Uniuse a {count} equipos}}", "broadcastBroadcasts": "Emisións en directo", + "broadcastMyBroadcasts": "As miñas emisións", "broadcastLiveBroadcasts": "Emisións de torneos en directo", + "broadcastBroadcastCalendar": "Calendario de emisións", + "broadcastNewBroadcast": "Nova emisión en directo", + "broadcastSubscribedBroadcasts": "Emisións subscritas", + "broadcastAboutBroadcasts": "Sobre as retransmisións", + "broadcastHowToUseLichessBroadcasts": "Como usar as Retransmisións de Lichess.", + "broadcastTheNewRoundHelp": "A nova rolda terá os mesmos membros e colaboradores cá rolda anterior.", + "broadcastAddRound": "Engadir unha rolda", + "broadcastOngoing": "En curso", + "broadcastUpcoming": "Proximamente", + "broadcastCompleted": "Completadas", + "broadcastCompletedHelp": "Malia que Lichess detecta o final das roldas, pódese equivocar. Usa esta opción para facelo manualmente.", + "broadcastRoundName": "Nome da rolda", + "broadcastRoundNumber": "Número de rolda", + "broadcastTournamentName": "Nome do torneo", + "broadcastTournamentDescription": "Breve descrición do torneo", + "broadcastFullDescription": "Descrición completa do torneo", + "broadcastFullDescriptionHelp": "Descrición longa opcional do torneo. {param1} está dispoñíbel. A lonxitude debe ser menor de {param2} caracteres.", + "broadcastSourceSingleUrl": "URL de orixe do arquivo PGN", + "broadcastSourceUrlHelp": "Ligazón que Lichess comprobará para obter actualizacións dos PGN. Debe ser publicamente accesíbel desde a Internet.", + "broadcastSourceGameIds": "Até 64 identificadores de partidas de Lichess, separados por espazos.", + "broadcastStartDateTimeZone": "Data de inicio do torneo (na zona horaria local): {param}", + "broadcastStartDateHelp": "Opcional, se sabes cando comeza o evento", + "broadcastCurrentGameUrl": "Ligazón da partida actual", + "broadcastDownloadAllRounds": "Descargar todas as roldas", + "broadcastResetRound": "Restablecer esta rolda", + "broadcastDeleteRound": "Borrar esta rolda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente a rolda e todas as súas partidas.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todas as partidas desta rolda. A transmisión en orixe terá que estar activa para volver crealas.", + "broadcastEditRoundStudy": "Editar o estudo da rolda", + "broadcastDeleteTournament": "Eliminar este torneo", + "broadcastDefinitivelyDeleteTournament": "Eliminar o torneo de forma definitiva, con todas as súas roldas e partidas.", + "broadcastShowScores": "Amosar os puntos dos xogadores en función dos resultados das partidas", + "broadcastReplacePlayerTags": "Opcional: substituír os nomes dos xogadores, as puntuacións e os títulos", + "broadcastFideFederations": "Federacións FIDE", + "broadcastTop10Rating": "Media do top 10", + "broadcastFidePlayers": "Xogadores FIDE", + "broadcastFidePlayerNotFound": "Xogador FIDE non atopado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federación", + "broadcastAgeThisYear": "Idade actual", + "broadcastUnrated": "Sen puntuar", + "broadcastRecentTournaments": "Torneos recentes", + "broadcastOpenLichess": "Abrir en Lichess", + "broadcastTeams": "Equipos", + "broadcastBoards": "Taboleiros", + "broadcastOverview": "Visión de conxunto", + "broadcastSubscribeTitle": "Subscríbete para ser notificado ó comezo de cada rolda. Podes activar/desactivar o son das notificacións ou as notificacións emerxentes para as emisións en directo nas preferencias da túa conta.", + "broadcastUploadImage": "Subir a imaxe do torneo", + "broadcastNoBoardsYet": "Aínda non hai taboleiros. Aparecerán cando se suban as partidas.", + "broadcastBoardsCanBeLoaded": "Os taboleiros pódense cargar dende a fonte ou a través da {param}", + "broadcastStartsAfter": "Comeza tras a {param}", + "broadcastStartVerySoon": "A emisión comeza decontado.", + "broadcastNotYetStarted": "A emisión aínda non comezou.", + "broadcastOfficialWebsite": "Páxina web oficial", + "broadcastStandings": "Clasificación", + "broadcastOfficialStandings": "Clasificación oficial", + "broadcastIframeHelp": "Máis opcións na {param}", + "broadcastWebmastersPage": "páxina do administrador web", + "broadcastPgnSourceHelp": "Unha fonte dos PGN pública e en tempo real para esta rolda. Tamén ofrecemos unha {param} para unha sincronización máis rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incrustar esta emisión na túa páxina web", + "broadcastEmbedThisRound": "Incrustar a {param} na túa páxina web", + "broadcastRatingDiff": "Diferenza de puntuación", + "broadcastGamesThisTournament": "Partidas neste torneo", + "broadcastScore": "Resultado", + "broadcastAllTeams": "Todos os equipos", + "broadcastTournamentFormat": "Formato do torneo", + "broadcastTournamentLocation": "Lugar do torneo", + "broadcastTopPlayers": "Mellores xogadores", + "broadcastTimezone": "Zona horaria", + "broadcastFideRatingCategory": "Categoría de puntuación FIDE", + "broadcastOptionalDetails": "Detalles opcionais", + "broadcastPastBroadcasts": "Emisións finalizadas", + "broadcastAllBroadcastsByMonth": "Ver todas as emisións por mes", + "broadcastNbBroadcasts": "{count, plural, =1{{count} emisión} other{{count} emisións}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafía a unha partida", "challengeChallengeDeclined": "Desafío rexeitado", @@ -137,7 +214,7 @@ "preferencesZenMode": "Modo zen", "preferencesShowPlayerRatings": "Amosar a puntuación dos xogadores", "preferencesShowFlairs": "Amosar as habelencias dos xogadores", - "preferencesExplainShowPlayerRatings": "Isto permite ocultar todas as puntuacións do sitio, para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.", + "preferencesExplainShowPlayerRatings": "Oculta todas as puntuacións de Lichess para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.", "preferencesDisplayBoardResizeHandle": "Mostrar o control de redimensionamento do taboleiro", "preferencesOnlyOnInitialPosition": "Só na posición inicial", "preferencesInGameOnly": "Só durante a partida", @@ -147,7 +224,7 @@ "preferencesHorizontalGreenProgressBars": "Barras horizontais de progreso verdes", "preferencesSoundWhenTimeGetsCritical": "Aviso cando se esgota o tempo", "preferencesGiveMoreTime": "Dar máis tempo", - "preferencesGameBehavior": "Comportamento do xogo", + "preferencesGameBehavior": "Comportamento durante a partida", "preferencesHowDoYouMovePieces": "Como moves as pezas?", "preferencesClickTwoSquares": "Premendo na casa de orixe e despois na de destino", "preferencesDragPiece": "Arrastrando a peza", @@ -167,7 +244,7 @@ "preferencesConfirmResignationAndDrawOffers": "Confirmar abandono e ofertas de táboas", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "Método de enroque", "preferencesCastleByMovingTwoSquares": "Movendo o rei dúas casas", - "preferencesCastleByMovingOntoTheRook": "Movendo rei ata a torre", + "preferencesCastleByMovingOntoTheRook": "Movendo o rei cara a torre", "preferencesInputMovesWithTheKeyboard": "Introdución de movementos co teclado", "preferencesInputMovesWithVoice": "Introdución de xogadas coa voz", "preferencesSnapArrowsToValidMoves": "Adherir frechas a movementos válidos", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Son da notificación", + "preferencesBlindfold": "Ás cegas", "puzzlePuzzles": "Crebacabezas", "puzzlePuzzleThemes": "Temas de quebracabezas", "puzzleRecommended": "Recomendado", @@ -242,7 +320,7 @@ "puzzleStrengths": "Puntos fortes", "puzzleHistory": "Historial de crebacabezas", "puzzleSolved": "resoltos", - "puzzleFailed": "fallado", + "puzzleFailed": "incorrecto", "puzzleStreakDescription": "Soluciona exercicios cada vez máis difíciles e consigue unha secuencia de vitorias. Non hai conta atrás, así que podes ir amodo. Se te equivocas nun só movemento, acabouse! Pero lembra que podes omitir unha xogada por sesión.", "puzzleYourStreakX": "A túa secuencia de vitorias: {param}", "puzzleStreakSkipExplanation": "Omite este movemento para conservar a túa secuencia. Só se pode facer unha vez por sesión.", @@ -303,7 +381,7 @@ "puzzleThemeDeflection": "Desviación", "puzzleThemeDeflectionDescription": "Un movemento que distrae unha peza rival dunha tarefa que desempeña, como a protección dunha casa chave. Ás veces denomínase \"sobrecarga\".", "puzzleThemeDiscoveredAttack": "Ataque descuberto", - "puzzleThemeDiscoveredAttackDescription": "Apartar unha peza que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo un cabalo fóra do camiño dunha torre).", + "puzzleThemeDiscoveredAttackDescription": "Apartar unha peza (por exemplo un cabalo) que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo unha torre).", "puzzleThemeDoubleCheck": "Xaque dobre", "puzzleThemeDoubleCheckDescription": "Xaque con dúas pezas á vez, como resultado dun ataque descuberto onde tanto a peza en movemento como a desvelada atacan ao rei do opoñente.", "puzzleThemeEndgame": "Final", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Unha peza ataca ou defende un escaque a través dunha peza do opoñente.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O rival ten os movementos limitados e calquera xogada que faga empeora a súa posición.", - "puzzleThemeHealthyMix": "Mestura equilibrada", - "puzzleThemeHealthyMixDescription": "Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.", + "puzzleThemeMix": "Mestura equilibrada", + "puzzleThemeMixDescription": "Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.", "puzzleThemePlayerGames": "Partidas de xogadores", "puzzleThemePlayerGamesDescription": "Busca crebacabezas xerados a partir das túas partidas ou das doutros xogadores.", "puzzleThemePuzzleDownloadInformation": "Estes problemas son de dominio público e poden ser descargados en {param}.", @@ -505,7 +583,6 @@ "replayMode": "Modo de repetición", "realtimeReplay": "Tempo real", "byCPL": "Por PCP", - "openStudy": "Abrir estudo", "enable": "Activar", "bestMoveArrow": "Frecha coa mellor xogada", "showVariationArrows": "Amosar as frechas das variantes", @@ -515,7 +592,6 @@ "memory": "Memoria", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Elimina o límite de profundidade, e mantén o teu ordenador quente", - "engineManager": "Administrador do motor de análise", "blunder": "Metida de zoca", "mistake": "Erro", "inaccuracy": "Imprecisión", @@ -567,7 +643,7 @@ "changePassword": "Cambiar contrasinal", "changeEmail": "Cambiar correo", "email": "Correo electrónico", - "passwordReset": "Cambiar contrasinal", + "passwordReset": "restablecer o contrasinal", "forgotPassword": "Esqueciches o teu contrasinal?", "error_weakPassword": "Ese contrasinal é extremadamente común e demasiado doado de adiviñar.", "error_namePassword": "Por favor, non uses o teu usuario como contrasinal.", @@ -597,6 +673,7 @@ "rank": "Posición", "rankX": "Posición: {param}", "gamesPlayed": "Partidas xogadas", + "ok": "Ok", "cancel": "Cancelar", "whiteTimeOut": "Acabou o tempo das brancas", "blackTimeOut": "Acabou o tempo das negras", @@ -677,7 +754,7 @@ "startedStreaming": "comezou unha retransmisión", "xStartedStreaming": "{param} comezou unha retransmisión", "averageElo": "Puntuación media", - "location": "Ubicación", + "location": "Lugar", "filterGames": "Filtrar partidas", "reset": "Restablecer", "apply": "Aplicar", @@ -713,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Séguete", "xStartedFollowingY": "{param1} comezou a seguir a {param2}", "more": "Máis", "memberSince": "Membro dende", @@ -819,7 +895,9 @@ "cheat": "Trampa", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Pega a ligazón á(s) partida(s) e explica o que é incorrecto no comportamento deste usuario. Non digas só \"fai trampas\", cóntanos como chegaches a esa conclusión. A túa denuncia será procesada máis rapidamente se está escrita en inglés.", + "reportCheatBoostHelp": "Pega a ligazón á(s) partida(s) e explica que ten de malo o comportamento deste usuario. Non digas soamente \"fai trampas\", mais cóntanos como chegaches a esta conclusión.", + "reportUsernameHelp": "Explica que ten de ofensivo este nome de usuario. Non digas soamente \"é ofensivo/inadecuado\". Cóntanos como chegaches a esta conclusión, especialmente se o insulto está camuflado, non está en inglés, é xerga ou é unha referencia histórica ou cultural.", + "reportProcessedFasterInEnglish": "A túa denuncia será procesada máis rápido se está escrita en Inglés.", "error_provideOneCheatedGameLink": "Por favor, incorpora cando menos unha ligazón a unha partida na que se fixeron trampas.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -885,7 +963,7 @@ "error_email_unique": "Este enderezo de correo non é válido ou xa foi empregado", "error_email_different": "Este xa é o teu enderezo de correo electrónico", "error_minLength": "A lonxitude mínima é de {param} caracteres", - "error_maxLength": "A lonxitude máxima é de {param} caracteres", + "error_maxLength": "Debe conter polo menos {param} caracteres", "error_min": "Debe ser maior ou igual que {param}", "error_max": "Debe ser menor ou igual que {param}", "ifRatingIsPlusMinusX": "Se a puntuación é ± {param}", @@ -898,7 +976,7 @@ "blackCastlingKingside": "Negras O-O", "tpTimeSpentPlaying": "Tempo xogando: {param}", "watchGames": "Ver partidas", - "tpTimeSpentOnTV": "Tempo en TV: {param}", + "tpTimeSpentOnTV": "Tempo saíndo en TV: {param}", "watch": "Ver", "videoLibrary": "Videoteca", "streamersMenu": "Presentadores", @@ -924,14 +1002,14 @@ "aboutSimul": "As simultáneas son partidas dun xogador contra varios ó mesmo tempo.", "aboutSimulImage": "De 50 rivais, Fischer gañou 47 partidas, empatou 2 e perdeu 1.", "aboutSimulRealLife": "O concepto tómase de eventos reais. Nestes, o anfitrión das simultáneas móvese de mesa en mesa e fai unha xogada de cada vez.", - "aboutSimulRules": "Cando as simultáneas comezan, cada xogador inicia unha partida contra o anfitrión. As simultáneas finalizan cando todas as partidas rematan.", + "aboutSimulRules": "Cando comezan as simultáneas, cada xogador inicia unha partida co anfitrión. As simultáneas finalizan cando se completan todas as partidas.", "aboutSimulSettings": "As simultáneas son sempre amigables. As opcións de desquite, de desfacer a xogada e de engadir tempo non están activadas.", "create": "Crear", "whenCreateSimul": "Cando creas unha exhibición de simultáneas, tes que enfrontarte a varios xogadores ó mesmo tempo.", "simulVariantsHint": "Se seleccionas distintas variantes, cada xogador pode escoller cal xogar.", "simulClockHint": "Modo Fischer de reloxo. Cantos máis xogadores, máis tempo necesitas.", "simulAddExtraTime": "Podes engadir tempo extra ó teu reloxo para axudarte coas simultáneas.", - "simulHostExtraTime": "Tempo extra para o anfitrión", + "simulHostExtraTime": "Tempo inicial extra para o anfitrión", "simulAddExtraTimePerPlayer": "Engadir tempo inicial ao teu reloxo por cada xogador que se una ás simultáneas.", "simulHostExtraTimePerPlayer": "Tempo extra do anfitrión por cada xogador", "lichessTournaments": "Torneos de Lichess", @@ -955,7 +1033,7 @@ "toggleVariationArrows": "Activar/desactivar as frechas das variantes", "cyclePreviousOrNextVariation": "Variante anterior/seguinte", "toggleGlyphAnnotations": "Activar/desactivar as anotacións con símbolos", - "togglePositionAnnotations": "Alternar anotaciones de posición", + "togglePositionAnnotations": "Activar/desactivar as anotacións", "variationArrowsInfo": "As frechas das variantes permítenche navegar sen usar a lista de movementos.", "playSelectedMove": "facer a xogada seleccionada", "newTournament": "Novo torneo", @@ -986,7 +1064,7 @@ "downloadRaw": "Descargar sen anotar", "downloadImported": "Descargar importadas", "crosstable": "Táboa de cruces", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Tamén podes usar a roda do rato sobre o taboleiro para moverte pola partida.", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Usa a roda do rato sobre o taboleiro para moverte pola partida.", "scrollOverComputerVariationsToPreviewThem": "Pasa o punteiro sobre as variantes da computadora para visualizalas.", "analysisShapesHowTo": "Pulsa Maiúsculas+clic ou preme o botón dereito do rato para debuxar círculos e frechas no taboleiro.", "letOtherPlayersMessageYou": "Permitir que outros xogadores che envíen mensaxes", @@ -1166,7 +1244,7 @@ "cancelTournament": "Cancelar o torneo", "tournDescription": "Descrición do torneo", "tournDescriptionHelp": "Queres dicirlles algo en especial ós participantes? Tenta ser breve. Hai dispoñibles ligazóns de Markdown: [name](https://url)", - "ratedFormHelp": "As partidas son puntuadas\ne afectan ás puntuacións dos xogadores", + "ratedFormHelp": "As partidas son puntuadas e afectan ás puntuacións dos xogadores", "onlyMembersOfTeam": "Só membros do equipo", "noRestriction": "Sen restrición", "minimumRatedGames": "Mínimo de partidas puntuadas", @@ -1209,7 +1287,7 @@ "youCantStartNewGame": "Non podes comezar unha nova partida ata que esta remate.", "since": "Desde", "until": "Ata", - "lichessDbExplanation": "Partidas puntuadas de todos os xogadores de Lichess", + "lichessDbExplanation": "Partidas puntuadas xogadas en Lichess", "switchSides": "Cambiar de cor", "closingAccountWithdrawAppeal": "Se pechas a túa conta, retirarás a túa apelación", "ourEventTips": "Os nosos consellos para organizar eventos", @@ -1217,6 +1295,7 @@ "showMeEverything": "Amósamo todo", "lichessPatronInfo": "Lichess é unha organización benéfica e un programa totalmente libre e de código aberto.\nTodos os custos de funcionamento, desenvolvemento e contidos fináncianse unicamente mediante as doazóns dos usuarios.", "nothingToSeeHere": "Nada que ver aquí polo de agora.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O teu opoñente saíu da partida. Poderás reclamar a vitoria en {count} segundo.} other{O teu opoñente saíu da partida. Poderás reclamar a vitoria en {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} media xogada} other{Mate en {count} medias xogadas}}", "nbBlunders": "{count, plural, =1{{count} metida de zoca} other{{count} metidas de zoca}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 quenda} other{{count} quendas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Xogou unha quenda de {param2}} other{Xogou {count} quendas de {param2}}}", "streamerLichessStreamers": "Presentadores de Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Os meus estudos", + "studyStudiesIContributeTo": "Estudos nos que contribúo", + "studyMyPublicStudies": "Os meus estudos públicos", + "studyMyPrivateStudies": "Os meus estudos privados", + "studyMyFavoriteStudies": "Os meus estudos favoritos", + "studyWhatAreStudies": "Que son os estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos creados por {param}", + "studyNoneYet": "Aínda non hai.", + "studyHot": "Candentes", + "studyDateAddedNewest": "Data engadida (máis novos)", + "studyDateAddedOldest": "Data engadida (máis antigos)", + "studyRecentlyUpdated": "Actualizados recentemente", + "studyMostPopular": "Máis populares", + "studyAlphabetical": "Alfabeticamente", + "studyAddNewChapter": "Engadir un novo capítulo", + "studyAddMembers": "Engadir membros", + "studyInviteToTheStudy": "Invitar ao estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convida só a persoas que te coñezan e que desexen activamente unirse a este estudo.", + "studySearchByUsername": "Buscar por nome de usuario", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Deixar o estudo", + "studyYouAreNowAContributor": "Agora es un colaborador", + "studyYouAreNowASpectator": "Agora es un espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gústame", + "studyUnlike": "Xa non me gusta", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar nesta posición", + "studyCommentThisMove": "Comentar este movemento", + "studyAnnotateWithGlyphs": "Anotar con símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é demasiado curto para analizalo.", + "studyOnlyContributorsCanRequestAnalysis": "Só os colaboradores do estudo poden solicitar unha análise por ordenador.", + "studyGetAFullComputerAnalysis": "Obtén unha análise completa da liña principal dende o servidor.", + "studyMakeSureTheChapterIsComplete": "Asegúrate de que o capítulo está completo. Só podes solicitar a análise unha vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros sincronizados permanecen na mesma posición", + "studyShareChanges": "Comparte os cambios cos espectadores e gárdaos no servidor", + "studyPlaying": "En xogo", + "studyShowEvalBar": "Indicadores de avaliación", + "studyFirst": "Primeiro", + "studyPrevious": "Anterior", + "studyNext": "Seguinte", + "studyLast": "Último", "studyShareAndExport": "Compartir e exportar", - "studyStart": "Comezar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN do estudo", + "studyDownloadAllGames": "Descargar todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descargar partida", + "studyStudyUrl": "URL do estudo", + "studyCurrentChapterUrl": "Ligazón do capítulo actual", + "studyYouCanPasteThisInTheForumToEmbed": "Podes pegar esta URL no foro ou no teu blog de Lichess para incrustala", + "studyStartAtInitialPosition": "Comezar desde a posición inicial do estudo", + "studyStartAtX": "Comezar en {param}", + "studyEmbedInYourWebsite": "Incrustar na túa páxina web ou blog", + "studyReadMoreAboutEmbedding": "Ler máis sobre como inserir contido", + "studyOnlyPublicStudiesCanBeEmbedded": "Só se poden inserir estudos públicos!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1} traído para ti por {param2}", + "studyStudyNotFound": "Estudo non atopado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientación", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentario do capítulo fixado", + "studySaveChapter": "Gardar capítulo", + "studyClearAnnotations": "Borrar anotacións", + "studyClearVariations": "Borrar variantes", + "studyDeleteChapter": "Borrar capítulo", + "studyDeleteThisChapter": "Realmente queres borrar o capítulo? Non hai volta atrás!", + "studyClearAllCommentsInThisChapter": "Borrar todos os comentarios, símbolos e marcas do capítulo", + "studyRightUnderTheBoard": "Xusto debaixo do taboleiro", + "studyNoPinnedComment": "Ningún", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar os seguintes movementos", + "studyInteractiveLesson": "Lección interactiva", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Baleiro", + "studyStartFromInitialPosition": "Comezar desde a posición inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Comezar dende unha posición personalizada", + "studyLoadAGameByUrl": "Cargar as partidas dende unha URL", + "studyLoadAPositionFromFen": "Cargar unha posición dende o FEN", + "studyLoadAGameFromPgn": "Cargar as partidas dende o PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "Ligazóns das partidas, unha por liña", + "studyLoadAGameFromXOrY": "Cargar partidas dende {param1} ou {param2}", + "studyCreateChapter": "Crear capítulo", + "studyCreateStudy": "Crear estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Sen listar", + "studyInviteOnly": "Acceso só mediante invitación", + "studyAllowCloning": "Permitir clonado", + "studyNobody": "Ninguén", + "studyOnlyMe": "Só eu", + "studyContributors": "Colaboradores", + "studyMembers": "Membros", + "studyEveryone": "Todo o mundo", + "studyEnableSync": "Activar sincronización", + "studyYesKeepEveryoneOnTheSamePosition": "Si: todos verán a mesma posición", + "studyNoLetPeopleBrowseFreely": "Non: permitir que a xente navegue libremente", + "studyPinnedStudyComment": "Comentario fixado do estudo", + "studyStart": "Comezar", + "studySave": "Gardar", + "studyClearChat": "Borrar a sala de conversa", + "studyDeleteTheStudyChatHistory": "Borrar o historial da sala de conversa? Esta acción non se pode desfacer!", + "studyDeleteStudy": "Borrar estudo", + "studyConfirmDeleteStudy": "Borrar todo o estudo? Non se poderá recuperar! Teclea o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde queres estudar isto?", + "studyGoodMove": "Bo movemento", + "studyMistake": "Erro", + "studyBrilliantMove": "Movemento brillante", + "studyBlunder": "Metida de zoca", + "studyInterestingMove": "Movemento interesante", + "studyDubiousMove": "Movemento dubidoso", + "studyOnlyMove": "Movemento único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posición igualada", + "studyUnclearPosition": "Posición pouco clara", + "studyWhiteIsSlightlyBetter": "As brancas están lixeiramente mellor", + "studyBlackIsSlightlyBetter": "As negras están lixeiramente mellor", + "studyWhiteIsBetter": "As brancas están mellor", + "studyBlackIsBetter": "As negras están mellor", + "studyWhiteIsWinning": "As brancas están gañando", + "studyBlackIsWinning": "As negras están gañando", + "studyNovelty": "Novidade", + "studyDevelopment": "Desenvolvemento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contraataque", + "studyTimeTrouble": "Apuros de tempo", + "studyWithCompensation": "Con compensación", + "studyWithTheIdea": "Coa idea", + "studyNextChapter": "Capítulo seguinte", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Accións de estudo", + "studyTopics": "Temas", + "studyMyTopics": "Os meus temas", + "studyPopularTopics": "Temas populares", + "studyManageTopics": "Administrar temas", + "studyBack": "Voltar", + "studyPlayAgain": "Xogar de novo", + "studyWhatWouldYouPlay": "Que xogarías nesta posición?", + "studyYouCompletedThisLesson": "Parabéns! Completaches esta lección.", + "studyPerPage": "{param} por páxina", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega o teu texto PGN aquí, ata {count} partida} other{Pega o teu texto PGN aquí, ata {count} partidas}}", + "timeagoJustNow": "agora mesmo", + "timeagoRightNow": "agora", + "timeagoCompleted": "completado", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segundo} other{en {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{en {count} día} other{en {count} días}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semana} other{en {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{en {count} ano} other{en {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Hai {count} minuto} other{Hai {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{Hai {count} hora} other{Hai {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{Hai {count} día} other{Hai {count} días}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Hai {count} semana} other{Hai {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Hai {count} mes} other{Hai {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{Hai {count} ano} other{Hai {count} anos}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gsw.arb b/lib/l10n/lila_gsw.arb index eb1c30da21..95e7e08ea3 100644 --- a/lib/l10n/lila_gsw.arb +++ b/lib/l10n/lila_gsw.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "All Schpiel", + "mobileAreYouSure": "Bisch sicher?", + "mobileBlindfoldMode": "Blind schpille", + "mobileCancelTakebackOffer": "Zugsrücknam-Offerte zruggzieh", + "mobileClearButton": "Leere", + "mobileCorrespondenceClearSavedMove": "Lösch die gschpeicherete Züg", + "mobileCustomGameJoinAGame": "Es Schpiel mitschpille", + "mobileFeedbackButton": "Rückmäldig", + "mobileGreeting": "Hoi, {param}", + "mobileGreetingWithoutName": "Hoi", + "mobileHideVariation": "Variante verberge", "mobileHomeTab": "Afangssite", - "mobilePuzzlesTab": "Ufgabe", - "mobileToolsTab": "Werchzüg", - "mobileWatchTab": "Luege", - "mobileSettingsTab": "Ischtelle", + "mobileLiveStreamers": "Live Streamer", "mobileMustBeLoggedIn": "Muesch iglogt si, zum die Site z'gseh.", - "mobileSystemColors": "Syschtem-Farbe", - "mobileFeedbackButton": "Rückmäldig", + "mobileNoSearchResults": "Nüt g'funde", + "mobileNotFollowingAnyUser": "Du folgsch keim Schpiller.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Schpiller mit \"{param}%", + "mobilePrefMagnifyDraggedPiece": "Vegrösserig vu de zogene Figur", + "mobilePuzzleStormConfirmEndRun": "Wottsch de Lauf beände?", + "mobilePuzzleStormFilterNothingToShow": "Nüt zum Zeige, bitte d'Filter ändere", + "mobilePuzzleStormNothingToShow": "Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Lös i 3 Minute so vill Ufgabe wie möglich.", + "mobilePuzzleStreakAbortWarning": "Du verlürsch din aktuelle Lauf und din Rekord wird g'schpeicheret.", + "mobilePuzzleThemesSubtitle": "Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.", + "mobilePuzzlesTab": "Ufgabe", + "mobileRecentSearches": "Kürzlich Gsuechts", "mobileSettingsHapticFeedback": "Rückmäldig mit Vibration", "mobileSettingsImmersiveMode": "Ibettete Modus", "mobileSettingsImmersiveModeSubtitle": "UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.", - "mobileNotFollowingAnyUser": "Du folgsch keim Schpiller.", - "mobileAllGames": "Alli Partie", - "mobileRecentSearches": "Kürzlich Gsuechts", - "mobileClearButton": "Leere", - "mobilePlayersMatchingSearchTerm": "Schpiller mit \"{param}%", - "mobileNoSearchResults": "Nüt g'funde", - "mobileAreYouSure": "Bisch sicher?", - "mobilePuzzleStreakAbortWarning": "Du verlürsch din aktuelle Lauf und din Rekord wird g'schpeicheret.", - "mobilePuzzleStormNothingToShow": "Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.", - "mobileSharePuzzle": "Teil die Ufgab", - "mobileShareGameURL": "Teil d'Schpiel-URL", + "mobileSettingsTab": "Ischtelle", "mobileShareGamePGN": "Teil s'PGN", + "mobileShareGameURL": "Teil d'Schpiel-URL", "mobileSharePositionAsFEN": "Teil d'Position als FEN", - "mobileShowVariations": "Zeig Variante", - "mobileHideVariation": "Variante verberge", + "mobileSharePuzzle": "Teil die Ufgab", "mobileShowComments": "Zeig Kommentär", - "mobilePuzzleStormConfirmEndRun": "Wottsch de Lauf beände?", - "mobilePuzzleStormFilterNothingToShow": "Nüt zum Zeige, bitte d'Filter ändere", - "mobileCancelTakebackOffer": "Zugsrücknam-Offerte zruggzieh", - "mobileCancelDrawOffer": "Remis-Agebot zruggzieh", - "mobileWaitingForOpponentToJoin": "Warte bis en Gegner erschint...", - "mobileBlindfoldMode": "Blind schpille", - "mobileLiveStreamers": "Live Streamer", - "mobileCustomGameJoinAGame": "Bi ere Partie mitschpille", - "mobileCorrespondenceClearSavedMove": "Lösch die gschpeicherete Züg", - "mobileSomethingWentWrong": "Es isch öppis schief gange.", "mobileShowResult": "Resultat zeige", - "mobilePuzzleThemesSubtitle": "Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.", - "mobilePuzzleStormSubtitle": "Lös i 3 Minute so vill Ufgabe wie möglich.", - "mobileGreeting": "Hoi, {param}", - "mobileGreetingWithoutName": "Hoi", + "mobileShowVariations": "Zeig Variante", + "mobileSomethingWentWrong": "Es isch öppis schief gange.", + "mobileSystemColors": "Syschtem-Farbe", + "mobileTheme": "Farbschema", + "mobileToolsTab": "Werchzüg", + "mobileWaitingForOpponentToJoin": "Warte bis en Gegner erschint...", + "mobileWatchTab": "Luege", "activityActivity": "Aktivitäte", "activityHostedALiveStream": "Hät en Live Stream gmacht", "activityRankedInSwissTournament": "Hät Rang #{param1} im Turnier {param2} erreicht", @@ -49,11 +50,12 @@ "activitySupportedNbMonths": "{count, plural, =1{Underschtüzt lichess.org sit {count} Monet als {param2}} other{Underschtützt lichess.org sit {count} Mönet als {param2}}}", "activityPracticedNbPositions": "{count, plural, =1{Hät {count} Schtellig bi {param2} güebt} other{Hät {count} Schtellige bi {param2} güebt}}", "activitySolvedNbPuzzles": "{count, plural, =1{Hät {count} Taktikufgab glöst} other{Hät {count} Taktikufgabe glöst}}", - "activityPlayedNbGames": "{count, plural, =1{Hät {count} Partie {param2} gschpillt} other{Hät {count} Partie {param2} gschpillt}}", + "activityPlayedNbGames": "{count, plural, =1{Hät {count} Schpiel {param2} gschpillt} other{Hät {count} Schpiel {param2} gschpillt}}", "activityPostedNbMessages": "{count, plural, =1{Hät {count} Nachricht in {param2} gschribe} other{Hät {count} Nachrichte in {param2} gschribe}}", "activityPlayedNbMoves": "{count, plural, =1{Hät {count} Zug gschpillt} other{Hät {count} Züg gschpillt}}", - "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} Fernschachpartie} other{i {count} Fernschachpartie}}", - "activityCompletedNbGames": "{count, plural, =1{Hät {count} Fernschachpartie gschpillt} other{Hät {count} Fernschachpartie gschpillt}}", + "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} Korrespondänz-Schpiel} other{in {count} Korrespondänz-Schpiel}}", + "activityCompletedNbGames": "{count, plural, =1{Hät {count} Korrespondänz-Schpiel gschpillt} other{Hät {count} Korrespondänz-Schpiel gschpillt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} {param2} Korrespondänz-Schpiel gschpillt} other{{count} {param2} Korrespondänz-Schpiel gschpillt}}", "activityFollowedNbPlayers": "{count, plural, =1{Folgt {count} Schpiller} other{Folgt {count} Schpiller}}", "activityGainedNbFollowers": "{count, plural, =1{Hät {count} neui Folgendi} other{Hät {count} neui Folgendi}}", "activityHostedNbSimuls": "{count, plural, =1{Hät {count} Simultanschach gmacht} other{Hät {count} Simultanschachs gmacht}}", @@ -64,9 +66,84 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Hät a {count} Turnier nach \"Schweizer System\" teilgnah} other{Hät a {count} Turnier nach \"Schweizer System\" teilgnah}}", "activityJoinedNbTeams": "{count, plural, =1{Isch {count} Team biträte} other{Isch {count} Teams biträte}}", "broadcastBroadcasts": "Überträgige", + "broadcastMyBroadcasts": "Eigeni Überträgige", "broadcastLiveBroadcasts": "Live Turnier-Überträgige", + "broadcastBroadcastCalendar": "Überträgigs-Kaländer", + "broadcastNewBroadcast": "Neui Live-Überträgige", + "broadcastSubscribedBroadcasts": "Abonnierti Überträgige", + "broadcastAboutBroadcasts": "Über Überträgige", + "broadcastHowToUseLichessBroadcasts": "Wie mer Lichess-Überträgige benutzt.", + "broadcastTheNewRoundHelp": "Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig.", + "broadcastAddRound": "E Rundi zuefüege", + "broadcastOngoing": "Laufend", + "broadcastUpcoming": "Demnächscht", + "broadcastCompleted": "Beändet", + "broadcastCompletedHelp": "Lichess erkännt de Rundeschluss oder au nöd! De Schalter setz das uf \"manuell\".", + "broadcastRoundName": "Runde Name", + "broadcastRoundNumber": "Runde Nummere", + "broadcastTournamentName": "Turnier Name", + "broadcastTournamentDescription": "Churzi Turnier Beschribig", + "broadcastFullDescription": "Vollschtändigi Ereignisbeschribig", + "broadcastFullDescriptionHelp": "Optionali, usfüehrlichi Beschribig vu de Überträgig. {param1} isch verfügbar. Die Beschribig muess chürzer als {param2} Zeiche si.", + "broadcastSourceSingleUrl": "PGN Quälle URL", + "broadcastSourceUrlHelp": "URL wo Lichess abfrögt, für PGN Aktualisierige z'erhalte. Sie muess öffentlich im Internet zuegänglich si.", + "broadcastSourceGameIds": "Bis zu 64 Lichess Schpiel-IDs, trännt dur en Leerschlag.", + "broadcastStartDateTimeZone": "Startdatum i de lokale Zitzone vum Turnier: {param}", + "broadcastStartDateHelp": "Optional, falls du weisch, wänn das Ereignis afangt", + "broadcastCurrentGameUrl": "URL vom laufende Schpiel", + "broadcastDownloadAllRounds": "Alli Runde abelade", + "broadcastResetRound": "Die Rundi zruggsetze", + "broadcastDeleteRound": "Die Rundi lösche", + "broadcastDefinitivelyDeleteRound": "Die Rundi, mit allne Schpiel, definitiv lösche.", + "broadcastDeleteAllGamesOfThisRound": "Lösch alli Schpiel vu dere Rundi. D'Quälle muess aktiv si, dass sie neu erschtellt werde chönd.", + "broadcastEditRoundStudy": "Runde-Schtudie bearbeite", + "broadcastDeleteTournament": "Lösch das Turnier", + "broadcastDefinitivelyDeleteTournament": "Das ganze Turnier, alli Runde und alli Schpiel definitiv lösche.", + "broadcastShowScores": "Zeigt d'Erfolg vu de Schpiller, anhand vu Schpiel-Ergäbnis", + "broadcastReplacePlayerTags": "Optional: Schpillernäme, Wertige und Titel weg lah", + "broadcastFideFederations": "FIDE Wältschachverband", + "broadcastTop10Rating": "Top 10 Ratings", + "broadcastFidePlayers": "FIDE Schpiller", + "broadcastFidePlayerNotFound": "FIDE Schpiller nöd g'funde", + "broadcastFideProfile": "FIDE Profil", + "broadcastFederation": "Verband", + "broadcastAgeThisYear": "Alter i dem Jahr", + "broadcastUnrated": "Ungwertet", + "broadcastRecentTournaments": "Aktuellschti Turnier", + "broadcastOpenLichess": "In Lichess öffne", + "broadcastTeams": "Teams", + "broadcastBoards": "Brätter", + "broadcastOverview": "Überblick", + "broadcastSubscribeTitle": "Mäld dich a, zum über jede Rundeschtart informiert z'werde. Du chasch de Alarm- oder d'Push-Benachrichtigung, für Überträgige, i dine Kontoischtellige umschalte.", + "broadcastUploadImage": "Turnier-Bild ufelade", + "broadcastNoBoardsYet": "No kei Brätter. Die erschined, sobald Schpiel ufeglade sind.", + "broadcastBoardsCanBeLoaded": "Brätter chönd mit ere Quälle oder via {param} ufeglade werde", + "broadcastStartsAfter": "Schtartet nach {param}", + "broadcastStartVerySoon": "Die Überträgig schtartet sehr bald.", + "broadcastNotYetStarted": "Die Überträgig hät nonig agfange.", + "broadcastOfficialWebsite": "Offizielli Website", + "broadcastStandings": "Tabälle", + "broadcastOfficialStandings": "Offizielli Ranglischte", + "broadcastIframeHelp": "Meh Optionen uf {param}", + "broadcastWebmastersPage": "Webmaster Site", + "broadcastPgnSourceHelp": "Öffentlichi, real-time PGN Quälle, für die Rundi. Mir offeriered au {param} für e schnälleri und effiziänteri Synchronisation.", + "broadcastEmbedThisBroadcast": "Nimm die Überträgig uf dini Website", + "broadcastEmbedThisRound": "Nimm {param} uf dini Website", + "broadcastRatingDiff": "Wertigs Differänz", + "broadcastGamesThisTournament": "Schpiel i dem Turnier", + "broadcastScore": "Resultat", + "broadcastAllTeams": "Alli Teams", + "broadcastTournamentFormat": "Turnier-Format", + "broadcastTournamentLocation": "Turnier-Lokal", + "broadcastTopPlayers": "Top-Schpiller", + "broadcastTimezone": "Zitzone", + "broadcastFideRatingCategory": "FIDE-Wertigskategorie", + "broadcastOptionalDetails": "Optionali Details", + "broadcastPastBroadcasts": "G'machti Überträgige", + "broadcastAllBroadcastsByMonth": "Zeig alli Überträgige im Monet", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Überträgige} other{{count} Überträgige}}", "challengeChallengesX": "Useforderige: {param1}", - "challengeChallengeToPlay": "Zunere Partie usefordere", + "challengeChallengeToPlay": "Zum Schpiel fordere", "challengeChallengeDeclined": "Useforderig abglehnt", "challengeChallengeAccepted": "Useforderig agnah!", "challengeChallengeCanceled": "Useforderig zrugg'zoge.", @@ -95,12 +172,12 @@ "perfStatPerfStats": "{param}-Schtatischtike", "perfStatViewTheGames": "Schpil azeige", "perfStatProvisional": "provisorisch", - "perfStatNotEnoughRatedGames": "Nöd gnueg gwerteti Schpil, für e verlässlichi Wertig z'errächne.", + "perfStatNotEnoughRatedGames": "Nöd gnueg g'werteti Schpil, zum e verlässlichi Wertig z'errächne.", "perfStatProgressOverLastXGames": "Fortschritt über di letschte {param} Schpil:", "perfStatRatingDeviation": "Wertigssabwichig: {param}.", "perfStatRatingDeviationTooltip": "En niedrige Wert bedütet, dass d'Wertig schtabiler isch. Über {param1} wird d'Wertig als provisorisch betrachtet. In Ranglischtene chunnt mer, wänn de Wert under {param2} (Standard) oder {param3} (Variante) isch.", "perfStatTotalGames": "Alli Schpil", - "perfStatRatedGames": "Gwerteti Schpil", + "perfStatRatedGames": "G'werteti Schpiel", "perfStatTournamentGames": "Turnier Schpil", "perfStatBerserkedGames": "Berserk Schpil", "perfStatTimeSpentPlaying": "Gsamti Schpillzit", @@ -117,7 +194,7 @@ "perfStatLongestStreak": "Längschti Serie: {param}", "perfStatCurrentStreak": "Aktuelli Serie: {param}", "perfStatBestRated": "die beschte Sieg", - "perfStatGamesInARow": "In Serie gschpillti Partie", + "perfStatGamesInARow": "Nachenand g'schpillti Schpiel", "perfStatLessThanOneHour": "Weniger als 1 Schtund zwüsche de Schpil", "perfStatMaxTimePlaying": "Maximali Schpillzit", "perfStatNow": "jetzt", @@ -137,7 +214,7 @@ "preferencesZenMode": "Zen Modus", "preferencesShowPlayerRatings": "Zeig Schpiller Wertige", "preferencesShowFlairs": "Benutzer-Emojis azeige", - "preferencesExplainShowPlayerRatings": "Das erlaubt s'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z'konzentriere. Partie chönd immer no bewertet werde, es gaht nur um das, was du gsesch.", + "preferencesExplainShowPlayerRatings": "Das erlaubt s'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z'konzentriere. s'Schpiel wird immer no bewertet werde, es gaht nur drum, was mer gseht.", "preferencesDisplayBoardResizeHandle": "Zeig de Brättgrössi Regler", "preferencesOnlyOnInitialPosition": "Nur bi de Afangsschtellig", "preferencesInGameOnly": "Nur im Schpiel", @@ -154,7 +231,7 @@ "preferencesBothClicksAndDrag": "Beides", "preferencesPremovesPlayingDuringOpponentTurn": "Voruszüg (Premoves), während de Gägner am Zug isch", "preferencesTakebacksWithOpponentApproval": "Zugsrücknahm (mit Erlaubnis vom Gägner)", - "preferencesInCasualGamesOnly": "Nur in ungwertete Schpiel", + "preferencesInCasualGamesOnly": "Nur in nöd g'wertete Schpiel", "preferencesPromoteToQueenAutomatically": "Automatischi Umwandlig zur Dame", "preferencesExplainPromoteToQueenAutomatically": "Druck bi de Umwandlig, so wird sie vorübergehend abgschtellt", "preferencesWhenPremoving": "Bim Voruszug", @@ -174,7 +251,7 @@ "preferencesSayGgWpAfterLosingOrDrawing": "Säg \"guet gschpillt\" nach ere Niderlag oder bi me Remis", "preferencesYourPreferencesHaveBeenSaved": "Dini Ischtellige sind gschpeicheret.", "preferencesScrollOnTheBoardToReplayMoves": "Mit em Muszeiger uf em Brätt, chasch mit em Musrad all Züg vor- und zrugg scrolle", - "preferencesCorrespondenceEmailNotification": "Täglichi E-Mail-Benachrichtigung, wo Dini Fernschachpartie uflischtet", + "preferencesCorrespondenceEmailNotification": "Täglichi E-Mail-Benachrichtigung, wo dini Korrespondänz-Schpiel uflischtet", "preferencesNotifyStreamStart": "De Streamer gaht live", "preferencesNotifyInboxMsg": "Neui Nachricht im Poschtigang", "preferencesNotifyForumMention": "En Forumkommentar erwähnt dich", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Grät", "preferencesBellNotificationSound": "Ton für Benachrichtige", + "preferencesBlindfold": "Blind schpille", "puzzlePuzzles": "Ufgabe", "puzzlePuzzleThemes": "Ufgabe Theme", "puzzleRecommended": "Empfohle", @@ -218,14 +296,14 @@ "puzzlePuzzleComplete": "Ufgab abgschlosse!", "puzzleByOpenings": "Nach Eröffnige", "puzzlePuzzlesByOpenings": "Ufgabe nach Eröffnige", - "puzzleOpeningsYouPlayedTheMost": "Dini meischt-gschpillte Eröffnige, i gwertete Partie", + "puzzleOpeningsYouPlayedTheMost": "Dini meischt-gschpillte Eröffnige, i g'wertete Schpiel", "puzzleUseFindInPage": "Benutz im Browser \"Suchen...\", um dini bevorzugti Eröffnig z'finde!", "puzzleUseCtrlF": "Find dini bevorzugti Eröffnig mit Ctrl+F !", "puzzleNotTheMove": "Das isch nöd de Zug!", "puzzleTrySomethingElse": "Probier öppis Anders.", "puzzleRatingX": "Wertig: {param}", "puzzleHidden": "verschteckt", - "puzzleFromGameLink": "Us de Partie {param}", + "puzzleFromGameLink": "Vom Schpiel {param}", "puzzleContinueTraining": "Witer trainiere", "puzzleDifficultyLevel": "Schwierigi Stufe", "puzzleNormal": "Normal", @@ -249,11 +327,11 @@ "puzzleContinueTheStreak": "Erfolgsserie witerfüehre", "puzzleNewStreak": "Neui Erfolgsserie", "puzzleFromMyGames": "Us eigene Schpil", - "puzzleLookupOfPlayer": "Suech Ufgabe us de Partie vu me Schpiller", - "puzzleFromXGames": "Ufgabe us Partie vu {param}", + "puzzleLookupOfPlayer": "Suech Ufgabe us de Schpiel vu me Schpiller", + "puzzleFromXGames": "Ufgabe us Schpiel vu {param}", "puzzleSearchPuzzles": "Suech Ufgabe", - "puzzleFromMyGamesNone": "Eu häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und klassischi Partie, so erhöht sich d'Chance, dass au Ufgabe us dine eigene Schpil zuegfüegt werded!", - "puzzleFromXGamesFound": "{param1} Ufgabe in {param2} Partie gfunde", + "puzzleFromMyGamesNone": "Du häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und au klassischi Schpiel, will so erhöht sich d'Chance, dass Ufgabe vu dine eigene Schpiel zuegfüegt werded!", + "puzzleFromXGamesFound": "{param1} Ufgabe in Schpiel vu {param2} gfunde", "puzzlePuzzleDashboardDescription": "Trainiere analysiere und besser werde", "puzzlePercentSolved": "{param} glöst", "puzzleNoPuzzlesToShow": "Es git no nüt zum Zeige, lös zerscht es paar Ufgabe!", @@ -307,7 +385,7 @@ "puzzleThemeDoubleCheck": "Doppelschach", "puzzleThemeDoubleCheckDescription": "Abzug mit doppletem Schachgebot, wobi die vorher verdeckti Figur- und die Abzogeni, de König glichzitig agrifed.", "puzzleThemeEndgame": "Ändschpil", - "puzzleThemeEndgameDescription": "E Taktik für die letscht Fase vu de Partie.", + "puzzleThemeEndgameDescription": "E Taktik für die letscht Fase vum Schpiel.", "puzzleThemeEnPassantDescription": "E Taktik wo \"En-Passant\" beinhaltet - e Regle wo en Pur cha en gägnerische Pur schlaa, wänn de ihn mit em \"Zwei-Fälder-Zug\" übergange hät.", "puzzleThemeExposedKing": "Exponierte König", "puzzleThemeExposedKingDescription": "E Taktik wo de König nu vu wenige Figure verteidigt wird und oft zu Schachmatt fühert.", @@ -325,10 +403,10 @@ "puzzleThemeKnightEndgameDescription": "Es Ändschpiel, nur mit Schpringer und Pure.", "puzzleThemeLong": "Mehrzügigi Ufgab", "puzzleThemeLongDescription": "3 Züg zum Sieg.", - "puzzleThemeMaster": "Meischter Partie", - "puzzleThemeMasterDescription": "Ufgabe us Partie vu Schpiller mit Titel.", - "puzzleThemeMasterVsMaster": "Meischter gäge Meischter Partie", - "puzzleThemeMasterVsMasterDescription": "Ufgabe us Partie vu 2 Schpiller mit Titel.", + "puzzleThemeMaster": "Meischter Schpiel", + "puzzleThemeMasterDescription": "Ufgabe us Schpiel vu Schpiller mit Titel.", + "puzzleThemeMasterVsMaster": "Meischter gäge Meischter Schpiel", + "puzzleThemeMasterVsMasterDescription": "Ufgabe us Schpiel vu 2 Schpiller mit Titel.", "puzzleThemeMate": "Schachmatt", "puzzleThemeMateDescription": "Günn das Schpiel mit Schtil.", "puzzleThemeMateIn1": "Matt in 1", @@ -342,11 +420,11 @@ "puzzleThemeMateIn5": "Matt in 5 oder meh", "puzzleThemeMateIn5Description": "Find e langi Mattfüherig.", "puzzleThemeMiddlegame": "Mittelschpiel", - "puzzleThemeMiddlegameDescription": "E Taktik für die zweit Fase vu de Partie.", + "puzzleThemeMiddlegameDescription": "E Taktik für die zweit Fase vum Schpiel.", "puzzleThemeOneMove": "1-zügigi Ufgab", "puzzleThemeOneMoveDescription": "E Ufgab, mit nur 1 Zug.", "puzzleThemeOpening": "Eröffnig", - "puzzleThemeOpeningDescription": "E Taktik für die erscht Fase vu de Partie.", + "puzzleThemeOpeningDescription": "E Taktik für die erscht Fase vum Schpiel.", "puzzleThemePawnEndgame": "Pure Ändschpiel", "puzzleThemePawnEndgameDescription": "Es Ändschpiel nur mit Pure.", "puzzleThemePin": "Fesslig", @@ -371,8 +449,8 @@ "puzzleThemeSkewerDescription": "Bim \"Schpiess\" wird e Figur, wo vor ere Andere staht - oft wird sie au dezue zwunge, sich vor die Ander z'schtelle - agriffe, so dass sie us em Wäg muess und ermöglicht, die Hinder z'schlah. Quasi umgekehrti Fesslig.", "puzzleThemeSmotheredMate": "Erschtickigs Matt", "puzzleThemeSmotheredMateDescription": "Es Schachmatt mit em Springer, wo sich de König nöd bewege cha, will er vu sine eigene Figuren umstellt-, also vollkomme igschlosse, wird.", - "puzzleThemeSuperGM": "Super-Grossmeischter-Partie", - "puzzleThemeSuperGMDescription": "Ufgabe us Partie, vu de beschte Schpiller uf de Wält.", + "puzzleThemeSuperGM": "Super-Grossmeischter-Schpiel", + "puzzleThemeSuperGMDescription": "Ufgabe us Schpiel, vu de beschte Schpiller uf de Wält.", "puzzleThemeTrappedPiece": "G'fangeni Figur", "puzzleThemeTrappedPieceDescription": "E Figur cha em Schlah nöd entgah, will sie nur begränzt Züg mache cha.", "puzzleThemeUnderPromotion": "Underverwandlig", @@ -383,9 +461,9 @@ "puzzleThemeXRayAttackDescription": "E Figur attackiert oder verteidigt es Fäld dur e gägnerischi Figur.", "puzzleThemeZugzwang": "Zugszwang", "puzzleThemeZugzwangDescription": "De Gägner hät nur e limitierti Azahl Züg und Jede verschlächteret sini Schtellig.", - "puzzleThemeHealthyMix": "En gsunde Mix", - "puzzleThemeHealthyMixDescription": "Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g'fasst - genau wie bi richtige Schachschpiel.", - "puzzleThemePlayerGames": "Schpiller Partie", + "puzzleThemeMix": "En gsunde Mix", + "puzzleThemeMixDescription": "Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g'fasst - genau wie bi richtige Schachschpiel.", + "puzzleThemePlayerGames": "Schpiller-Schpiel", "puzzleThemePlayerGamesDescription": "Suech nach Ufgabe us dine Schpiel oder Ufgabe us Schpiel vu Andere.", "puzzleThemePuzzleDownloadInformation": "Die Ufgabe sind öffentlich, mer channs abelade under {param}.", "searchSearch": "Suechi", @@ -431,7 +509,7 @@ "variantEnding": "Variante ändet", "newOpponent": "En neue Gägner", "yourOpponentWantsToPlayANewGameWithYou": "Din Gägner wett es neus Schpil mit dir schpille", - "joinTheGame": "Tritt de Partie bi", + "joinTheGame": "Gang is Schpiel", "whitePlays": "Wiss am Zug", "blackPlays": "Schwarz am Zug", "opponentLeftChoices": "Din Gägner hät s'Schpiel verlah. Du chasch de Sieg beaschpruche, es Remis mache oder warte.", @@ -442,7 +520,7 @@ "whiteResigned": "Wiss hät ufgeh", "blackResigned": "Schwarz hät ufgeh", "whiteLeftTheGame": "Wiss hät d'Partie verlah", - "blackLeftTheGame": "Schwarz hät d'Partie verlah", + "blackLeftTheGame": "Schwarz hät s'Schpiel verlah", "whiteDidntMove": "Wiss hät nöd zoge", "blackDidntMove": "Schwarz hät nöd zoge", "requestAComputerAnalysis": "Computer-Analyse afordere", @@ -481,9 +559,9 @@ "database": "Datäbank", "whiteDrawBlack": "Wiss / Remis / Schwarz", "averageRatingX": "Durchschnittlichi Wertigszahl: {param}", - "recentGames": "Aktuelli Partie", - "topGames": "Beschti-Partie", - "masterDbExplanation": "2 Millione OTB Schpil vu {param1}+ FIDE-gwertete Schpiller vu {param2} bis {param3}", + "recentGames": "Aktuelli Schpiel", + "topGames": "Schpitzeschpiel", + "masterDbExplanation": "2 Millione OTB Schpiel vu {param1}+ FIDE-g'wertete Schpiller vu {param2} bis {param3}", "dtzWithRounding": "DTZ50'' mit Rundig, basierend uf de Azahl Halbzüg bis zum nächschte Schlag- oder Pure-Zug", "noGameFound": "Keis Schpiel gfunde", "maxDepthReached": "Die maximali Tüfi isch erreicht!", @@ -505,7 +583,6 @@ "replayMode": "Widergabemodus", "realtimeReplay": "Ächtzit", "byCPL": "Nach CPL", - "openStudy": "Schtudie eröffne", "enable": "Ischalte", "bestMoveArrow": "Pfil für de bescht Zug", "showVariationArrows": "Pfil für Variante azeige", @@ -515,7 +592,6 @@ "memory": "Arbetsschpeicher", "infiniteAnalysis": "Unändlichi Analyse", "removesTheDepthLimit": "Entfernt d'Tüfebegränzig und haltet din Computer warm", - "engineManager": "Engine Betreuer", "blunder": "En Patzer", "mistake": "Fähler", "inaccuracy": "Ungnauigkeit", @@ -527,7 +603,7 @@ "draw": "Remis", "drawByMutualAgreement": "Remis dur Einigung", "fiftyMovesWithoutProgress": "50 Züg ohni Fortschritt", - "currentGames": "Laufendi Partie", + "currentGames": "Laufendi Schpiel", "viewInFullSize": "In voller Grössi azeige", "logOut": "Abmälde", "signIn": "Ilogge", @@ -535,7 +611,7 @@ "youNeedAnAccountToDoThat": "Für das bruchsch du es Benutzerkonto", "signUp": "Regischtriere", "computersAreNotAllowedToPlay": "Computer und Computer-Understützig isch nöd erlaubt. Bitte lass dir bim Schpille nöd vu Schachengines, Datäbanke oder andere Schpiller hälfe. \nAu vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multikonteverhalte chann zume Usschluss fühere.", - "games": "Partie", + "games": "Schpiel", "forum": "Forum", "xPostedInForumY": "{param1} hät zum Thema {param2} öppis gschribä", "latestForumPosts": "Neuschti Forum-Biträg", @@ -596,7 +672,8 @@ "accountRegisteredWithoutEmail": "s'Konto {param} isch ohni E-Mail-Adrässe regischtriert worde.", "rank": "Rang", "rankX": "Platz: {param}", - "gamesPlayed": "Gschpillti Partie", + "gamesPlayed": "Schpiel g'macht", + "ok": "OK", "cancel": "Abbräche", "whiteTimeOut": "Wiss hät d'Zit überschritte", "blackTimeOut": "Schwarz hät d'Zit überschritte", @@ -610,11 +687,11 @@ "yourOpponentOffersADraw": "Din Gägner offeriert es Remis", "accept": "Akzeptiere", "decline": "Ablehnä", - "playingRightNow": "Partie isch am laufe", + "playingRightNow": "Schpiel lauft", "eventInProgress": "Lauft jetzt", "finished": "Beändet", - "abortGame": "Partie abbräche", - "gameAborted": "Partie abbroche", + "abortGame": "Schpiel abbräche", + "gameAborted": "Schpiel abbroche", "standard": "Standard", "customPosition": "Benutzerdefinierti Schtellig", "unlimited": "Unbegränzt", @@ -623,7 +700,7 @@ "rated": "Gwertet", "casualTournament": "Ungwertet", "ratedTournament": "Gwertet", - "thisGameIsRated": "Das Schpiel isch gwertet", + "thisGameIsRated": "Das Schpiel isch g'wertet", "rematch": "Revanche", "rematchOfferSent": "Agebot zur Revanche gsändet", "rematchOfferAccepted": "Revanche-Agebot aktzeptiert", @@ -643,7 +720,7 @@ "send": "Schicke", "incrementInSeconds": "Inkrement in Sekunde", "freeOnlineChess": "Gratis Online-Schach", - "exportGames": "Partie exportiere", + "exportGames": "Schpiel exportiere", "ratingRange": "Wertigsbereich", "thisAccountViolatedTos": "Das Konto hät gäge d'Lichess-Nutzigsbedingige verschtosse", "openingExplorerAndTablebase": "Eröffnigsbuech & Ändschpiel-Datäbank", @@ -666,8 +743,8 @@ "yourPerfRatingIsTooHigh": "Dini {param1} Wertig ({param2}) isch z'höch", "yourTopWeeklyPerfRatingIsTooHigh": "Dini höchschti {param1} Wertig ({param2}) isch z'höch", "yourPerfRatingIsTooLow": "Dini {param1} Wertig ({param2}) isch z'tüüf", - "ratedMoreThanInPerf": "Gwertet ≥ {param1} in {param2}", - "ratedLessThanInPerf": "Wertig ≤ {param1} im {param2} die letschte 7 Täg", + "ratedMoreThanInPerf": "G'wertet ≥ {param1} in {param2}", + "ratedLessThanInPerf": "≤ {param1} im {param2} die letscht Wuche", "mustBeInTeam": "Du muesch im Team {param} si", "youAreNotInTeam": "Du bisch nöd im Team {param}", "backToGame": "Zrugg zum Schpiel", @@ -678,7 +755,7 @@ "xStartedStreaming": "{param} hät mit Streame agfange", "averageElo": "Durchschnittswertig", "location": "Ortschaft/Land", - "filterGames": "Partie filtere", + "filterGames": "Schpiel filtere", "reset": "Zruggsetze", "apply": "Awände", "save": "Schpeichere", @@ -691,7 +768,7 @@ "fromPosition": "Ab Schtellig", "continueFromHere": "Vu da us witer schpille", "toStudy": "Schtudie", - "importGame": "Partie importiere", + "importGame": "Schpiel importiere", "importGameExplanation": "Füeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL.", "importGameCaveat": "d'Variazione werded glöscht. Zums b'halte, muesch d'PGN mit ere Schtudie importiere.", "importGameDataPrivacyWarning": "De PGN isch öffentlich zuegänglich. Zum es Schpiel privat importiere, nimmsch e Schtudie.", @@ -713,7 +790,6 @@ "block": "Blockiärä", "blocked": "Blockiärt", "unblock": "Blockierig ufhebe", - "followsYou": "Folgt dir", "xStartedFollowingY": "{param1} folgt jetzt {param2}", "more": "Meh", "memberSince": "Mitglid sit", @@ -734,8 +810,8 @@ "inappropriateNameWarning": "Alles - au liecht Unagmässes - cha zur Schlüssig vu dim Konto fühere.", "emptyTournamentName": "Frei la zum s'Turnier nach eme namhafte Schachschpiller z'benänne.", "makePrivateTournament": "Mach das Turnier privat und beschränk de Zuegang mit Passwort", - "join": "Mach mit", - "withdraw": "Usstige", + "join": "Mitmache", + "withdraw": "Usschtige", "points": "Pünkt", "wins": "Sieg", "losses": "Niederlage", @@ -743,7 +819,7 @@ "tournamentIsStarting": "s'Turnier fangt a", "tournamentPairingsAreNowClosed": "Es werded kei Turnierschpiel meh gschpillt.", "standByX": "Achtung {param}, es git neui Paarige, mach dich parat!", - "pause": "underbräche", + "pause": "Pausiere", "resume": "wieder ischtige", "youArePlaying": "Dis Schpiel fangt a!", "winRate": "Gwünn Rate", @@ -787,9 +863,9 @@ "previouslyOnLichessTV": "Zletscht uf Lichess TV", "onlinePlayers": "Online Schpiller", "activePlayers": "Aktivi Schpiller", - "bewareTheGameIsRatedButHasNoClock": "Achtung, das Schpiel isch gwertet, aber ohni Zitlimit!", + "bewareTheGameIsRatedButHasNoClock": "Achtung, das Schpiel isch g'wertet, aber ohni Zitlimit!", "success": "Korräkt", - "automaticallyProceedToNextGameAfterMoving": "Nach em Zug automatisch zur nächschte Partie", + "automaticallyProceedToNextGameAfterMoving": "Nach em Zug automatisch zum nächschte Schpiel", "autoSwitch": "Automatische Wächsel", "puzzles": "Ufgabe", "onlineBots": "Online Roboter", @@ -819,7 +895,9 @@ "cheat": "Bschiss", "troll": "Troll", "other": "Suschtigs", - "reportDescriptionHelp": "Füeg de Link dere/dene Partie bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).", + "reportCheatBoostHelp": "Füeg de Link dem/dene Schpiel bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).", + "reportUsernameHelp": "Erchlär, was am Benutzername nöd in Ornig isch: Säg nöd eifach \"er isch beleidigend/unagmässe\", sondern schrib eus, wie du zu dere Folgerig cho bisch. B'sunders wänn die Beleidigung verschleieret-, nöd uf Änglisch- oder in Dialäkt isch oder wänn sie en historische oder kulturelle Bezug hät.", + "reportProcessedFasterInEnglish": "Änglisch g'schribeni Mäldige werded schnäller behandlet.", "error_provideOneCheatedGameLink": "Bitte gib mindeschtens 1 Link zume Schpiel a, wo bschisse worde isch.", "by": "vu {param}", "importedByX": "Importiert vu {param}", @@ -853,7 +931,7 @@ "insideTheBoard": "Uf em Brätt", "outsideTheBoard": "Usse vum Brätt", "allSquaresOfTheBoard": "Uf jedem Fäld", - "onSlowGames": "Bi langsame Partie", + "onSlowGames": "Bi langsame Schpiel", "always": "Immer", "never": "Nie", "xCompetesInY": "{param1} nimmt bi {param2} teil", @@ -897,7 +975,7 @@ "whiteCastlingKingside": "Wiss 0-0", "blackCastlingKingside": "Schwarz 0-0", "tpTimeSpentPlaying": "Gsamti-Schpillzit: {param}", - "watchGames": "Partie zueluege", + "watchGames": "Schpiel beobachte", "tpTimeSpentOnTV": "Gsamtzit uf Lichess TV: {param}", "watch": "Zueluege", "videoLibrary": "Video-Bibliothek", @@ -922,10 +1000,10 @@ "noSimulExplanation": "Das Simultanschach exischtiert nöd.", "returnToSimulHomepage": "Zrugg zur Simultanschach Startsite", "aboutSimul": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner.", - "aboutSimulImage": "Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft; nur 1 Partie hät er verlore.", - "aboutSimulRealLife": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Partie fertig gschpillt sind.", + "aboutSimulImage": "Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft - nu 1 Schpiel hät er verlore.", + "aboutSimulRealLife": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Schpiel fertig gschpillt sind.", "aboutSimulRules": "Wie bi reale Simultanschach Veraschtaltige, gaht \nde Simultanschpiller vu eim Simultangägner zum \nNächschte und macht bi jedem Brätt 1 Zug.", - "aboutSimulSettings": "Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch immer ungwertet.", + "aboutSimulSettings": "Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch nie g'wertet.", "create": "Erschtelle", "whenCreateSimul": "Wänn du dir es Simultanschach machsch, chasch du glichzitig gäge mehreri Gägner schpille.", "simulVariantsHint": "Wänn du mehreri Variante wählsch, chann jede Simultangägner ussueche, was er schpille will.", @@ -1065,7 +1143,7 @@ "seeBestMove": "Zeig de bescht Zug", "hideBestMove": "Verschteck de bescht Zug", "getAHint": "Lass dir en Tipp geh", - "evaluatingYourMove": "Din Zug wird gwertet ...", + "evaluatingYourMove": "Din Zug wird g'wertet ...", "whiteWinsGame": "Wiss günnt", "blackWinsGame": "Schwarz günnt", "learnFromYourMistakes": "Lern us dine Fähler", @@ -1106,7 +1184,7 @@ "goodPractice": "Zum dem Zwäck müend mir sicher si, dass sich all Schpiller korräkt verhalted.", "potentialProblem": "Wänn es möglichs Problem entdeckt wird, zeiged mir die Mäldig.", "howToAvoidThis": "Wie chasch das verhindere?", - "playEveryGame": "Schpill jedi Partie, wo du afangsch, au fertig.", + "playEveryGame": "Schpill jedes Schpiel, wo du afangsch, au fertig.", "tryToWin": "Probier jedes Schpil z'günne (oder mindeschtens es Remis z'erreiche).", "resignLostGames": "Gib es verlores Schpiel uf (lass nöd eifach d'Uhr ablaufe).", "temporaryInconvenience": "Mir entschuldiged eus für die vorübergehende Unannehmlichkeite,", @@ -1129,7 +1207,7 @@ "blitzDesc": "Schnälli Schpiel: 3 bis 8 Minute", "rapidDesc": "Schnällschach: 8 bis 25 Minute", "classicalDesc": "Klassischi Schpiel: 25 Minute und meh", - "correspondenceDesc": "Fernschach Partie: 1 oder mehreri Täg pro Zug", + "correspondenceDesc": "Korrespondänz-Schpiel: 1 oder mehreri Täg pro Zug", "puzzleDesc": "Schach-Taktik Trainer", "important": "Wichtig", "yourQuestionMayHaveBeenAnswered": "Dini Frag chönnt bereits {param1} beantwortet worde si", @@ -1143,7 +1221,7 @@ "thisTopicIsArchived": "Das Thema isch archiviert und chann nüme beantwortet werde.", "joinTheTeamXToPost": "Tritt {param1} bi, um i dem Forum Biträg z'schribe", "teamNamedX": "{param1} Team", - "youCannotPostYetPlaySomeGames": "Du chasch nonig im Forum schribe: Schpill zerscht no es paar Partie!", + "youCannotPostYetPlaySomeGames": "Du chasch no nüt im Forum schribe: Schpill vorher es paar Schpiel!", "subscribe": "Abonniere", "unsubscribe": "Abmälde", "mentionedYouInX": "hät dich in \"{param1}\" erwähnt.", @@ -1154,7 +1232,7 @@ "youHaveJoinedTeamX": "Du bisch \"{param1}\" biträte.", "someoneYouReportedWasBanned": "Öpper wo du gmäldet häsch, isch bannt worde", "congratsYouWon": "Gratuliere, du häsch gunne!", - "gameVsX": "Partie gäge {param1}", + "gameVsX": "Schpiel gäge {param1}", "resVsX": "{param1} gäge {param2}", "lostAgainstTOSViolator": "Du häsch Wertigspünkt verlore, gäge öpper, wo d'Lichess-Regle verletzt hät", "refundXpointsTimeControlY": "Rückerschtattig: {param1} {param2} Wertigspünkt.", @@ -1166,15 +1244,15 @@ "cancelTournament": "Brich das Turnier ab", "tournDescription": "Turnier Beschribig", "tournDescriptionHelp": "Wottsch de Teilnehmer öppis Speziells mitteile? Probier dich churz z'fasse. Url mit Name sind möglich: [name](https://url)", - "ratedFormHelp": "Alli Partie sind gwertet\nund beiflussed dini Wertig", + "ratedFormHelp": "Alli Schpiel sind g'wertet\nund beiflussed dini Wertig", "onlyMembersOfTeam": "Nur Mitglider vum Team", "noRestriction": "Kei Ischränkige", - "minimumRatedGames": "Minimum gwerteti Partie", + "minimumRatedGames": "Minimum g'werteti Schpiel", "minimumRating": "Minimali Wertig", "maximumWeeklyRating": "Maximali wüchentlichi Wertig", "positionInputHelp": "E gültigi FEN schtartet jedes Schpiel ab ere beschtimmte Schtellig. \nDas funktioniert nur für Schtandardschpiel und nöd für Variante!\nDu chasch mit {param} so e FEN-Schtellig generiere und \nsie dänn im Fäld \"Afangsposition\" ifüege oder das \nFäld leer lah und all Schpiel normal schtarte.", "cancelSimul": "Simultanschach abbräche", - "simulHostcolor": "Farb vom Simultanschpiller, für jedi Partie", + "simulHostcolor": "Farb vom Simultanschpiller, für jedes Schpiel", "estimatedStart": "Vorussichtlichi Schtartzit", "simulFeatured": "Uf {param} zeige", "simulFeaturedHelp": "Zeig allne uf {param} dis Simultanschach. Privats Simultanschach deaktiviert.", @@ -1203,13 +1281,13 @@ "sentEmailWithLink": "Mir hän dir e E-Mail mit eme Link gschickt.", "tournamentEntryCode": "Turnier-Bitrittscode", "hangOn": "En Momänt!", - "gameInProgress": "Du häsch no e laufedi Partie mit {param}.", - "abortTheGame": "Partie abbräche", - "resignTheGame": "Partie ufgeh", - "youCantStartNewGame": "Du chasch kei neui Partie starte, bevor die no Laufendi nöd fertig gschpillt isch.", + "gameInProgress": "Du häsch no es laufeds Schpiel mit {param}.", + "abortTheGame": "Schpiel abbräche", + "resignTheGame": "Schpiel ufgeh", + "youCantStartNewGame": "Du chasch kei neus Schpiel schtarte, bevor s'Laufende nöd fertig isch.", "since": "Sit", "until": "Bis", - "lichessDbExplanation": "Gwerteti Schpiel vu allne Lichess Schpiller", + "lichessDbExplanation": "G'werteti Schpiel vu allne Lichess Schpiller", "switchSides": "Farb wächsle", "closingAccountWithdrawAppeal": "Wänn du dis Konto schlüssisch, ziehsch du au din Ischpruch zrugg", "ourEventTips": "Eusi Tipps, fürs Organisiere vu Events", @@ -1217,13 +1295,14 @@ "showMeEverything": "Zeig mer alles", "lichessPatronInfo": "Lichess isch e Wohltätigkeitsorganisation und e völlig choschtelosi/freii Open-Source-Software.\nAlli Betriebs-Choschte, d'Entwicklig und d'Inhält werded usschliesslich dur Benutzerschpände finanziert.", "nothingToSeeHere": "Da gits im monumäntan nüt z'gseh.", - "opponentLeftCounter": "{count, plural, =1{Din Gägner hät d'Partie verlah. Du chasch in {count} Sekunde din Sieg beaschpruche} other{Din Gägner hät d'Partie verlah, du chasch in {count} Sekunde din Sieg beaschpruche}}", + "stats": "Schtatistike", + "opponentLeftCounter": "{count, plural, =1{Din Gägner hät s'Schpiel verlah. Du chasch in {count} Sekunde de Sieg beaschpruche} other{Din Gägner hät d'Partie verlah, du chasch in {count} Sekunde din Sieg beaschpruche}}", "mateInXHalfMoves": "{count, plural, =1{Matt i {count} Halbzug} other{Matt i {count} Halbzüg}}", "nbBlunders": "{count, plural, =1{{count} Patzer} other{{count} Patzer}}", "nbMistakes": "{count, plural, =1{{count} Fähler} other{{count} Fähler}}", "nbInaccuracies": "{count, plural, =1{{count} Ungnauigkeit} other{{count} Ungnauigkeite}}", "nbPlayers": "{count, plural, =1{{count} Schpiller} other{{count} Schpiller}}", - "nbGames": "{count, plural, =1{{count} Partie} other{{count} Partie}}", + "nbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", "ratingXOverYGames": "{count, plural, =1{{count} Wertig vu {param2} Spiel} other{{count} Wertig vu {param2} Spiel}}", "nbBookmarks": "{count, plural, =1{{count} Läsezeiche} other{{count} Läsezeiche}}", "nbDays": "{count, plural, =1{{count} Tag} other{{count} Täg}}", @@ -1231,26 +1310,26 @@ "nbMinutes": "{count, plural, =1{{count} Minute} other{{count} Minute}}", "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{De Rang wird jedi Minute aktualisiert} other{De Rang wird alli {count} Minute aktualisiert}}", "nbPuzzles": "{count, plural, =1{{count} Ufgab} other{{count} Ufgabe}}", - "nbGamesWithYou": "{count, plural, =1{{count} Partie mit dir} other{{count} Partie mit dir}}", - "nbRated": "{count, plural, =1{{count} gwertet} other{{count} gwerteti}}", + "nbGamesWithYou": "{count, plural, =1{{count} Schpiel mit dir} other{{count} Schpiel mit dir}}", + "nbRated": "{count, plural, =1{{count} g'wertet} other{{count} gwerteti}}", "nbWins": "{count, plural, =1{{count} Sieg} other{{count} Sieg}}", "nbLosses": "{count, plural, =1{{count} Niederlag} other{{count} Niderlage}}", "nbDraws": "{count, plural, =1{{count} Remis} other{{count} Remis}}", - "nbPlaying": "{count, plural, =1{{count} laufendi Partie} other{{count} laufendi Partie}}", + "nbPlaying": "{count, plural, =1{{count} am Schpille} other{{count} am Schpille}}", "giveNbSeconds": "{count, plural, =1{Gib dim Gägner {count} Sekunde} other{Gib dim Gägner {count} Sekunde}}", "nbTournamentPoints": "{count, plural, =1{{count} Turnierpunkt} other{{count} Turnierpünkt}}", "nbStudies": "{count, plural, =1{{count} Schtudie} other{{count} Schtudie}}", "nbSimuls": "{count, plural, =1{{count} Simultan} other{{count} Simultan}}", - "moreThanNbRatedGames": "{count, plural, =1{≥ {count} gwertets Schpiel} other{≥ {count} gwerteti Schpiel}}", - "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} gwertets {param2} Schpiel} other{≥ {count} gwerteti {param2} Schpiel}}", - "needNbMorePerfGames": "{count, plural, =1{Du muesch no {count} gwerteti Partie meh {param2} schpille} other{Du muesch no {count} gwerteti Partie meh {param2} schpille}}", + "moreThanNbRatedGames": "{count, plural, =1{≥ {count} g'wertets Schpiel} other{≥ {count} g'werteti Schpiel}}", + "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} g'wertets {param2} Schpiel} other{≥ {count} g'werteti {param2} Schpiel}}", + "needNbMorePerfGames": "{count, plural, =1{Du muesch no {count} g'wertets Schpiel meh {param2} schpille} other{Du muesch no {count} g'werteti Schpiel meh {param2} schpille}}", "needNbMoreGames": "{count, plural, =1{Du bruchsch no {count} Wertigs-Schpiel meh} other{Du bruchsch no {count} Wertigs-Schpiel meh}}", - "nbImportedGames": "{count, plural, =1{{count} importierti Partie} other{{count} importierti Partie}}", + "nbImportedGames": "{count, plural, =1{{count} importierts Schpiel} other{{count} importierti Schpiel}}", "nbFriendsOnline": "{count, plural, =1{{count} Fründ online} other{{count} Fründe online}}", "nbFollowers": "{count, plural, =1{{count} Follower} other{{count} Follower}}", "nbFollowing": "{count, plural, =1{{count} folgänd} other{{count} folgänd}}", "lessThanNbMinutes": "{count, plural, =1{Weniger als {count} Minute} other{Weniger als {count} Minute}}", - "nbGamesInPlay": "{count, plural, =1{{count} laufendi Partie} other{{count} laufendi Partie}}", + "nbGamesInPlay": "{count, plural, =1{{count} laufends Schpiel} other{{count} laufendi Schpiel}}", "maximumNbCharacters": "{count, plural, =1{Höchschtens {count} Buechstabe.} other{Höchschtens {count} Buechstabe.}}", "blocks": "{count, plural, =1{{count} blockt} other{{count} Blockti}}", "nbForumPosts": "{count, plural, =1{{count} Forum-Bitrag} other{{count} Forum-Biträg}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 Lauf} other{{count} Läuf}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Het en Lauf vo {param2} gschpillt} other{Hät {count} Läuf vu {param2} gschpillt}}", "streamerLichessStreamers": "Lichess Streamer/-in", + "studyPrivate": "Privat", + "studyMyStudies": "Mini Schtudie", + "studyStudiesIContributeTo": "Schtudie, wo ich mitwürke", + "studyMyPublicStudies": "Mini öffentliche Schtudie", + "studyMyPrivateStudies": "Mini private Schtudie", + "studyMyFavoriteStudies": "Mini liebschte Schtudie", + "studyWhatAreStudies": "Was sind Schtudie?", + "studyAllStudies": "All Schtudie", + "studyStudiesCreatedByX": "Vu {param} erschtellte Schtudie", + "studyNoneYet": "No kei.", + "studyHot": "agseit", + "studyDateAddedNewest": "wänn zuegfüegt (neui)", + "studyDateAddedOldest": "wänn zuegfüegt (älti)", + "studyRecentlyUpdated": "frisch aktualisiert", + "studyMostPopular": "beliebtschti", + "studyAlphabetical": "alphabetisch", + "studyAddNewChapter": "Neus Kapitel zuefüege", + "studyAddMembers": "Mitglider zuefüege", + "studyInviteToTheStudy": "Lad zu de Schtudie i", + "studyPleaseOnlyInvitePeopleYouKnow": "Bitte lad nur Lüt i, wo du kännsch und wo wänd aktiv a dere Schtudie teilneh.", + "studySearchByUsername": "Suech noch Benutzername", + "studySpectator": "Zueschauer", + "studyContributor": "Mitwürkende", + "studyKick": "Userüere", + "studyLeaveTheStudy": "Schtudie verlah", + "studyYouAreNowAContributor": "Du bisch jetzt en Mitwürkende", + "studyYouAreNowASpectator": "Du bisch jetzt Zueschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gfallt mir", + "studyUnlike": "Gfallt mir nümme", + "studyNewTag": "Neue Tag", + "studyCommentThisPosition": "Kommentier die Schtellig", + "studyCommentThisMove": "Kommentier de Zug", + "studyAnnotateWithGlyphs": "Mit Symbol kommentiere", + "studyTheChapterIsTooShortToBeAnalysed": "Das Kapitel isch z'churz zum analysiere.", + "studyOnlyContributorsCanRequestAnalysis": "Nur wer a dere Schtudie mit macht, chann e Coputeranalyse afordere.", + "studyGetAFullComputerAnalysis": "Erhalt e vollschtändigi, serversitigi Computeranalyse vu de Hauptvariante.", + "studyMakeSureTheChapterIsComplete": "Schtell sicher, dass das Kapitel vollschtändig isch. Die Analyse chann nur eimal agforderet werde.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alli synchronisierte Mitglider gsehnd die glich Schtellig", + "studyShareChanges": "Teil Änderige mit de Zueschauer und speicher sie uf em Server", + "studyPlaying": "Laufend", + "studyShowEvalBar": "Bewertigs-Skala", + "studyFirst": "zur erschte Site", + "studyPrevious": "zrugg", + "studyNext": "nächschti", + "studyLast": "zur letschte Site", "studyShareAndExport": "Teile & exportiere", - "studyStart": "Schtart" + "studyCloneStudy": "Klone", + "studyStudyPgn": "Schtudie PGN", + "studyDownloadAllGames": "All Schpiel abelade", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopiere", + "studyDownloadGame": "Das Schpiel abelade", + "studyStudyUrl": "Schtudie URL", + "studyCurrentChapterUrl": "URL aktuells Kapitel", + "studyYouCanPasteThisInTheForumToEmbed": "Du chasch das, zum ibinde, im Forum oder i dim Liches Tagebuech ifüege", + "studyStartAtInitialPosition": "Fang ab de Grundschtellig a", + "studyStartAtX": "Fang mit {param} a", + "studyEmbedInYourWebsite": "I dini Website ibinde", + "studyReadMoreAboutEmbedding": "Lies meh über s'Ibinde", + "studyOnlyPublicStudiesCanBeEmbedded": "Mer chann nur öffentlichi Schtudie ibinde!", + "studyOpen": "Öffne", + "studyXBroughtToYouByY": "{param1} präsentiert vu {param2}", + "studyStudyNotFound": "Schtudie nöd gfunde", + "studyEditChapter": "Kapitel bearbeite", + "studyNewChapter": "Neus Kapitel", + "studyImportFromChapterX": "Importiers us {param}", + "studyOrientation": "Orientierig", + "studyAnalysisMode": "Analyse Modus", + "studyPinnedChapterComment": "Aghänkte Kapitel Kommentar", + "studySaveChapter": "Kapitel schpeichere", + "studyClearAnnotations": "Amerkige lösche", + "studyClearVariations": "Variante lösche", + "studyDeleteChapter": "Kapitel lösche", + "studyDeleteThisChapter": "Kapitel lösche? Das chann nöd rückgängig gmacht werde!", + "studyClearAllCommentsInThisChapter": "Alli Kommentär, Symbol und Zeichnigsforme i dem Kapitel lösche?", + "studyRightUnderTheBoard": "Diräkt underhalb vom Brätt", + "studyNoPinnedComment": "Kei", + "studyNormalAnalysis": "Normali Analyse", + "studyHideNextMoves": "Nögschti Züg uusbländä", + "studyInteractiveLesson": "Interaktivi Üäbig", + "studyChapterX": "Kapitäl {param}", + "studyEmpty": "Läär", + "studyStartFromInitialPosition": "Fang vu de Usgangsschtellig a", + "studyEditor": "Ändärä", + "studyStartFromCustomPosition": "Fang vunere benutzerdefinierte Schtellig a", + "studyLoadAGameByUrl": "Lad es Schpiel mit ere URL", + "studyLoadAPositionFromFen": "Lad e Schtellig mit ere FEN", + "studyLoadAGameFromPgn": "Lad Schpiel mit eme PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL vu de Schpiel", + "studyLoadAGameFromXOrY": "Lad es Schpiel vo {param1} oder {param2}", + "studyCreateChapter": "Kapitäl ärschtelä", + "studyCreateStudy": "Schtudie erschtelle", + "studyEditStudy": "Schtudie bearbeite", + "studyVisibility": "Sichtbarkeit", + "studyPublic": "Öffentlich", + "studyUnlisted": "Unglischtet", + "studyInviteOnly": "Nur mit Iladig", + "studyAllowCloning": "Chlone erlaube", + "studyNobody": "Niemer", + "studyOnlyMe": "Nur ich", + "studyContributors": "Mitwirkändi", + "studyMembers": "Mitglider", + "studyEveryone": "Alli", + "studyEnableSync": "Sync aktiviärä", + "studyYesKeepEveryoneOnTheSamePosition": "Jawoll: Glichi Schtellig für alli", + "studyNoLetPeopleBrowseFreely": "Nei: Unabhängigi Navigation für alli", + "studyPinnedStudyComment": "Agheftete Schtudiekommentar", + "studyStart": "Schtart", + "studySave": "Schpeichärä", + "studyClearChat": "Tschätt löschä", + "studyDeleteTheStudyChatHistory": "Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!", + "studyDeleteStudy": "Schtudie lösche", + "studyConfirmDeleteStudy": "Die ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: {param}", + "studyWhereDoYouWantToStudyThat": "Welli Schtudie wottsch bruche?", + "studyGoodMove": "Guete Zug", + "studyMistake": "Fähler", + "studyBrilliantMove": "Briliantä Zug", + "studyBlunder": "Grobä Patzer", + "studyInterestingMove": "Intressantä Zug", + "studyDubiousMove": "Frogwürdigä Zug", + "studyOnlyMove": "Einzigä Zug", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Usglicheni Schtellig", + "studyUnclearPosition": "Unklari Schtellig", + "studyWhiteIsSlightlyBetter": "Wiss schtaht es bitzli besser", + "studyBlackIsSlightlyBetter": "Schwarz schtaht es bitzli besser", + "studyWhiteIsBetter": "Wiss schtaht besser", + "studyBlackIsBetter": "Schwarz schtoht besser", + "studyWhiteIsWinning": "Wiss schtaht uf Gwünn", + "studyBlackIsWinning": "Schwarz schtoht uf Gwünn", + "studyNovelty": "Neuerig", + "studyDevelopment": "Entwicklig", + "studyInitiative": "Initiativä", + "studyAttack": "Agriff", + "studyCounterplay": "Gägeschpiel", + "studyTimeTrouble": "Zitnot", + "studyWithCompensation": "Mit Kompänsation", + "studyWithTheIdea": "Mit dä Idee", + "studyNextChapter": "Nögschts Kapitäl", + "studyPrevChapter": "Vorhärigs Kapitäl", + "studyStudyActions": "Lärnaktionä", + "studyTopics": "Theme", + "studyMyTopics": "Mini Theme", + "studyPopularTopics": "Beliebti Theme", + "studyManageTopics": "Theme verwalte", + "studyBack": "Zrugg", + "studyPlayAgain": "Vo vornä", + "studyWhatWouldYouPlay": "Was würdisch du ih derä Stellig spiele?", + "studyYouCompletedThisLesson": "Gratulation! Du häsch die Lektion abgschlosse.", + "studyPerPage": "{param} pro Site", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitäl}}", + "studyNbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", + "studyNbMembers": "{count, plural, =1{{count} Mitglid} other{{count} Mitglider}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füeg din PGN Tegscht da i, bis zu {count} Schpiel} other{Füeg din PGN Tegscht da i, bis zu {count} Schpiel}}", + "timeagoJustNow": "grad jetzt", + "timeagoRightNow": "genau jetzt", + "timeagoCompleted": "beändet", + "timeagoInNbSeconds": "{count, plural, =1{i {count} Sekunde} other{i {count} Sekunde}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} Minute} other{in {count} Minute}}", + "timeagoInNbHours": "{count, plural, =1{i {count} Schtund} other{i {count} Schtunde}}", + "timeagoInNbDays": "{count, plural, =1{i {count} Tag} other{i {count} Täg}}", + "timeagoInNbWeeks": "{count, plural, =1{i {count} Wuche} other{i {count} Wuche}}", + "timeagoInNbMonths": "{count, plural, =1{i {count} Monet} other{i {count} Mönet}}", + "timeagoInNbYears": "{count, plural, =1{i {count} Jahr} other{i {count} Jahr}}", + "timeagoNbMinutesAgo": "{count, plural, =1{vor {count} Minute} other{vor {count} Minute}}", + "timeagoNbHoursAgo": "{count, plural, =1{vor {count} Schtund} other{vor {count} Schtunde}}", + "timeagoNbDaysAgo": "{count, plural, =1{vor {count} Tag} other{vor {count} Täg}}", + "timeagoNbWeeksAgo": "{count, plural, =1{vor {count} Wuche} other{vor {count} Wuche}}", + "timeagoNbMonthsAgo": "{count, plural, =1{vor {count} Monet} other{vor {count} Mönet}}", + "timeagoNbYearsAgo": "{count, plural, =1{vor {count} Jahr} other{vor {count} Jahr}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minute blibt} other{{count} Minute blibed}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Schtund blibt} other{{count} Schtunde blibed}}" } \ No newline at end of file diff --git a/lib/l10n/lila_he.arb b/lib/l10n/lila_he.arb index 3b559ed481..7463e7342a 100644 --- a/lib/l10n/lila_he.arb +++ b/lib/l10n/lila_he.arb @@ -1,52 +1,53 @@ { + "mobileAllGames": "כל המשחקים", + "mobileAreYouSure": "בטוח?", + "mobileBlindfoldMode": "משחק עיוור", + "mobileCancelTakebackOffer": "ביטול ההצעה להחזיר את המהלך האחרון", + "mobileClearButton": "ניקוי", + "mobileCorrespondenceClearSavedMove": "ניקוי המהלך השמור", + "mobileCustomGameJoinAGame": "הצטרפות למשחק", + "mobileFeedbackButton": "משוב", + "mobileGreeting": "שלום, {param}", + "mobileGreetingWithoutName": "שלום", + "mobileHideVariation": "הסתרת וריאציות", "mobileHomeTab": "בית", - "mobilePuzzlesTab": "חידות", - "mobileToolsTab": "כלים", - "mobileWatchTab": "צפייה", - "mobileSettingsTab": "הגדרות", + "mobileLiveStreamers": "שדרנים בשידור חי", "mobileMustBeLoggedIn": "יש להתחבר כדי לצפות בדף זה.", - "mobileSystemColors": "צבעי מערכת ההפעלה", - "mobileFeedbackButton": "משוב", + "mobileNoSearchResults": "אין תוצאות", + "mobileNotFollowingAnyUser": "אינכם עוקבים אחר אף אחד.", "mobileOkButton": "בסדר", + "mobilePlayersMatchingSearchTerm": "שחקנים עם ״{param}״", + "mobilePrefMagnifyDraggedPiece": "הגדלת הכלי הנגרר", + "mobilePuzzleStormConfirmEndRun": "האם לסיים את הסבב?", + "mobilePuzzleStormFilterNothingToShow": "אין מה להראות. ניתן לשנות את חתכי הסינון", + "mobilePuzzleStormNothingToShow": "אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.", + "mobilePuzzleStormSubtitle": "פתרו כמה שיותר חידות ב־3 דקות.", + "mobilePuzzleStreakAbortWarning": "הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.", + "mobilePuzzleThemesSubtitle": "פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.", + "mobilePuzzlesTab": "חידות", + "mobileRecentSearches": "חיפושים אחרונים", "mobileSettingsHapticFeedback": "רטט בכל מהלך", "mobileSettingsImmersiveMode": "מצב ריכוז", "mobileSettingsImmersiveModeSubtitle": "הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.", - "mobileNotFollowingAnyUser": "אינכם עוקבים אחר אף אחד.", - "mobileAllGames": "כל המשחקים", - "mobileRecentSearches": "חיפושים אחרונים", - "mobileClearButton": "ניקוי", - "mobilePlayersMatchingSearchTerm": "שחקנים עם ״{param}״", - "mobileNoSearchResults": "אין תוצאות", - "mobileAreYouSure": "בטוח?", - "mobilePuzzleStreakAbortWarning": "הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.", - "mobilePuzzleStormNothingToShow": "אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.", - "mobileSharePuzzle": "שיתוף החידה", - "mobileShareGameURL": "שיתוף הקישור למשחק", + "mobileSettingsTab": "הגדרות", "mobileShareGamePGN": "שיתוף ה־PGN", + "mobileShareGameURL": "שיתוף הקישור למשחק", "mobileSharePositionAsFEN": "שיתוף העמדה כ־FEN", - "mobileShowVariations": "הצגת וריאציות", - "mobileHideVariation": "הסתרת וריאציות", + "mobileSharePuzzle": "שיתוף החידה", "mobileShowComments": "הצגת הערות", - "mobilePuzzleStormConfirmEndRun": "האם לסיים את הסבב?", - "mobilePuzzleStormFilterNothingToShow": "אין מה להראות. ניתן לשנות את חתכי הסינון", - "mobileCancelTakebackOffer": "ביטול ההצעה להחזיר את המהלך האחרון", - "mobileCancelDrawOffer": "ביטול הצעת התיקו", - "mobileWaitingForOpponentToJoin": "ממתין שיריב יצטרף...", - "mobileBlindfoldMode": "משחק עיוור", - "mobileLiveStreamers": "שדרנים בשידור חי", - "mobileCustomGameJoinAGame": "הצטרפות למשחק", - "mobileCorrespondenceClearSavedMove": "ניקוי המהלך השמור", - "mobileSomethingWentWrong": "משהו השתבש.", "mobileShowResult": "הצגת תוצאת המשחק", - "mobilePuzzleThemesSubtitle": "פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.", - "mobilePuzzleStormSubtitle": "פתרו כמה שיותר חידות ב־3 דקות.", - "mobileGreeting": "שלום, {param}", - "mobileGreetingWithoutName": "שלום", + "mobileShowVariations": "הצגת וריאציות", + "mobileSomethingWentWrong": "משהו השתבש.", + "mobileSystemColors": "צבעי מערכת ההפעלה", + "mobileTheme": "עיצוב", + "mobileToolsTab": "כלים", + "mobileWaitingForOpponentToJoin": "ממתין שיריב יצטרף...", + "mobileWatchTab": "צפייה", "activityActivity": "פעילות", - "activityHostedALiveStream": "עלה (או עלתה) לשידור חי", + "activityHostedALiveStream": "על\\תה לשידור חי", "activityRankedInSwissTournament": "סיים/ה במקום {param1} ב־{param2}", "activitySignedUp": "נרשם/ה לlichess.org", - "activitySupportedNbMonths": "{count, plural, =1{תמכ/ה בליצ'ס במשך חודש {count} כ{param2}} =2{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} many{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} other{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}}}", + "activitySupportedNbMonths": "{count, plural, =1{תמכ/ה ב-lichess במשך חודש {count} כ{param2}} =2{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} many{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} other{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}}}", "activityPracticedNbPositions": "{count, plural, =1{התאמן/ה על עמדה {count} ב{param2}} =2{התאמן/ה על {count} עמדות ב{param2}} many{התאמן/ה על {count} עמדות ב{param2}} other{התאמן/ה על {count} עמדות ב{param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{פתר/ה חידה טקטית {count}} =2{פתר/ה {count} חידות טקטיות} many{פתר/ה {count} חידות טקטיות} other{פתר/ה {count} חידות טקטיות}}", "activityPlayedNbGames": "{count, plural, =1{שיחק/ה משחק {param2} {count}} =2{שיחק/ה {count} משחקי {param2}} many{שיחק/ה {count} משחקי {param2}} other{שיחק/ה {count} משחקי {param2}}}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{שיחק/ה מהלך {count}} =2{שיחק/ה {count} מהלכים} many{שיחק/ה {count} מהלכים} other{שיחק/ה {count} מהלכים}}", "activityInNbCorrespondenceGames": "{count, plural, =1{במשחק {count} בהתכתבות} =2{ב{count} משחקים בהתכתבות} many{ב{count} משחקים בהתכתבות} other{ב{count} משחקים בהתכתבות}}", "activityCompletedNbGames": "{count, plural, =1{השלים/ה משחק התכתבות {count}} =2{השלים/ה {count} משחקי התכתבות} many{השלים/ה {count} משחקי התכתבות} other{השלים/ה {count} משחקי התכתבות}}", + "activityCompletedNbVariantGames": "{count, plural, =1{השלים/ה משחק התכתבות {count} מסוג {param2}} =2{השלים/ה {count} משחקי התכתבות מסוג {param2}} many{השלים/ה {count} משחקי התכתבות מסוג {param2}} other{השלים/ה {count} משחקי התכתבות מסוג {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{התחיל/ה לעקוב אחר שחקן {count}} =2{התחיל/ה לעקוב אחר {count} שחקנים} many{התחיל/ה לעקוב אחר {count} שחקנים} other{התחיל/ה לעקוב אחר {count} שחקנים}}", "activityGainedNbFollowers": "{count, plural, =1{השיג/ה עוקב/ת {count} חדש/ה} =2{השיג/ה {count} עוקבים חדשים} many{השיג/ה {count} עוקבים חדשים} other{השיג/ה {count} עוקבים חדשים}}", "activityHostedNbSimuls": "{count, plural, =1{אירח/ה משחק סימולטני {count}} =2{אירח/ה {count} משחקים סימולטניים} many{אירח/ה {count} משחקים סימולטניים} other{אירח/ה {count} משחקים סימולטניים}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{השתתף/ה בטורניר שוויצרי {count}} =2{השתתף/ה ב־{count} טורנירים שוויצריים} many{השתתף/ה ב־{count} טורנירים שוויצריים} other{השתתף/ה ב־{count} טורנירים שוויצריים}}", "activityJoinedNbTeams": "{count, plural, =1{הצטרף/ה לקבוצה {count}} =2{הצטרף/ה ל־{count} קבוצות} many{הצטרף/ה ל־{count} קבוצות} other{הצטרף/ה ל־{count} קבוצות}}", "broadcastBroadcasts": "הקרנות", + "broadcastMyBroadcasts": "ההקרנות שלי", "broadcastLiveBroadcasts": "צפייה ישירה בטורנירים", + "broadcastBroadcastCalendar": "לוח השידורים", + "broadcastNewBroadcast": "הקרנה ישירה חדשה", + "broadcastSubscribedBroadcasts": "הקרנות שנרשמת אליהן", + "broadcastAboutBroadcasts": "הסבר על הקרנות", + "broadcastHowToUseLichessBroadcasts": "איך להשתמש בהקרנות ב־Lichess.", + "broadcastTheNewRoundHelp": "הסבב החדש יכלול את אותם התורמים והחברים כמו בסבב הקודם.", + "broadcastAddRound": "הוספת סבב", + "broadcastOngoing": "כרגע", + "broadcastUpcoming": "בקרוב", + "broadcastCompleted": "שהושלמו", + "broadcastCompletedHelp": "Lichess מאתר מתי הושלם הסבב על פי המשחקים שבקישור למהלכים בשידור חי (המקור). הפעילו את האפשרות הזאת אם אין מקור שממנו נשאבים המשחקים.", + "broadcastRoundName": "שם סבב", + "broadcastRoundNumber": "מספר סבב", + "broadcastTournamentName": "שם הטורניר", + "broadcastTournamentDescription": "תיאור הטורניר בקצרה", + "broadcastFullDescription": "תיאור מלא של הטורניר", + "broadcastFullDescriptionHelp": "תיאור מפורט של הטורניר (אופציונאלי). {param1} זמין. אורך התיאור לא יעלה על {param2} תווים.", + "broadcastSourceSingleUrl": "קישור המקור של ה־PGN", + "broadcastSourceUrlHelp": "הקישור ש־Lichess יבדוק כדי לקלוט עדכונים ב־PGN. הוא חייב להיות פומבי ונגיש דרך האינטרנט.", + "broadcastSourceGameIds": "עד 64 מזהי משחק של Lichess, מופרדים ברווחים.", + "broadcastStartDateTimeZone": "שעת ההתחלה באזור הזמן המקומי של הטורניר: {param}", + "broadcastStartDateHelp": "אופציונאלי, אם את/ה יודע/ת מתי האירוע צפוי להתחיל", + "broadcastCurrentGameUrl": "הקישור למשחק הנוכחי", + "broadcastDownloadAllRounds": "הורדת כל הסבבים", + "broadcastResetRound": "אפס את הסיבוב הזה", + "broadcastDeleteRound": "מחיקת הסבב הזה", + "broadcastDefinitivelyDeleteRound": "מחיקת הסבב הזה והמשחקים שבו לצמיתות", + "broadcastDeleteAllGamesOfThisRound": "מחיקת כל המשחקים בסבב הזה. כדי ליצור אותם מחדש, קישור המקור צריך להיות פעיל.", + "broadcastEditRoundStudy": "עריכת לוח הלמידה של הסבב", + "broadcastDeleteTournament": "מחיקת הטורניר הזה", + "broadcastDefinitivelyDeleteTournament": "מחיקה לצמיתות של הטורניר הזה, על כל סבביו והמשחקים שבו.", + "broadcastShowScores": "הצגת הניקוד של השחקנים בהתבסס על תוצאות המשחקים", + "broadcastReplacePlayerTags": "אופציונאלי: החלפה של שמות השחקנים, דירוגיהם ותאריהם", + "broadcastFideFederations": "איגודי FIDE", + "broadcastTop10Rating": "דירוג עשרת המובילים", + "broadcastFidePlayers": "שחקני FIDE", + "broadcastFidePlayerNotFound": "לא נמצא שחקן FIDE", + "broadcastFideProfile": "פרופיל FIDE", + "broadcastFederation": "איגוד", + "broadcastAgeThisYear": "גיל השנה", + "broadcastUnrated": "לא מדורג", + "broadcastRecentTournaments": "טורנירים אחרונים", + "broadcastOpenLichess": "פתיחה ב־Lichess", + "broadcastTeams": "קבוצות", + "broadcastBoards": "לוחות", + "broadcastOverview": "מידע כללי", + "broadcastSubscribeTitle": "הירשמו כדי לקבל התראה בתחילת כל סבב. ניתן להפעיל או לבטל התראות קופצות או התראות ״פעמון״ בהגדרות החשבון שלך.", + "broadcastUploadImage": "העלאת תמונה עבור הטורניר", + "broadcastNoBoardsYet": "אין עדיין לוחות. הם יופיעו כשיעלו המשחקים.", + "broadcastBoardsCanBeLoaded": "ניתן להעלות לוחות באמצעות קישור מקור או דרך {param}", + "broadcastStartsAfter": "מתחיל אחרי {param}", + "broadcastStartVerySoon": "ההקרנה תחל ממש בקרוב.", + "broadcastNotYetStarted": "ההקרנה טרם החלה.", + "broadcastOfficialWebsite": "האתר הרשמי", + "broadcastStandings": "תוצאות", + "broadcastOfficialStandings": "טבלת מובילים רשמית", + "broadcastIframeHelp": "ישנן אפשרויות נוספות ב{param}", + "broadcastWebmastersPage": "עמוד המתכנתים", + "broadcastPgnSourceHelp": "קישור ל־PGN פומבי המתעדכן בשידור חי. אנו מציעים גם {param} לסנכרון מיטבי ומהיר.", + "broadcastEmbedThisBroadcast": "הטמעת ההקרנה באתר האינטרנט שלך", + "broadcastEmbedThisRound": "הטמעת {param} באתר האינטרנט שלך", + "broadcastRatingDiff": "הפרש הדירוג", + "broadcastGamesThisTournament": "משחקים בטורניר זה", + "broadcastScore": "ניקוד", + "broadcastAllTeams": "כל הקבוצות", + "broadcastTournamentFormat": "שיטת הטורניר", + "broadcastTournamentLocation": "מיקום הטורניר", + "broadcastTopPlayers": "שחקני צמרת", + "broadcastTimezone": "אזור זמן", + "broadcastFideRatingCategory": "קטגוריית דירוג FIDE", + "broadcastOptionalDetails": "פרטים אופציונאליים", + "broadcastPastBroadcasts": "הקרנות עבר", + "broadcastAllBroadcastsByMonth": "צפו בכל ההקרנות לפי חודש", + "broadcastNbBroadcasts": "{count, plural, =1{הקרנה {count}} =2{{count} הקרנות} many{{count} הקרנות} other{{count} הקרנות}}", "challengeChallengesX": "הזמנות למשחק: {param1}", "challengeChallengeToPlay": "הזמינו למשחק", "challengeChallengeDeclined": "ההזמנה למשחק נדחתה", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "דפדפן", "preferencesNotifyDevice": "מכשיר", "preferencesBellNotificationSound": "השמע צליל עבור התראות פעמון", + "preferencesBlindfold": "משחק עיוור", "puzzlePuzzles": "פאזלים", "puzzlePuzzleThemes": "חידות נושאיות", "puzzleRecommended": "מומלץ", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "כלי המאיים או מגן על משבצת דרך כלי יריב.", "puzzleThemeZugzwang": "כפאי", "puzzleThemeZugzwangDescription": "היריב מוגבל במסעים שביכולתו לבצע, וכל אחד מחמיר את מצבו.", - "puzzleThemeHealthyMix": "שילוב בריא", - "puzzleThemeHealthyMixDescription": "קצת מהכל. לא תדעו למה לצפות. עליכם להיות מוכנים להכל! בדיוק כמו משחקים אמיתיים.", + "puzzleThemeMix": "שילוב בריא", + "puzzleThemeMixDescription": "קצת מהכול. לא תדעו למה לצפות. עליכם להיות מוכנים להכול! בדיוק כמו במשחקים אמיתיים.", "puzzleThemePlayerGames": "המשחקים שלי", "puzzleThemePlayerGamesDescription": "חפשו חידות אשר נוצרו ממשחקים שלכם או של שחקנים אחרים.", "puzzleThemePuzzleDownloadInformation": "החידות האלו הן נחלת הכלל, וניתן להוריד אותן מ־{param}.", @@ -393,7 +471,7 @@ "settingsCloseAccount": "סגירת החשבון", "settingsManagedAccountCannotBeClosed": "חשבונך מנוהל, ולכן לא ניתן לסגור אותו.", "settingsClosingIsDefinitive": "הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?", - "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות ולהיפך.", + "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך.", "settingsChangedMindDoNotCloseAccount": "שיניתי את דעתי, אל תסגרו את החשבון שלי", "settingsCloseAccountExplanation": "האם אכן ברצונך לסגור את חשבונך? סגירת חשבונך היא החלטה סופית. לעולם לא יהיה אפשר להתחבר לחשבון הזה שוב.", "settingsThisAccountIsClosed": "החשבון הזה סגור.", @@ -505,7 +583,6 @@ "replayMode": "מצב הרצת המסעים", "realtimeReplay": "זמן אמת", "byCPL": "עפ\"י CPL", - "openStudy": "פתח לוח למידה", "enable": "הפעלה", "bestMoveArrow": "חץ המהלך הטוב ביותר", "showVariationArrows": "הצגת חצי ההמשכים האלטרנטיביים", @@ -515,7 +592,6 @@ "memory": "זיכרון", "infiniteAnalysis": "ניתוח אינסופי", "removesTheDepthLimit": "מסיר את מגבלת העומק ו\"מחמם\" את המחשב", - "engineManager": "מנהל המנועים", "blunder": "טעות גסה", "mistake": "שגיאה", "inaccuracy": "אי־דיוק", @@ -573,7 +649,7 @@ "error_namePassword": "נא לא להשתמש בשם המשתמש בתור הסיסמה.", "blankedPassword": "השתמשת בסיסמה שלך באתר אחר, ויתכן שהיא מועדת לפריצה. כדי להגן על חשבונך ב־Lichess, עליך להגדיר סיסמה חדשה. תודה על ההבנה.", "youAreLeavingLichess": "את/ה עוזב/ת את Lichess", - "neverTypeYourPassword": "לעולם אל תקלידו את סיסמתכם ב־Lichessבאף אתר אחר!", + "neverTypeYourPassword": "לעולם אל תקלידו את סיסמתכם ב־Lichess באף אתר אחר!", "proceedToX": "מעבר ל־{param}", "passwordSuggestion": "אל תשתמשו בסיסמה שהציע לכם אדם אחר. הוא ישתמש בה כדי לגנוב את חשבונכם!", "emailSuggestion": "אל תשמשו בכתובת מייל שהציע אדם אחר. הוא ישתמש בה כדי לגנוב את חשבונכם.", @@ -597,6 +673,7 @@ "rank": "מיקום", "rankX": "מיקום: {param}", "gamesPlayed": "משחקים בטורניר", + "ok": "אוקיי", "cancel": "ביטול", "whiteTimeOut": "נגמר הזמן ללבן", "blackTimeOut": "נגמר הזמן לשחור", @@ -645,7 +722,7 @@ "freeOnlineChess": "שחמט חינמי ברשת", "exportGames": "ייצוא משחקים", "ratingRange": "טווח דירוג", - "thisAccountViolatedTos": "החשבון הזה הפר את תנאי השימוש של ליצ'ס", + "thisAccountViolatedTos": "החשבון הזה הפר את תנאי השימוש של Lichess", "openingExplorerAndTablebase": "סייר הפתיחות וטבלאות סיומים", "takeback": "החזרת מהלך", "proposeATakeback": "הצע החזרת המהלך האחרון", @@ -713,7 +790,6 @@ "block": "חסמו", "blocked": "חסום", "unblock": "בטל חסימה", - "followsYou": "עוקב/ת אחריך", "xStartedFollowingY": "{param1} התחיל/ה לעקוב אחרי {param2}", "more": "עוד", "memberSince": "רשום/ה מ", @@ -766,7 +842,7 @@ "clearBoard": "ניקוי הלוח", "loadPosition": "טעינת עמדה", "isPrivate": "פרטי", - "reportXToModerators": "דווח/י על {param} למנהלים", + "reportXToModerators": "דווח על {param} למנהלים", "profileCompletion": "השלמת הפרופיל: {param}", "xRating": "דירוג {param}", "ifNoneLeaveEmpty": "אם אין, השאירו ריק", @@ -784,7 +860,7 @@ "inlineNotation": "תיאור מהלכים בשורה", "makeAStudy": "כדי לשמור ולשתף, תוכל/י ליצור לוח למידה.", "clearSavedMoves": "הסרת המהלכים", - "previouslyOnLichessTV": "לאחרונה בטלוויזיה של ליצ'ס", + "previouslyOnLichessTV": "לאחרונה בטלוויזיה של Lichess", "onlinePlayers": "שחקנים מחוברים", "activePlayers": "הכי פעילים", "bewareTheGameIsRatedButHasNoClock": "שימו לב! המשחק מדורג אך אין שעון.", @@ -812,14 +888,16 @@ "reply": "תגובה", "message": "הודעה", "createTheTopic": "צור אשכול", - "reportAUser": "דווח/י על משתמש/ת", - "user": "משתמש/ת", + "reportAUser": "דיווח על משתמש/ת", + "user": "משתמש", "reason": "סיבה", "whatIsIheMatter": "מה הבעיה?", "cheat": "רמאות", "troll": "הטרלה", "other": "אחר", - "reportDescriptionHelp": "הדביקו את הקישור למשחק(ים) והסבירו מה לא בסדר בהתנהגות המשתמש. אל תכתבו סתם ״השחקן/ית מרמה״, הסבירו לנו כיצד הגעתם למסקנה הזו. הדיווח יטופל מהר יותר אם ייכתב באנגלית.", + "reportCheatBoostHelp": "הדביקו את הקישור למשחקים שעליהם תרצו לדווח והסבירו מה הבעיה בהתנהגות המשתמש כפי שהיא משתקפת בהם. אל תכתבו סתם ״השחקן מרמה״. הסבירו לנו כיצד הגעתם למסקנה הזו.", + "reportUsernameHelp": "הסבירו מה פוגעני בשם המשתמש הזה. אל תכתבו סתם ״שם המשתמש פוגעני״. הסבירו לנו כיצד הגעתם למסקנה הזו, במיוחד אם ההעלבה מוסווית, בשפה זרה (שאינה אנגלית), בלשון סלנג או תלויית תרבות והיסטוריה.", + "reportProcessedFasterInEnglish": "הדיווח שלך יטופל מהר יותר אם ייכתב באנגלית.", "error_provideOneCheatedGameLink": "בבקשה לספק לפחות קישור אחד למשחק עם רמאות.", "by": "על־ידי {param}", "importedByX": "יובא ע\"י {param}", @@ -872,7 +950,7 @@ "side": "צד", "clock": "שעון", "opponent": "יריב", - "learnMenu": "למד/י", + "learnMenu": "למדו", "studyMenu": "לוחות למידה", "practice": "תרגול", "community": "קהילה", @@ -934,7 +1012,7 @@ "simulHostExtraTime": "זמן נוסף למארח/ת", "simulAddExtraTimePerPlayer": "הוספת זמן לשעון שלך בכל פעם שמצטרף/ת שחקן/ית למשחק הסימולטני.", "simulHostExtraTimePerPlayer": "זמן נוסף למארח/ת עבור כל שחקן/ית שמצטרף/ת", - "lichessTournaments": "טורנירים של ליצ'ס", + "lichessTournaments": "טורנירים של Lichess", "tournamentFAQ": "שאלות נפוצות לגבי טורנירי הזירה", "timeBeforeTournamentStarts": "זמן לתחילת הטורניר", "averageCentipawnLoss": "אובדן מאית־חייל ממוצע", @@ -980,7 +1058,7 @@ "weHaveSentYouAnEmailTo": "שלחנו אימייל ל{param}. לחץ על הלינק באימייל כדי לאפס את הסיסמה.", "byRegisteringYouAgreeToBeBoundByOur": "על ידי הרשמה, את/ה מסכים/ה ל{param}.", "readAboutOur": "קראו את {param}.", - "networkLagBetweenYouAndLichess": "עיכוב הרשת בינך לבין ליצ'ס", + "networkLagBetweenYouAndLichess": "עיכוב הרשת בינך לבין Lichess", "timeToProcessAMoveOnLichessServer": "זמן לעיבוד מהלך בשרת ליצ'ס", "downloadAnnotated": "הורדה עם הערות", "downloadRaw": "הורדה ללא הערות", @@ -998,7 +1076,7 @@ "kidMode": "מצב ילדים", "kidModeIsEnabled": "מצב ילדים מופעל.", "kidModeExplanation": "בשביל הבטיחות. במצב ילדים, כל אמצעי התקשורת באתר מבוטלים. הפעילו אופציה זו עבור ילדיכם ועבור תלמידי בית ספר. זאת כדי להגן עליהם מפני משתמשים אחרים.", - "inKidModeTheLichessLogoGetsIconX": "במצב ילדים הסמל של ליצ'ס מקבל אייקון {param}, כדי שתדעו שילדיכם מוגנים.", + "inKidModeTheLichessLogoGetsIconX": "במצב ילדים הסמל של Lichess מקבל אייקון {param}, כדי שתדעו שילדיכם מוגנים.", "askYourChessTeacherAboutLiftingKidMode": "החשבון שלך מנוהל. תוכל/י לבקש מהמורה שלך לשחמט להסיר את מצב הילדים.", "enableKidMode": "הפעילו מצב ילדים", "disableKidMode": "בטל מצב ילדים", @@ -1121,8 +1199,8 @@ "searchOrStartNewDiscussion": "חפשו את התחילו שיחה חדשה", "edit": "עריכה", "bullet": "Bullet", - "blitz": "Blitz", - "rapid": "Rapid", + "blitz": "בזק", + "rapid": "זריז", "classical": "Classical", "ultraBulletDesc": "משחקים מהירים בטירוף: פחות מ־30 שניות על השעון", "bulletDesc": "משחקים מהירים מאוד: פחות מ3 דקות על השעון", @@ -1217,6 +1295,7 @@ "showMeEverything": "הראו לי הכל", "lichessPatronInfo": "ליצ'ס הוא ארגון לטובת הכלל ותוכנת קוד פתוח חינמית.\nכל עלויות התפעול, הפיתוח והתוכן ממומנות אך ורק על ידי תרומות משתמשים.", "nothingToSeeHere": "אין כלום להצגה כאן, בינתיים.", + "stats": "סטטיסטיקות", "opponentLeftCounter": "{count, plural, =1{יריבך עזב את המשחק. תוכל/י להכריז על נצחון בעוד שנייה {count}.} =2{יריבך עזב את המשחק. תוכל/י להכריז על ניצחון בעוד {count} שניות.} many{יריבך עזב את המשחק. תוכל/י לדרוש ניצחון בעוד {count} שניות.} other{יריבך עזב את המשחק. תוכל/י לדרוש ניצחון בעוד {count} שניות.}}", "mateInXHalfMoves": "{count, plural, =1{מט בעוד חצי מהלך {count}} =2{מט בעוד {count} חצאי מהלכים} many{מט בעוד {count} חצאי מהלכים} other{מט בעוד {count} חצאי מהלכים}}", "nbBlunders": "{count, plural, =1{{count} טעות גסה} =2{{count} טעויות גסות} many{{count} טעויות גסות} other{{count} טעויות גסות}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{ניסיון אחד} =2{{count} ניסיונות} many{{count} ניסיונות} other{{count} נסיונות}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{שוחקה ריצה אחת של {param2}} =2{שוחקו {count} ריצות של {param2}} many{שוחקו {count} ריצות של {param2}} other{שוחקו {count} ריצות של {param2}}}", "streamerLichessStreamers": "שדרני Lichess", + "studyPrivate": "פרטי", + "studyMyStudies": "לוחות הלמידה שלי", + "studyStudiesIContributeTo": "לוחות למידה שתרמתי להם", + "studyMyPublicStudies": "לוחות הלמידה הפומביים שלי", + "studyMyPrivateStudies": "לוחות הלמידה הפרטיים שלי", + "studyMyFavoriteStudies": "לוחות הלמידה המועדפים שלי", + "studyWhatAreStudies": "מה הם לוחות למידה?", + "studyAllStudies": "כל לוחות הלמידה", + "studyStudiesCreatedByX": "לוחות למידה שנוצרו על ידי {param}", + "studyNoneYet": "אין עדיין.", + "studyHot": "כוכבים עולים", + "studyDateAddedNewest": "תאריך הוספה (החדש ביותר)", + "studyDateAddedOldest": "תאריך הוספה (הישן ביותר)", + "studyRecentlyUpdated": "עודכן לאחרונה", + "studyMostPopular": "הכי פופולריים", + "studyAlphabetical": "בסדר האלפבית", + "studyAddNewChapter": "הוסיפו פרק חדש", + "studyAddMembers": "הוספת משתמשים", + "studyInviteToTheStudy": "הזמינו ללוח הלמידה", + "studyPleaseOnlyInvitePeopleYouKnow": "אנא הזמינו רק שחקנים שאתם מכירים המעוניינים להצטרף ללוח הלמידה הזה.", + "studySearchByUsername": "חיפוש לפי שם משתמש", + "studySpectator": "צופה", + "studyContributor": "תורם", + "studyKick": "הסרה", + "studyLeaveTheStudy": "צא/י מלוח הלמידה", + "studyYouAreNowAContributor": "כעת את/ה תורם/ת", + "studyYouAreNowASpectator": "כעת את/ת צופה", + "studyPgnTags": "תוויות PGN", + "studyLike": "אהבתי", + "studyUnlike": "ביטול \"אהבתי\"", + "studyNewTag": "תג חדש", + "studyCommentThisPosition": "הערה לגבי העמדה", + "studyCommentThisMove": "הערה לגבי המסע", + "studyAnnotateWithGlyphs": "השתמשו בסימנים מוסכמים כדי להגיב על מהלכים", + "studyTheChapterIsTooShortToBeAnalysed": "פרק זה קצר מכדי להצדיק ניתוח.", + "studyOnlyContributorsCanRequestAnalysis": "רק תורמי לוח הלמידה יכולים לבקש ניתוח ממוחשב.", + "studyGetAFullComputerAnalysis": "קבל/י ניתוח צד־שרת מלא של המסעים העיקריים (mainline).", + "studyMakeSureTheChapterIsComplete": "ניתן לבקש ניתוח ממוחשב רק פעם אחת, ולכן ודאו שהפרק הושלם.", + "studyAllSyncMembersRemainOnTheSamePosition": "כולם צופים באותה העמדה", + "studyShareChanges": "שתפו שינויים עם הצופים ושמרו אותם על השרת", + "studyPlaying": "מתקיים כעת", + "studyShowEvalBar": "מדי הערכה", + "studyFirst": "ראשון", + "studyPrevious": "הקודם", + "studyNext": "הבא", + "studyLast": "אחרון", "studyShareAndExport": "שיתוף & ייצוא", - "studyStart": "שמירה" + "studyCloneStudy": "שכפול", + "studyStudyPgn": "ה-PGN של לוח הלמידה", + "studyDownloadAllGames": "הורדת כל המשחקים", + "studyChapterPgn": "ה-PGN של הפרק", + "studyCopyChapterPgn": "העתקת ה־PGN", + "studyDownloadGame": "הורדת המשחק", + "studyStudyUrl": "כתובת לוח הלמידה", + "studyCurrentChapterUrl": "כתובת האינטרנט של הפרק הנוכחי", + "studyYouCanPasteThisInTheForumToEmbed": "את/ה יכול/ה לפרסם את זה בפורום כדי להטמיע", + "studyStartAtInitialPosition": "התחילו בעמדת הפתיחה", + "studyStartAtX": "התחילו ב{param}", + "studyEmbedInYourWebsite": "הטמעה באתר שלך", + "studyReadMoreAboutEmbedding": "קראו עוד על הטמעה", + "studyOnlyPublicStudiesCanBeEmbedded": "ניתן להטמיע אך ורק לוחות למידה פומביים!", + "studyOpen": "פתח", + "studyXBroughtToYouByY": "{param1}, מוגש על ידי {param2}", + "studyStudyNotFound": "לוח הלמידה לא נמצא", + "studyEditChapter": "עריכת הפרק", + "studyNewChapter": "פרק חדש", + "studyImportFromChapterX": "ייבא מתוך {param}", + "studyOrientation": "כיוון הלוח", + "studyAnalysisMode": "מצב ניתוח", + "studyPinnedChapterComment": "תגובה מוצמדת לפרק", + "studySaveChapter": "שמור פרק", + "studyClearAnnotations": "נקה הערות", + "studyClearVariations": "נקה וריאציות", + "studyDeleteChapter": "מחיקת הפרק", + "studyDeleteThisChapter": "למחוק את הפרק? אין דרך חזרה!", + "studyClearAllCommentsInThisChapter": "ניקוי כל ההערות, הרישומים והציורים בפרק זה", + "studyRightUnderTheBoard": "ממש מתחת ללוח", + "studyNoPinnedComment": "ללא", + "studyNormalAnalysis": "ניתוח רגיל", + "studyHideNextMoves": "הסתרת המסעים הבאים", + "studyInteractiveLesson": "שיעור אינטראקטיבי", + "studyChapterX": "פרק {param}", + "studyEmpty": "ריק", + "studyStartFromInitialPosition": "התחילו מהעמדה ההתחלתית", + "studyEditor": "עורך", + "studyStartFromCustomPosition": "התחילו מעמדה מותאמת אישית", + "studyLoadAGameByUrl": "טען משחק ע\"י כתובת אינטרנט", + "studyLoadAPositionFromFen": "טען עמדה מFEN", + "studyLoadAGameFromPgn": "טען משחק מPGN", + "studyAutomatic": "אוטומטי", + "studyUrlOfTheGame": "כתובת אינטרנטית של משחק", + "studyLoadAGameFromXOrY": "טען משחק מ{param1} או מ{param2}", + "studyCreateChapter": "צור פרק", + "studyCreateStudy": "יצירת לוח למידה", + "studyEditStudy": "עריכת לוח למידה", + "studyVisibility": "חשיפה", + "studyPublic": "פומבי", + "studyUnlisted": "באמצעות קישור", + "studyInviteOnly": "מוזמנים בלבד", + "studyAllowCloning": "אפשרו יצירת עותקים", + "studyNobody": "אף אחד", + "studyOnlyMe": "רק אני", + "studyContributors": "תורמים", + "studyMembers": "חברים", + "studyEveryone": "כולם", + "studyEnableSync": "הפעל סנכרון", + "studyYesKeepEveryoneOnTheSamePosition": "כן: שמור את כולם באותה העמדה", + "studyNoLetPeopleBrowseFreely": "לא: תן לאנשים לדפדף בחופשיות", + "studyPinnedStudyComment": "תגובה מוצמדת ללוח הלמידה", + "studyStart": "שמירה", + "studySave": "שמירה", + "studyClearChat": "ניקוי הצ'אט", + "studyDeleteTheStudyChatHistory": "למחוק את היסטוריית הצ'אט של לוח הלמידה? אין דרך חזרה!", + "studyDeleteStudy": "מחיקת לוח למידה", + "studyConfirmDeleteStudy": "האם למחוק את כל לוח הלמידה? אין דרך חזרה! הקלידו את שם לוח הלמידה לאישור: {param}", + "studyWhereDoYouWantToStudyThat": "היכן ליצור את לוח הלמידה?", + "studyGoodMove": "מסע טוב", + "studyMistake": "טעות", + "studyBrilliantMove": "מסע מבריק", + "studyBlunder": "טעות חמורה", + "studyInterestingMove": "מסע מעניין", + "studyDubiousMove": "מסע מפוקפק", + "studyOnlyMove": "המסע היחיד", + "studyZugzwang": "כפאי", + "studyEqualPosition": "עמדה מאוזנת", + "studyUnclearPosition": "עמדה לא ברורה", + "studyWhiteIsSlightlyBetter": "יתרון קל ללבן", + "studyBlackIsSlightlyBetter": "יתרון קל לשחור", + "studyWhiteIsBetter": "יתרון ללבן", + "studyBlackIsBetter": "יתרון לשחור", + "studyWhiteIsWinning": "יתרון מכריע ללבן", + "studyBlackIsWinning": "יתרון מכריע לשחור", + "studyNovelty": "חידוש", + "studyDevelopment": "פיתוח", + "studyInitiative": "יוזמה", + "studyAttack": "התקפה", + "studyCounterplay": "מתקפת נגד", + "studyTimeTrouble": "מצוקת זמן", + "studyWithCompensation": "עם פיצוי", + "studyWithTheIdea": "עם הרעיון", + "studyNextChapter": "הפרק הבא", + "studyPrevChapter": "הפרק הקודם", + "studyStudyActions": "פעולות לוח למידה", + "studyTopics": "נושאים", + "studyMyTopics": "הנושאים שלי", + "studyPopularTopics": "נושאים פופולריים", + "studyManageTopics": "עריכת נושאים", + "studyBack": "חזרה", + "studyPlayAgain": "הפעל שוב", + "studyWhatWouldYouPlay": "מה הייתם משחקים בעמדה הזו?", + "studyYouCompletedThisLesson": "מזל טוב! סיימתם את השיעור.", + "studyPerPage": "{param} לכל עמוד", + "studyNbChapters": "{count, plural, =1{פרק {count}} =2{{count} פרקים} many{{count} פרקים} other{{count} פרקים}}", + "studyNbGames": "{count, plural, =1{{count} משחק} =2{{count} משחקים} many{{count} משחקים} other{{count} משחקים}}", + "studyNbMembers": "{count, plural, =1{משתמש אחד} =2{{count} משתמשים} many{{count} משתמשים} other{{count} משתמשים}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{הדבק את טקסט הPGN שלך כאן, עד למשחק {count}} =2{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} many{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} other{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים}}", + "timeagoJustNow": "בדיוק עכשיו", + "timeagoRightNow": "עכשיו", + "timeagoCompleted": "הושלם", + "timeagoInNbSeconds": "{count, plural, =1{עוד שנייה} =2{עוד {count} שניות} many{עוד {count} שניות} other{עוד {count} שניות}}", + "timeagoInNbMinutes": "{count, plural, =1{עוד דקה {count}} =2{עוד {count} דקות} many{עוד {count} דקות} other{עוד {count} דקות}}", + "timeagoInNbHours": "{count, plural, =1{עוד שעה {count}} =2{עוד {count} שעות} many{עוד {count} שעות} other{עוד {count} שעות}}", + "timeagoInNbDays": "{count, plural, =1{עוד יום {count}} =2{עוד {count} ימים} many{עוד {count} ימים} other{עוד {count} ימים}}", + "timeagoInNbWeeks": "{count, plural, =1{עוד שבוע {count}} =2{עוד {count} שבועות} many{עוד {count} שבועות} other{עוד {count} שבועות}}", + "timeagoInNbMonths": "{count, plural, =1{עוד חודש {count}} =2{עוד {count} חודשים} many{עוד {count} חודשים} other{עוד {count} חודשים}}", + "timeagoInNbYears": "{count, plural, =1{עוד שנה {count}} =2{עוד {count} שנים} many{עוד {count} שנים} other{עוד {count} שנים}}", + "timeagoNbMinutesAgo": "{count, plural, =1{לפני דקה {count}} =2{לפני {count} דקות} many{לפני {count} דקות} other{לפני {count} דקות}}", + "timeagoNbHoursAgo": "{count, plural, =1{לפני שעה {count}} =2{לפני {count} שעות} many{לפני {count} שעות} other{לפני {count} שעות}}", + "timeagoNbDaysAgo": "{count, plural, =1{לפני יום {count}} =2{לפני {count} ימים} many{לפני {count} ימים} other{לפני {count} ימים}}", + "timeagoNbWeeksAgo": "{count, plural, =1{לפני שבוע {count}} =2{לפני {count} שבועות} many{לפני {count} שבועות} other{לפני {count} שבועות}}", + "timeagoNbMonthsAgo": "{count, plural, =1{לפני חודש {count}} =2{לפני {count} חודשים} many{לפני {count} חודשים} other{לפני {count} חודשים}}", + "timeagoNbYearsAgo": "{count, plural, =1{לפני שנה {count}} =2{לפני {count} שנים} many{לפני {count} שנים} other{לפני {count} שנים}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{דקה {count} נותרה} =2{{count} דקות נותרו} many{{count} דקות נותרו} other{{count} דקות נותרו}}", + "timeagoNbHoursRemaining": "{count, plural, =1{שעה {count} נותרה} =2{{count} שעות נותרו} many{{count} שעות נותרו} other{{count} שעות נותרו}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hi.arb b/lib/l10n/lila_hi.arb index 6639517c23..e6f08b2986 100644 --- a/lib/l10n/lila_hi.arb +++ b/lib/l10n/lila_hi.arb @@ -1,27 +1,26 @@ { + "mobileAllGames": "सारे गेम्स", + "mobileAreYouSure": "क्या आप सुनिश्चित हैं?", + "mobileCancelTakebackOffer": "Takeback प्रस्ताव रद्द करें", + "mobileFeedbackButton": "फीडबैक", + "mobileHideVariation": "वेरिएशन छुपाए", "mobileHomeTab": "होम", - "mobilePuzzlesTab": "पज़ल", - "mobileToolsTab": "टूल्स", - "mobileWatchTab": "देखें", - "mobileSettingsTab": "सेटिंग", + "mobileLiveStreamers": "लाइव स्ट्रीमर्स", "mobileMustBeLoggedIn": "इस पेज को देखने के लिए आपको login करना होगा", - "mobileFeedbackButton": "फीडबैक", + "mobileNoSearchResults": "कोई परिणाम नहीं", "mobileOkButton": "ओके", + "mobilePuzzlesTab": "पज़ल", "mobileSettingsHapticFeedback": "कंपन फीडबैक", "mobileSettingsImmersiveMode": "इमर्सिव मोड", - "mobileAllGames": "सारे गेम्स", - "mobileNoSearchResults": "कोई परिणाम नहीं", - "mobileAreYouSure": "क्या आप सुनिश्चित हैं?", - "mobileSharePuzzle": "पज़ल शरीर करें", - "mobileShareGameURL": "गेम URL शेयर करें", + "mobileSettingsTab": "सेटिंग", "mobileShareGamePGN": "PGN शेयर करें", + "mobileShareGameURL": "गेम URL शेयर करें", "mobileSharePositionAsFEN": "पोजीशन FEN के रूप में शेयर करें", - "mobileShowVariations": "वेरिएशन देखें", - "mobileHideVariation": "वेरिएशन छुपाए", + "mobileSharePuzzle": "पज़ल शरीर करें", "mobileShowComments": "कमेंट्स देखें", - "mobileCancelTakebackOffer": "Takeback प्रस्ताव रद्द करें", - "mobileCancelDrawOffer": "Draw प्रस्ताव रद्द करें", - "mobileLiveStreamers": "लाइव स्ट्रीमर्स", + "mobileShowVariations": "वेरिएशन देखें", + "mobileToolsTab": "टूल्स", + "mobileWatchTab": "देखें", "activityActivity": "कार्यकलाप", "activityHostedALiveStream": "एक लाइव स्ट्रीम होस्ट किया गया", "activityRankedInSwissTournament": "#{param1} स्थान {param2} मे", @@ -44,7 +43,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} स्विस टूर्नामेंट में भाग लिया} other{{count} स्विस टुर्नामेंटों में भाग लिया}}", "activityJoinedNbTeams": "{count, plural, =1{{count} टीम में शामिल हुए} other{{count} टीमों में शामिल हुए}}", "broadcastBroadcasts": "प्रसारण", + "broadcastMyBroadcasts": "मेरा प्रसारण", "broadcastLiveBroadcasts": "लाइव टूर्नामेंट प्रसारण", + "broadcastNewBroadcast": "नया लाइव प्रसारण", + "broadcastAddRound": "एक दौर जोड़ें", + "broadcastOngoing": "चल रही है", + "broadcastUpcoming": "आगामी", + "broadcastCompleted": "पूर्ण", + "broadcastRoundName": "दौर का नाम", + "broadcastRoundNumber": "दौर संख्या", + "broadcastTournamentName": "प्रतियोगिता का नाम", + "broadcastTournamentDescription": "संक्षिप्त प्रतियोगिता वर्णन", + "broadcastFullDescription": "संक्षिप्त वर्णन", + "broadcastFullDescriptionHelp": "प्रसारण का वैकल्पिक लंबा विवरण. {param1} उपलब्ध है. लंबाई {param2} से कम होना चाहिए", + "broadcastSourceUrlHelp": "URL जो Lichess PGN अपडेट प्राप्त करने के लिए जाँच करेगा। यह सार्वजनिक रूप से इंटरनेट पर सुलभ होना चाहिए।", + "broadcastStartDateHelp": "वैकल्पिक, यदि आप जानना चाहते हो की प्रतिस्प्रधा कब शुरू होगी", + "broadcastCurrentGameUrl": "वर्तमान अध्याय URL", + "broadcastDownloadAllRounds": "सभी राउंड डाउनलोड करें", + "broadcastResetRound": "इस फॉर्म को रीसेट करें", + "broadcastDeleteRound": "इस राउंड को डिलीट करें", + "broadcastDefinitivelyDeleteRound": "राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।", + "broadcastDeleteAllGamesOfThisRound": "इस दौर के सभी गेम हटाएं. उन्हें पुनः बनाने के लिए स्रोत को सक्रिय होने की आवश्यकता होगी।", + "broadcastEditRoundStudy": "राउंड स्टडी संपादित करें", + "broadcastDeleteTournament": "इस टूर्नामेंट को हटाएं", + "broadcastDefinitivelyDeleteTournament": "संपूर्ण टूर्नामेंट, उसके सभी राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।", + "broadcastNbBroadcasts": "{count, plural, =1{{count} प्रसारण} other{{count} प्रसारण}}", "challengeChallengeToPlay": "एक खेल के लिए चुनौती", "challengeChallengeDeclined": "चुनौती इंकार कर दिया", "challengeChallengeAccepted": "चुनौती स्वीकार की गई!", @@ -338,8 +361,8 @@ "puzzleThemeXRayAttackDescription": "एक टुकड़ा एक दुश्मन के टुकड़े के माध्यम से एक वर्ग पर हमला करता है या बचाव करता है।", "puzzleThemeZugzwang": "ज़ुग्ज्वांग", "puzzleThemeZugzwangDescription": "प्रतिद्वंद्वी उन चालों में सीमित है जो वे कर सकते हैं, और सभी चालें उनकी स्थिति को खराब करती हैं।", - "puzzleThemeHealthyMix": "स्वस्थ मिश्रण", - "puzzleThemeHealthyMixDescription": "सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।", + "puzzleThemeMix": "स्वस्थ मिश्रण", + "puzzleThemeMixDescription": "सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।", "puzzleThemePlayerGames": "खिलाड़ियों के खेल", "searchSearch": "खोजें", "settingsSettings": "व्यवस्था (सेटिंग्स)", @@ -456,7 +479,6 @@ "replayMode": "रीप्ले मोड", "realtimeReplay": "रियल टाइम", "byCPL": "CPL द्वारा", - "openStudy": "अध्ययन खोलो", "enable": "सक्षम करें", "bestMoveArrow": "सर्वश्रेष्ठ चाल तीर", "showVariationArrows": "विविधता वाले तीर दिखाएँ", @@ -466,7 +488,6 @@ "memory": "स्मृति (मेमोरी)", "infiniteAnalysis": "अनंत विश्लेषण", "removesTheDepthLimit": "गहराई सीमा को निकालता है, और आपके कंप्यूटर को गर्म रखता है", - "engineManager": "इंजन प्रबंधक", "blunder": "भयंकर गलती", "mistake": "ग़लती", "inaccuracy": "गलती", @@ -661,7 +682,6 @@ "block": "अवस्र्द्ध (ब्लॉक) करें", "blocked": "अवस्र्द्ध (ब्लॉक) कर दिया गया", "unblock": "अवस्र्द्ध (ब्लॉक) न करें", - "followsYou": "आपका अनुसरण कर रहे हैं", "xStartedFollowingY": "{param1} ने {param2} का अनुसरण करना शुरू किया", "more": "अधिक", "memberSince": "सदस्य बनने की तारीख", @@ -760,7 +780,6 @@ "cheat": "धोखेबाज़ी", "troll": "ट्रोल", "other": "दूसरा", - "reportDescriptionHelp": "खेल/खेलों के लिंक को लगाएं (paste) और बताएँ की यूज़र के व्यवहार में क्या खराबी है|", "error_provideOneCheatedGameLink": "कृपया ठगे गए खेल के लिए कम से कम एक लिंक प्रदान करें।", "by": "{param} द्वारा", "importedByX": "{param} द्वारा आयातित", @@ -1124,6 +1143,7 @@ "closingAccountWithdrawAppeal": "आपका खाता बंद करने से आपकी अपील वापस ले ली जाएगी", "ourEventTips": "कार्यक्रम आयोजित करने कि सलाह", "lichessPatronInfo": "Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है।\nसभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं।", + "nothingToSeeHere": "इस समय यहां देखने को कुछ भी नहीं है।", "opponentLeftCounter": "{count, plural, =1{आपके प्रतिद्वंद्वी ने खेल छोड़ दिया। आप {count} सेकंड में जीत का दावा कर सकते हैं।} other{आपके प्रतिद्वंद्वी ने खेल छोड़ दिया। आप {count} सेकंड में जीत का दावा कर सकते हैं।}}", "mateInXHalfMoves": "{count, plural, =1{{count} हाफ मूव में मेट} other{{count} आधे-कदम में चेकमैट}}", "nbBlunders": "{count, plural, =1{{count} गंभीर गलती} other{{count} गंभीर गल्तियां}}", @@ -1212,6 +1232,176 @@ "stormAllTime": "सब समय", "stormXRuns": "{count, plural, =1{एक प्रयास} other{{count} रन}}", "streamerLichessStreamers": "लिचेस स्ट्रीमर", + "studyPrivate": "गोपनीय", + "studyMyStudies": "मेरे अध्ययन", + "studyStudiesIContributeTo": "मेरे योगदान वाले अध्ययन", + "studyMyPublicStudies": "मेरे सार्वजनिक अध्ययन", + "studyMyPrivateStudies": "मेरे निजी अध्ययन", + "studyMyFavoriteStudies": "मेरे पसंदीदा अध्ययन", + "studyWhatAreStudies": "अध्ययन सामग्री क्या है", + "studyAllStudies": "सभी अध्ययन", + "studyStudiesCreatedByX": "{param} द्वारा बनाए गए अध्ययन", + "studyNoneYet": "अभी तक नहीं।", + "studyHot": "लोकप्रिय", + "studyDateAddedNewest": "जोड़ा गया (नवीनतम)", + "studyDateAddedOldest": "जोड़ा गया (सबसे पुराना)", + "studyRecentlyUpdated": "हाल ही में अद्यतित", + "studyMostPopular": "सबसे लोकप्रिय", + "studyAlphabetical": "वर्णक्रमानुसार", + "studyAddNewChapter": "एक नया अध्याय जोड़ें", + "studyAddMembers": "सदस्य जोड़ें", + "studyInviteToTheStudy": "अध्ययन के लिए आमंत्रित करें", + "studyPleaseOnlyInvitePeopleYouKnow": "कृपया केवल उन लोगों को आमंत्रित करें जिन्हें आप जानते हैं, और जो इस अध्ययन में सक्रिय रूप से शामिल होना चाहते हैं।", + "studySearchByUsername": "यूज़रनेम से खोजें", + "studySpectator": "दर्शक", + "studyContributor": "योगदानकर्ता", + "studyKick": "बाहर निकालें", + "studyLeaveTheStudy": "अध्ययन छोड़े", + "studyYouAreNowAContributor": "अब आप एक योगदानकर्ता हैं", + "studyYouAreNowASpectator": "अब आप एक दर्शक हैं", + "studyPgnTags": "PGN टैग", + "studyLike": "लाइक", + "studyUnlike": "नापसन्द करे", + "studyNewTag": "नया टैग", + "studyCommentThisPosition": "इस स्थिति पर टिप्पणी करें", + "studyCommentThisMove": "इस चाल पर टिप्पणी करें", + "studyAnnotateWithGlyphs": "प्रतीक के साथ टिप्पणी करें", + "studyTheChapterIsTooShortToBeAnalysed": "यह अध्याय विश्लेषण के लिए बहुत छोटा है", + "studyOnlyContributorsCanRequestAnalysis": "केवल अध्ययन योगदानकर्ता ही कंप्यूटर विश्लेषण का अनुरोध कर सकते हैं।", + "studyGetAFullComputerAnalysis": "मेनलाइन का पूर्ण सर्वर-साइड कंप्यूटर विश्लेषण प्राप्त करें।", + "studyMakeSureTheChapterIsComplete": "सुनिश्चित करें कि अध्याय पूरा हो गया है। आप केवल एक बार विश्लेषण का अनुरोध कर सकते हैं", + "studyAllSyncMembersRemainOnTheSamePosition": "सभी SYNC सदस्य एक ही स्थिति पर रहेंगे", + "studyShareChanges": "दर्शकों के साथ परिवर्तन साझा करें और उन्हें सर्वर पर सहेजें", + "studyPlaying": "वर्तमान खेल", + "studyFirst": "प्रथम", + "studyPrevious": "पिछला", + "studyNext": "अगला", + "studyLast": "अंतिम", "studyShareAndExport": "शेयर & एक्सपोर्ट करें", - "studyStart": "शुरू करिए" + "studyCloneStudy": "प्रतिलिपि", + "studyStudyPgn": "PGN का अध्ययन करें", + "studyDownloadAllGames": "सभी खेल नीचे लादें", + "studyChapterPgn": "अध्याय PGN", + "studyCopyChapterPgn": "पीजीएन की नकल लें", + "studyDownloadGame": "खेल नीचे लादें", + "studyStudyUrl": "अध्ययन का URL", + "studyCurrentChapterUrl": "वर्तमान अध्याय URL", + "studyYouCanPasteThisInTheForumToEmbed": "आप अध्याय को जोड़ने के लिए इसे फ़ोरम में जोर सकते हैं", + "studyStartAtInitialPosition": "प्रारंभिक स्थिति में शुरू करें", + "studyStartAtX": "{param} से प्रारंभ करें", + "studyEmbedInYourWebsite": "अपनी वेबसाइट अथवा ब्लॉग पर प्रकाशित करें", + "studyReadMoreAboutEmbedding": "एम्बेड करने के बारे में और पढ़ें", + "studyOnlyPublicStudiesCanBeEmbedded": "केवल सार्वजनिक अध्ययनों को एम्बेड किया जा सकता है!", + "studyOpen": "खोलें", + "studyXBroughtToYouByY": "{param1}, {param2} द्वारा आपके लिए", + "studyStudyNotFound": "अध्ययन नहीं मिला", + "studyEditChapter": "अध्याय संपादित करें", + "studyNewChapter": "नया अध्याय", + "studyImportFromChapterX": "{param} से आयात करें", + "studyOrientation": "अभिविन्यास", + "studyAnalysisMode": "विश्लेषण प्रणाली", + "studyPinnedChapterComment": "अध्याय पर की गयी महत्वपूर्ण टिप्पणी", + "studySaveChapter": "अध्याय सहेजें", + "studyClearAnnotations": "टिप्पणी मिटाएँ", + "studyClearVariations": "विविधताओं को मिटाये", + "studyDeleteChapter": "अध्याय हटाएं", + "studyDeleteThisChapter": "इस अध्याय को हटाएं? हटाने के पश्चात वापसी नहीं होगी!", + "studyClearAllCommentsInThisChapter": "इस अध्याय में सभी टिप्पणियाँ, प्रतीक, और आकृतियाँ साफ़ करें?", + "studyRightUnderTheBoard": "बोर्ड के ठीक नीचे", + "studyNoPinnedComment": "खाली", + "studyNormalAnalysis": "सामान्य विश्लेषण", + "studyHideNextMoves": "अगली चालें छिपाएँ", + "studyInteractiveLesson": "संवादमूलक सबक", + "studyChapterX": "अध्याय {param}", + "studyEmpty": "खाली", + "studyStartFromInitialPosition": "प्रारंभिक स्थिति से शुरू करें", + "studyEditor": "संपादक", + "studyStartFromCustomPosition": "कृत्रिम स्थिति से शुरू करें", + "studyLoadAGameByUrl": "URL द्वारा एक गेम लोड करें", + "studyLoadAPositionFromFen": "FEN द्वारा स्थिति लोड करें", + "studyLoadAGameFromPgn": "PGN से एक गेम लोड करें", + "studyAutomatic": "स्वचालित", + "studyUrlOfTheGame": "खेल का URL", + "studyLoadAGameFromXOrY": "{param1} या {param2} से एक गेम लोड करें", + "studyCreateChapter": "अध्याय बनाएँ", + "studyCreateStudy": "अध्ययन बनाएँ", + "studyEditStudy": "अध्ययन संपादित करें", + "studyVisibility": "दृश्यता", + "studyPublic": "सार्वजनिक", + "studyUnlisted": "असूचीबद्ध", + "studyInviteOnly": "केवल आमंत्रित", + "studyAllowCloning": "नकल की अनुमति दें", + "studyNobody": "कोई भी नहीं", + "studyOnlyMe": "केवल मैं", + "studyContributors": "योगदानकर्ता", + "studyMembers": "सदस्य", + "studyEveryone": "सभी", + "studyEnableSync": "Sync चालू", + "studyYesKeepEveryoneOnTheSamePosition": "जी हां सभी को एक ही स्थान पर रखे", + "studyNoLetPeopleBrowseFreely": "नहीं सभी लोगो को अपनी इच्छा से ब्राउज करने दें", + "studyPinnedStudyComment": "रुकिए पढ़िए विचार रखिए", + "studyStart": "शुरू करिए", + "studySave": "बचा कर रखिए", + "studyClearChat": "बातें मिटा दे", + "studyDeleteTheStudyChatHistory": "क्या इस पड़ाई से सम्बन्धित बातों को मिटा देना चाहिए? इससे पीछे जाने का कोई रास्ता शेष नहीं है!", + "studyDeleteStudy": "अध्याय को मिटा दे", + "studyConfirmDeleteStudy": "संपूर्ण अध्ययन हटाएं? वहां से कोई वापसी नहीं है! पुष्टि करने के लिए अध्ययन का नाम टाइप करें:{param}", + "studyWhereDoYouWantToStudyThat": "आप इसको खा से पड़ना चाहते है", + "studyGoodMove": "अच्छी चाल!", + "studyMistake": "ग़लती", + "studyBrilliantMove": "अद्भुत चाल​।", + "studyBlunder": "भयंकर गलती", + "studyInterestingMove": "दिलचस्प चाल​ |", + "studyDubiousMove": "संदिग्ध चाल", + "studyOnlyMove": "इकलौता चाल", + "studyZugzwang": "जबरन चाल", + "studyEqualPosition": "बराबर स्थिति", + "studyUnclearPosition": "अस्पष्ट स्थिति", + "studyWhiteIsSlightlyBetter": "सफेद थोड़ा सा बेहतर है", + "studyBlackIsSlightlyBetter": "काला थोड़ा बेहतर है", + "studyWhiteIsBetter": "सफेद बेहतर है!", + "studyBlackIsBetter": "काला बेहतर है।", + "studyWhiteIsWinning": "सफेद जीत रहा है", + "studyBlackIsWinning": "काला जीत रहा है", + "studyNovelty": "नवीनता", + "studyDevelopment": "विकास", + "studyInitiative": "पहल", + "studyAttack": "आक्रमण", + "studyCounterplay": "काउंटरप्ले", + "studyTimeTrouble": "समय की समस्या", + "studyWithCompensation": "लग मुआवजा।", + "studyWithTheIdea": "विचीर के साथ।", + "studyNextChapter": "अगला अध्याय।", + "studyPrevChapter": "पिछला अध्याय।", + "studyStudyActions": "अध्ययन क्रिया", + "studyTopics": "विषय", + "studyMyTopics": "मेरे विषय", + "studyPopularTopics": "लोकप्रिय विषय", + "studyManageTopics": "विषय प्रबंधन", + "studyBack": "पीछे", + "studyPlayAgain": "फिर से खेलेंगे?", + "studyWhatWouldYouPlay": "आप इस स्थिति में क्या खेलेंगे?", + "studyYouCompletedThisLesson": "बधाई हो! आपने यह सबक पूरा कर लिया है।", + "studyNbChapters": "{count, plural, =1{{count} अध्याय} other{{count} अध्याय}}", + "studyNbGames": "{count, plural, =1{{count} खेल} other{{count} खेल}}", + "studyNbMembers": "{count, plural, =1{{count} सदस्य} other{{count} सदस्य}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक} other{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक}}", + "timeagoJustNow": "अभी", + "timeagoRightNow": "अभी", + "timeagoCompleted": "पूर्ण", + "timeagoInNbSeconds": "{count, plural, =1{{count} सेकेंड में} other{{count} सेकंड में}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} मिनट में} other{{count} मिनट में}}", + "timeagoInNbHours": "{count, plural, =1{{count} घंटों में} other{{count} घंटों में}}", + "timeagoInNbDays": "{count, plural, =1{{count} दिन में} other{{count} दिनो में}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} हफ़्ते में} other{{count} हफ़्तों में}}", + "timeagoInNbMonths": "{count, plural, =1{{count} महीने बाद​} other{{count} महीनो में}}", + "timeagoInNbYears": "{count, plural, =1{{count} साल में} other{{count} सालों में}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} मिनट पहले} other{{count} मिनटों पहले}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} घंटे पहले} other{{count} घंटे पहले}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} दिन पहले} other{{count} दिनों पहले}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} सप्ताह पहले} other{{count} सप्ताह पहले}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} महीने पहले} other{{count} महीने पहले}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} वर्ष पहले} other{{count} वर्षों पहले}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} मिनट बचा है} other{{count} मिनट बचे हैं}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} घंटा बचा है} other{{count} घंटे बचे हैं}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hr.arb b/lib/l10n/lila_hr.arb index 50e1c14efa..172a33e533 100644 --- a/lib/l10n/lila_hr.arb +++ b/lib/l10n/lila_hr.arb @@ -22,6 +22,28 @@ "activityJoinedNbTeams": "{count, plural, =1{Pridružio/la se {count} timu} few{Pridružio/la se {count} tima} other{Pridružio/la se {count} timova}}", "broadcastBroadcasts": "Prijenosi", "broadcastLiveBroadcasts": "Prijenosi turnira uživo", + "broadcastNewBroadcast": "Novi prijenos uživo", + "broadcastAddRound": "Dodajte rundu", + "broadcastOngoing": "U tijeku", + "broadcastUpcoming": "Nadolazi", + "broadcastCompleted": "Završeno", + "broadcastRoundName": "Ime runde", + "broadcastRoundNumber": "Broj runde", + "broadcastTournamentName": "Ime turnira", + "broadcastTournamentDescription": "Kratak opis turnira", + "broadcastFullDescription": "Potpuni opis događaja", + "broadcastFullDescriptionHelp": "Neobavezni dugi opis prijenosa. {param1} je dostupno. Duljina mora biti manja od {param2} znakova.", + "broadcastSourceUrlHelp": "Link koji će Lichess ispitavati kako bi dobio PGN ažuriranja. Mora biti javno dostupan s interneta.", + "broadcastStartDateHelp": "Neobavezno, ako znaš kada događaj počinje", + "broadcastCurrentGameUrl": "URL trenutne igre", + "broadcastDownloadAllRounds": "Preuzmite sve igre", + "broadcastResetRound": "Resetiraj ovu rundu", + "broadcastDeleteRound": "Izbriši ovu rundu", + "broadcastDefinitivelyDeleteRound": "Definitivno izbrišite rundu i njezine igre.", + "broadcastDeleteAllGamesOfThisRound": "Izbriši sve igre ovog kola. Izvor mora biti aktivan kako bi ih se ponovno stvorilo.", + "broadcastDeleteTournament": "Izbriši ovaj turnir", + "broadcastNbBroadcasts": "{count, plural, =1{{count} prijenos} few{{count} prijenosa} other{{count} prijenosa}}", + "challengeChallengesX": "Izazova: {param1}", "challengeChallengeToPlay": "Poziv na partiju", "challengeChallengeDeclined": "Izazov odbijen", "challengeChallengeAccepted": "Izazov prihvaćen!", @@ -137,7 +159,7 @@ "preferencesNotifyTournamentSoon": "Turnir započinje ubrzo", "preferencesNotifyTimeAlarm": "Sat za dopisivanje ističe", "preferencesNotifyBell": "Obavijest zvonom unutar Lichessa", - "preferencesNotifyPush": "Obavijest uređaja kada niste na Lichessu", + "preferencesNotifyPush": "Obavijest uređaja kada niste na Lichess-u", "preferencesNotifyWeb": "Preglednik", "preferencesNotifyDevice": "Uređaj", "preferencesBellNotificationSound": "Obavijest kao zvuk", @@ -336,8 +358,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ili brani polje kroz protivničku figuru.", "puzzleThemeZugzwang": "Iznuđeni potez (Zugzwang)", "puzzleThemeZugzwangDescription": "Protivnik je prisiljen odigrati potez koji mu pogoršava poziciju.", - "puzzleThemeHealthyMix": "Pomalo svega", - "puzzleThemeHealthyMixDescription": "Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.", + "puzzleThemeMix": "Pomalo svega", + "puzzleThemeMixDescription": "Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.", "puzzleThemePlayerGames": "Igračeve partije", "puzzleThemePlayerGamesDescription": "Pogledaj zadatke generirate iz vlastitih partija ili iz partija određenog igrača.", "puzzleThemePuzzleDownloadInformation": "Ovi zadaci su u javnom vlasništvu i mogu biti preuzeti sa {param}.", @@ -456,7 +478,6 @@ "replayMode": "Repriza partije", "realtimeReplay": "U stvarnom vremenu", "byCPL": "Po SDP", - "openStudy": "Otvori studiju", "enable": "Omogući", "bestMoveArrow": "Strelica za najbolji potez", "showVariationArrows": "Pokaži strelice varijacija", @@ -466,7 +487,6 @@ "memory": "Memorija", "infiniteAnalysis": "Neprekidna analiza", "removesTheDepthLimit": "Uklanja granicu do koje računalo može analizirati, i održava tvoje računalo toplim", - "engineManager": "Upravitelj enginea", "blunder": "Gruba greška", "mistake": "Greška", "inaccuracy": "Nepreciznost", @@ -512,7 +532,7 @@ "changeUsernameNotSame": "Jedino se veličina slova može promijeniti. Primjerice, ''johndoe'' u ''JohnDoe''.", "changeUsernameDescription": "Promijeni korisničko ime. Ovo možeš učiniti samo jednom i samo možeš promijeniti veličinu slova svog korisničkog imena.", "signupUsernameHint": "Obavezno odaberi obiteljsko korisničko ime. Ne možeš ga kasnije promijeniti i svi računi s neprikladnim korisničkim imenima bit će zatvoreni!", - "signupEmailHint": "Koristit ćemo ga samo za ponovno postavljanje lozinke.", + "signupEmailHint": "Koristiti ćemo ga samo za ponovno postavljanje lozinke.", "password": "Lozinka", "changePassword": "Promijeni lozinku", "changeEmail": "Promijeni email", @@ -520,7 +540,7 @@ "passwordReset": "Resetiraj lozinku", "forgotPassword": "Zaboravio/la si lozinku?", "error_weakPassword": "Ova je lozinka iznimno česta i previše je lako pogoditi.", - "error_namePassword": "Molimo da ne koristitiš svoje korisničko ime kao lozinku.", + "error_namePassword": "Molimo da ne koristiš svoje korisničko ime kao lozinku.", "blankedPassword": "Koristio si istu lozinku na drugom mjestu, a to je mjesto ugroženo. Kako bismo osigurali sigurnost tvoga Lichess računa, potrebno je da postaviš novu lozinku. Hvala na razumijevanju.", "youAreLeavingLichess": "Odlazite sa Lichess-a", "neverTypeYourPassword": "Nikada nemojte upisivati svoju Lichess lozinku na drugom mjestu!", @@ -659,7 +679,6 @@ "block": "Blokiraj", "blocked": "Blokirani", "unblock": "Odblokiraj", - "followsYou": "Prati te", "xStartedFollowingY": "{param1} je počeo pratiti {param2}", "more": "Više", "memberSince": "Član od", @@ -717,7 +736,9 @@ "ifNoneLeaveEmpty": "Nemaš rejting? Ostavi polje prazno", "profile": "Profil", "editProfile": "Uredi profil", + "realName": "Puno ime", "biography": "Životopis", + "countryRegion": "Država ili regija", "thankYou": "Hvala!", "socialMediaLinks": "Linkovi društvenih mreža", "oneUrlPerLine": "Jedan URL po liniji.", @@ -756,7 +777,6 @@ "cheat": "Varanje", "troll": "Provokacija", "other": "Ostalo", - "reportDescriptionHelp": "Zalijepi link na partiju/e u pitanju i objasni što nije u redu s ponašanjem korisnika. Nemoj samo reći \"varao je\", nego reci kako si došao/la do tog zaključka. Tvoja prijava bit će obrađena brže ako ju napišeš na engleskom jeziku.", "error_provideOneCheatedGameLink": "Molimo navedite barem jedan link igre u kojoj je igrač varao.", "by": "od {param}", "importedByX": "Uvezao {param}", @@ -878,6 +898,8 @@ "keyGoToStartOrEnd": "idi na početak/kraj", "keyShowOrHideComments": "pokaži/sakrij komentare", "keyEnterOrExitVariation": "otvori/zatvori varijantu", + "keyRequestComputerAnalysis": "Zatraži računalnu analizu, Uči na svojim greškama", + "keyNextLearnFromYourMistakes": "Sljedeće (Uči na svojim greškama)", "newTournament": "Novi turnir", "tournamentHomeTitle": "Šahovski turniri s različitim vremenima partije i varijantama", "tournamentHomeDescription": "Igraj brze turnire! Pridruži se turniru ili stvori svoj turnir. Bullet, Blitz, Klasični šah, Šah 960 (Fischerov nasumični šah), Kralj na centru, Tri šaha, i još više opcija za neograničenu šahovsku zabavu.", @@ -950,6 +972,9 @@ "transparent": "Prozirna", "deviceTheme": "Tema uređaja", "backgroundImageUrl": "URL pozadinske slike:", + "board": "Ploča", + "size": "Veličina", + "brightness": "Svjetlina", "pieceSet": "Set figura", "embedInYourWebsite": "Ugradi u svoju stranicu", "usernameAlreadyUsed": "Ovo korisničko ime je već u uporabi, molimo probaj s drugim.", @@ -964,6 +989,7 @@ "invalidFen": "Nevaljan FEN", "custom": "Prilagođeno", "notifications": "Obavijesti", + "notificationsX": "Obavijesti: {param1}", "perfRatingX": "Rejting: {param}", "practiceWithComputer": "Vježbaj sa kompjuterom", "anotherWasX": "Drugi potez je {param}", @@ -1008,6 +1034,7 @@ "playVariationToCreateConditionalPremoves": "Odigraj varijantu da stvoriš uvjetni predpotez", "noConditionalPremoves": "Nema uvjetnih predpoteza", "playX": "Igraj {param}", + "clickHereToReadIt": "Klikni ovdje da pročitaš", "sorry": "Oprosti :(", "weHadToTimeYouOutForAWhile": "Trebali smo te na neko vrijeme izbaciti.", "why": "Zašto?", @@ -1080,7 +1107,7 @@ "minimumRatedGames": "Minimalni broj rejting partija", "minimumRating": "Minimalni rejting", "maximumWeeklyRating": "Maksimalni tjedni rejting", - "positionInputHelp": "Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti {param} za generiranje FEN pozicije, a zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.", + "positionInputHelp": "Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti {param} za generiranje FEN pozicije, zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.", "cancelSimul": "Otkaži simultanku", "simulHostcolor": "Boja domaćina u svakoj igri", "estimatedStart": "Predviđeno vrijeme početka", @@ -1122,6 +1149,7 @@ "closingAccountWithdrawAppeal": "Zatvaranje računa će povući vašu žalbu", "ourEventTips": "Naši savjeti za organizaciju događaja", "lichessPatronInfo": "Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda.\nSvi operativni troškovi, razvoj i sadržaj financiraju se isključivo donacijama korisnika.", + "stats": "Statistika", "opponentLeftCounter": "{count, plural, =1{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekundu.} few{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekunde.} other{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekundi.}}", "mateInXHalfMoves": "{count, plural, =1{Mat u {count} međupotezu} few{Mat u {count} međupoteza} other{Mat u {count} međupoteza}}", "nbBlunders": "{count, plural, =1{{count} gruba greška} few{{count} grube greške} other{{count} grubih grešaka}}", @@ -1216,6 +1244,174 @@ "stormXRuns": "{count, plural, =1{Jedna runda} few{{count} rundi} other{{count} rundi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odigrao jednu rundu od {param2}} few{Odigrao {count} rundi od {param2}} other{Odigrao {count} rundi {param2}}}", "streamerLichessStreamers": "Lichess emiteri", + "studyPrivate": "Privatno", + "studyMyStudies": "Moje studije", + "studyStudiesIContributeTo": "Studije kojima pridonosim", + "studyMyPublicStudies": "Moje javne studije", + "studyMyPrivateStudies": "Moje privatne studije", + "studyMyFavoriteStudies": "Moje omiljene studije", + "studyWhatAreStudies": "Što su studije?", + "studyAllStudies": "Sve studije", + "studyStudiesCreatedByX": "Studije koje je stvorio {param}", + "studyNoneYet": "Još niti jedna.", + "studyHot": "Aktualno", + "studyDateAddedNewest": "Po datumu (najnovije)", + "studyDateAddedOldest": "Po datumu (najstarije)", + "studyRecentlyUpdated": "Nedavno objavljene", + "studyMostPopular": "Najpopularnije", + "studyAlphabetical": "Abecednim redom", + "studyAddNewChapter": "Dodaj novo poglavlje", + "studyAddMembers": "Dodaj članove", + "studyInviteToTheStudy": "Pozovi na učenje", + "studyPleaseOnlyInvitePeopleYouKnow": "Molimo da pozovete ljude koje znate i koji su voljni sudjelovati u ovoj studiji.", + "studySearchByUsername": "Traži prema korisničkom imenu", + "studySpectator": "Gledatelj", + "studyContributor": "Suradnik", + "studyKick": "Izbaci", + "studyLeaveTheStudy": "Napusti studiju", + "studyYouAreNowAContributor": "Postao si suradnik", + "studyYouAreNowASpectator": "Postao si gledatelj", + "studyPgnTags": "PGN oznake", + "studyLike": "Sviđa mi se", + "studyUnlike": "Ne sviđa mi se", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentiraj ovu poziciju", + "studyCommentThisMove": "Komentiraj ovaj potez", + "studyAnnotateWithGlyphs": "Pribilježi glifovima", + "studyTheChapterIsTooShortToBeAnalysed": "Poglavlje je prekratko za analizu.", + "studyOnlyContributorsCanRequestAnalysis": "Samo suradnici u studiji mogu zahtijevati računalnu analizu.", + "studyGetAFullComputerAnalysis": "Dobi potpunu analizu \"main-line\" od servera.", + "studyMakeSureTheChapterIsComplete": "Budite sigurni da je poglavlje gotovo. Zahtjev za računalnom analizom se može dobiti samo jednom.", + "studyAllSyncMembersRemainOnTheSamePosition": "Svi sinkronizirani članovi ostaju na istoj poziciji", + "studyShareChanges": "Podijeli promjene sa gledateljima i pohrani ih na server", + "studyPlaying": "U tijeku", + "studyFirst": "Prvi", + "studyPrevious": "Prethodno", + "studyNext": "Sljedeće", + "studyLast": "Posljednja", "studyShareAndExport": "Podijeli & izvozi", - "studyStart": "Start" + "studyCloneStudy": "Kloniraj", + "studyStudyPgn": "Studiraj PGN", + "studyDownloadAllGames": "Preuzmite sve igre", + "studyChapterPgn": "PGN poglavlja", + "studyCopyChapterPgn": "Kopiraj PGN", + "studyDownloadGame": "Preuzmi igru", + "studyStudyUrl": "Studiraj URL", + "studyCurrentChapterUrl": "URL trenutnog poglavlja", + "studyYouCanPasteThisInTheForumToEmbed": "Možete zaljepiti ovo u forum da ugradite poglavlje", + "studyStartAtInitialPosition": "Kreni s početne pozicije", + "studyStartAtX": "Započni na {param}", + "studyEmbedInYourWebsite": "Ugradi u svoju stranicu ili blog", + "studyReadMoreAboutEmbedding": "Pročitajte više o ugradnji", + "studyOnlyPublicStudiesCanBeEmbedded": "Samo javne studije mogu biti uključene!", + "studyOpen": "Otvori", + "studyXBroughtToYouByY": "{param1} vam je donio {param2}", + "studyStudyNotFound": "Studija nije pronađena", + "studyEditChapter": "Uredi poglavlje", + "studyNewChapter": "Novo poglavlje", + "studyImportFromChapterX": "Unesi iz {param}", + "studyOrientation": "Orijentacija", + "studyAnalysisMode": "Tip analize", + "studyPinnedChapterComment": "Stalni komentar na poglavlje", + "studySaveChapter": "Spremi poglavlje", + "studyClearAnnotations": "Očisti pribilješke", + "studyClearVariations": "Očistiti varijacije", + "studyDeleteChapter": "Obriši poglavlje", + "studyDeleteThisChapter": "Dali želite obrisati ovo poglavlje? Nakon ovoga nema povratka!", + "studyClearAllCommentsInThisChapter": "Želite li očistiti sve komentare, glifove i nacrtane oblike u ovom poglavlju?", + "studyRightUnderTheBoard": "Točno ispod table", + "studyNoPinnedComment": "Ništa", + "studyNormalAnalysis": "Normalna analiza", + "studyHideNextMoves": "Sakrij sljedeći potez", + "studyInteractiveLesson": "Interaktivna poduka", + "studyChapterX": "Poglavlje {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Kreni s početne pozicije", + "studyEditor": "Uređivač", + "studyStartFromCustomPosition": "Kreni s prilagođene pozicije", + "studyLoadAGameByUrl": "Učitaj igru prema URL", + "studyLoadAPositionFromFen": "Učitaj poziciju od FENa", + "studyLoadAGameFromPgn": "Učitaj igru od PGNa", + "studyAutomatic": "Automatski", + "studyUrlOfTheGame": "URL igre", + "studyLoadAGameFromXOrY": "Učitaj igru sa {param1} ili {param2}", + "studyCreateChapter": "Stvori poglavlje", + "studyCreateStudy": "Stvori studiju", + "studyEditStudy": "Uredi studiju", + "studyVisibility": "Vidljivost", + "studyPublic": "Javno", + "studyUnlisted": "Neizlistane", + "studyInviteOnly": "Samo na poziv", + "studyAllowCloning": "Dopusti kloniranje", + "studyNobody": "Nitko", + "studyOnlyMe": "Samo ja", + "studyContributors": "Suradnici", + "studyMembers": "Članovi", + "studyEveryone": "Svi", + "studyEnableSync": "Aktiviraj sinkronizaciju", + "studyYesKeepEveryoneOnTheSamePosition": "Da: drži sve u istoj poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: neka ljudi slobodno pregledavaju", + "studyPinnedStudyComment": "Stalni komentar na studije", + "studyStart": "Start", + "studySave": "Spremi", + "studyClearChat": "Očistite razgovor", + "studyDeleteTheStudyChatHistory": "Dali želite obrisati povijest razgovora? Nakon ovoga nema povratka!", + "studyDeleteStudy": "Izbriši studiju", + "studyConfirmDeleteStudy": "Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: {param}", + "studyWhereDoYouWantToStudyThat": "Gdje želiš to studirati?", + "studyGoodMove": "Dobar potez", + "studyMistake": "Greška", + "studyBrilliantMove": "Briljantan potez", + "studyBlunder": "Gruba greška", + "studyInterestingMove": "Zanimljiv potez", + "studyDubiousMove": "Sumnjiv potez", + "studyOnlyMove": "Jedini potez", + "studyZugzwang": "Iznudica", + "studyEqualPosition": "Jednaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Bijeli je u blagoj prednosti", + "studyBlackIsSlightlyBetter": "Crni je u blagoj prednosti", + "studyWhiteIsBetter": "Bijeli je bolji", + "studyBlackIsBetter": "Crni je bolji", + "studyWhiteIsWinning": "Bijeli dobija", + "studyBlackIsWinning": "Crni dobija", + "studyNovelty": "Nov potez", + "studyDevelopment": "Razvoj", + "studyInitiative": "Inicijativa", + "studyAttack": "Napad", + "studyCounterplay": "Protunapad", + "studyTimeTrouble": "Vremenska nevolja", + "studyWithCompensation": "S kompenzacijom", + "studyWithTheIdea": "S idejom", + "studyNextChapter": "Sljedeće poglavlje", + "studyPrevChapter": "Prethodno poglavlje", + "studyStudyActions": "Studijske radnje", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Popularne teme", + "studyManageTopics": "Upravljaj temama", + "studyBack": "Nazad", + "studyPlayAgain": "Igraj ponovno", + "studyWhatWouldYouPlay": "Što bi igrali u ovoj poziciji?", + "studyYouCompletedThisLesson": "Čestitamo! Završili ste lekciju.", + "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", + "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partije}}", + "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Član} other{{count} Članova}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} igre} few{Ovdje zalijepite svoj PGN tekst, do {count} igri} other{Ovdje zalijepite svoj PGN tekst, do {count} igara}}", + "timeagoJustNow": "upravo sada", + "timeagoRightNow": "upravo sada", + "timeagoCompleted": "završeno", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekunda} few{za {count} sekundi} other{za {count} sekunda}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minute} other{za {count} minuta}}", + "timeagoInNbHours": "{count, plural, =1{za {count} sat} few{za {count} sata} other{za {count} sati}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dan} few{za {count} dana} other{za {count} dana}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} tjedan} few{za {count} tjedna} other{za {count} tjedana}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} mjesec} few{za {count} mjeseca} other{za {count} mjeseci}}", + "timeagoInNbYears": "{count, plural, =1{za {count} godinu} few{za {count} godine} other{za {count} godina}}", + "timeagoNbMinutesAgo": "{count, plural, =1{prije {count} minutu} few{prije {count} minute} other{prije {count} minuta}}", + "timeagoNbHoursAgo": "{count, plural, =1{prije {count} sat} few{prije {count} sata} other{prije {count} sati}}", + "timeagoNbDaysAgo": "{count, plural, =1{prije {count} dan} few{prije {count} dana} other{prije {count} dana}}", + "timeagoNbWeeksAgo": "{count, plural, =1{prije {count} tjedan} few{prije {count} tjedna} other{prije {count} tjedna}}", + "timeagoNbMonthsAgo": "{count, plural, =1{prije {count} mjesec} few{prije {count} mjeseca} other{prije {count} mjeseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{prije {count} godinu} few{prije {count} godine} other{prije {count} godina}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hu.arb b/lib/l10n/lila_hu.arb index 8a7fbce817..e16ee6540f 100644 --- a/lib/l10n/lila_hu.arb +++ b/lib/l10n/lila_hu.arb @@ -1,41 +1,46 @@ { + "mobileAllGames": "Összes játszma", + "mobileAreYouSure": "Biztos vagy benne?", + "mobileBlindfoldMode": "Vakjátszma mód", + "mobileCancelTakebackOffer": "Visszalépés kérésének visszavonása", + "mobileClearButton": "Törlés", + "mobileCorrespondenceClearSavedMove": "Mentett lépés törlése", + "mobileCustomGameJoinAGame": "Csatlakozás játszmához", + "mobileFeedbackButton": "Visszajelzés", + "mobileGreeting": "Üdv {param}!", + "mobileGreetingWithoutName": "Üdv", + "mobileHideVariation": "Változatok elrejtése", "mobileHomeTab": "Kezdőlap", - "mobilePuzzlesTab": "Feladvány", - "mobileToolsTab": "Eszközök", - "mobileWatchTab": "Néznivaló", - "mobileSettingsTab": "Beállítás", + "mobileLiveStreamers": "Lichess streamerek", "mobileMustBeLoggedIn": "Az oldal megtekintéséhez be kell jelentkezned.", - "mobileSystemColors": "Rendszerszínek", - "mobileFeedbackButton": "Visszajelzés", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Haptikus visszajelzés", - "mobileSettingsImmersiveMode": "Teljes képernyős mód", - "mobileSettingsImmersiveModeSubtitle": "A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.", + "mobileNoSearchResults": "Nincs találat", "mobileNotFollowingAnyUser": "Jelenleg nem követsz senkit.", - "mobileAllGames": "Az összes játszma", - "mobileRecentSearches": "Keresési előzmények", - "mobileClearButton": "Törlés", + "mobileOkButton": "OK", "mobilePlayersMatchingSearchTerm": "Játékosok {param} felhasználónévvel", - "mobileNoSearchResults": "Nincs találat", - "mobileAreYouSure": "Biztos vagy benne?", + "mobilePrefMagnifyDraggedPiece": "Mozdított bábu nagyítása", + "mobilePuzzleStormConfirmEndRun": "Befejezed a futamot?", + "mobilePuzzleStormFilterNothingToShow": "Nincs megjeleníthető elem, változtasd meg a szűrőket", + "mobilePuzzleStormSubtitle": "Oldd meg a lehető legtöbb feladványt 3 perc alatt.", "mobilePuzzleStreakAbortWarning": "A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.", - "mobileSharePuzzle": "Feladvány megosztása", - "mobileShareGameURL": "Játszma URL megosztása", + "mobilePuzzleThemesSubtitle": "Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.", + "mobilePuzzlesTab": "Feladvány", + "mobileRecentSearches": "Keresési előzmények", + "mobileSettingsHapticFeedback": "Érintésalapú visszajelzés", + "mobileSettingsImmersiveMode": "Teljes képernyős mód", + "mobileSettingsImmersiveModeSubtitle": "A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.", + "mobileSettingsTab": "Beállítás", "mobileShareGamePGN": "PGN megosztása", + "mobileShareGameURL": "Játszma URL megosztása", "mobileSharePositionAsFEN": "Állás megosztása FEN-ként", - "mobileShowVariations": "Változatok megjelenítése", - "mobileHideVariation": "Változatok elrejtése", + "mobileSharePuzzle": "Feladvány megosztása", "mobileShowComments": "Megjegyzések megjelenítése", - "mobilePuzzleStormConfirmEndRun": "Befejezed a futamot?", - "mobilePuzzleStormFilterNothingToShow": "Nincs megjeleníthető elem, változtasd meg a szűrőket", - "mobileCancelTakebackOffer": "Visszalépés kérésének visszavonása", - "mobileCancelDrawOffer": "Döntetlenkérés visszavonása", - "mobileWaitingForOpponentToJoin": "Várakozás az ellenfél csatlakozására...", - "mobileBlindfoldMode": "Vakjátszma mód", - "mobileLiveStreamers": "Lichess streamerek", - "mobileCustomGameJoinAGame": "Csatlakozás játszmához", - "mobileCorrespondenceClearSavedMove": "Mentett lépés törlése", + "mobileShowResult": "Eredmény mutatása", + "mobileShowVariations": "Változatok megjelenítése", "mobileSomethingWentWrong": "Hiba történt.", + "mobileSystemColors": "Rendszerszínek", + "mobileToolsTab": "Eszközök", + "mobileWaitingForOpponentToJoin": "Várakozás az ellenfél csatlakozására...", + "mobileWatchTab": "Néznivaló", "activityActivity": "Aktivitás", "activityHostedALiveStream": "Élőben közvetített", "activityRankedInSwissTournament": "Helyezés: {param1} / {param2}", @@ -59,6 +64,29 @@ "activityJoinedNbTeams": "{count, plural, =1{{count} csapathoz csatlakozott} other{{count} csapathoz csatlakozott}}", "broadcastBroadcasts": "Versenyközvetítések", "broadcastLiveBroadcasts": "Közvetítések élő versenyekről", + "broadcastNewBroadcast": "Új élő versenyközvetítés", + "broadcastAddRound": "Forduló hozzáadása", + "broadcastOngoing": "Folyamatban", + "broadcastUpcoming": "Közelgő", + "broadcastCompleted": "Befejeződött", + "broadcastRoundName": "Forduló neve", + "broadcastRoundNumber": "Forduló száma", + "broadcastTournamentName": "Verseny neve", + "broadcastTournamentDescription": "Verseny rövid leírása", + "broadcastFullDescription": "Esemény teljes leírása", + "broadcastFullDescriptionHelp": "Opcionális a közvetítés még bővebb leírása. {param1} használható. A hossz nem lehet több, mint {param2} karakter.", + "broadcastSourceUrlHelp": "URL amit a Lichess időnként PGN frissítésekért ellenőriz. Ennek nyilvános internetcímnek kell lennie.", + "broadcastStartDateHelp": "Opcionális, ha tudod mikor kezdődik az esemény", + "broadcastCurrentGameUrl": "Jelenlegi játszma URL", + "broadcastDownloadAllRounds": "Összes játszma letöltése", + "broadcastResetRound": "A forduló újrakezdése", + "broadcastDeleteRound": "A forduló törlése", + "broadcastDefinitivelyDeleteRound": "A forduló és játszmáinak végleges törlése.", + "broadcastDeleteAllGamesOfThisRound": "Minden játék törlése ebben a fordulóban. A forrásnak aktívnak kell lennie, hogy újra létre lehessen hozni őket.", + "broadcastEditRoundStudy": "Forduló tanulmányának szerkesztése", + "broadcastDeleteTournament": "Verseny törlése", + "broadcastDefinitivelyDeleteTournament": "Az egész verseny végleges törlése az összes fordulóval és játszmával együtt.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} versenyközvetítés} other{{count} versenyközvetítés}}", "challengeChallengesX": "Kihívások: {param1}", "challengeChallengeToPlay": "Kihívás játszmára", "challengeChallengeDeclined": "Kihívás elutasítva", @@ -181,6 +209,7 @@ "preferencesNotifyWeb": "Böngésző", "preferencesNotifyDevice": "Eszköz", "preferencesBellNotificationSound": "Hangjelzés", + "preferencesBlindfold": "Vakjátszma mód", "puzzlePuzzles": "Feladványok", "puzzlePuzzleThemes": "Feladvány témák", "puzzleRecommended": "Ajánlott", @@ -376,8 +405,8 @@ "puzzleThemeXRayAttackDescription": "Figura ami egy ellenséges figurán áthatolva véd vagy támad egy mezőt.", "puzzleThemeZugzwang": "Lépéskényszer", "puzzleThemeZugzwangDescription": "Az ellenfélnek kevés lehetséges lépése van, és mind csak tovább rontja a pozícióját.", - "puzzleThemeHealthyMix": "Vegyes mix", - "puzzleThemeHealthyMixDescription": "Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.", + "puzzleThemeMix": "Vegyes mix", + "puzzleThemeMixDescription": "Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.", "puzzleThemePlayerGames": "Felhasználók játszmái", "puzzleThemePlayerGamesDescription": "A saját vagy mások játszmáiból generált feladványok keresése.", "puzzleThemePuzzleDownloadInformation": "Ezek a feladványok közkincsnek minősülnek és innen letölthetők: {param}.", @@ -496,7 +525,6 @@ "replayMode": "Visszajátszás", "realtimeReplay": "Valós idejű", "byCPL": "CPL", - "openStudy": "Tanulmány megnyitása", "enable": "Engedélyezve", "bestMoveArrow": "Legjobb lépés mutatása", "showVariationArrows": "Változatok nyilainak megjelenítése", @@ -506,7 +534,6 @@ "memory": "Memória", "infiniteAnalysis": "Végtelen elemzés", "removesTheDepthLimit": "Feloldja a mélységi korlátot, és melegen tartja a számítógéped", - "engineManager": "Motor menedzser", "blunder": "Baklövés", "mistake": "Hiba", "inaccuracy": "Pontatlanság", @@ -703,7 +730,6 @@ "block": "Letiltás", "blocked": "Letiltva", "unblock": "Letiltás feloldása", - "followsYou": "Követ téged", "xStartedFollowingY": "{param1} {param2} követője lett", "more": "Több", "memberSince": "Tagság kezdete:", @@ -806,7 +832,6 @@ "cheat": "Csalás", "troll": "Trollkodás", "other": "Egyéb", - "reportDescriptionHelp": "Másold be a játék(ok) linkjét, és mondd el, mi a gond a játékos viselkedésével. Ne csak annyit írj, hogy \"csalt\", hanem próbáld elmondani, miből gondolod ezt. A jelentésedet hamarabb feldolgozzák, ha angolul írod.", "error_provideOneCheatedGameLink": "Kérünk, legalább adj meg linket legalább egy csalt játszmához.", "by": "Létrehozta: {param}", "importedByX": "Importálva {param} által", @@ -1289,6 +1314,176 @@ "stormXRuns": "{count, plural, =1{1 futam} other{{count} futam}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Egy {param2} futamot játszott} other{{count} {param2} futamot játszott}}", "streamerLichessStreamers": "Lichess streamerek", + "studyPrivate": "Privát", + "studyMyStudies": "Tanulmányaim", + "studyStudiesIContributeTo": "Tanulmányaim szerkesztőként", + "studyMyPublicStudies": "Nyilvános tanulmányaim", + "studyMyPrivateStudies": "Saját tanulmányaim", + "studyMyFavoriteStudies": "Kedvenc tanulmányaim", + "studyWhatAreStudies": "Mik azok a tanulmányok?", + "studyAllStudies": "Összes tanulmány", + "studyStudiesCreatedByX": "{param} tanulmányai", + "studyNoneYet": "Nincs még ilyen tanulmány.", + "studyHot": "Felkapott", + "studyDateAddedNewest": "Újabbak elöl", + "studyDateAddedOldest": "Hozzáadva (legrégebbi)", + "studyRecentlyUpdated": "Nemrégiben frissítve", + "studyMostPopular": "Legnépszerűbb", + "studyAlphabetical": "Betűrendben", + "studyAddNewChapter": "Új fejezet hozzáadása", + "studyAddMembers": "Tagok hozzáadása", + "studyInviteToTheStudy": "Meghívás a tanulmányba", + "studyPleaseOnlyInvitePeopleYouKnow": "Csak olyan ismerőst hívj meg, aki szeretne részt venni a tanulmány készítésében.", + "studySearchByUsername": "Keresés felhasználónév alapján", + "studySpectator": "Néző", + "studyContributor": "Szerkesztő", + "studyKick": "Eltávolítás", + "studyLeaveTheStudy": "Tanulmány elhagyása", + "studyYouAreNowAContributor": "Szerkesztő lettél", + "studyYouAreNowASpectator": "Néző lettél", + "studyPgnTags": "PGN címkék", + "studyLike": "Kedvel", + "studyUnlike": "Mégse tetszik", + "studyNewTag": "Új címke", + "studyCommentThisPosition": "Megjegyzés ehhez az álláshoz", + "studyCommentThisMove": "Megjegyzés ehhez a lépéshez", + "studyAnnotateWithGlyphs": "Lépések megjelölése", + "studyTheChapterIsTooShortToBeAnalysed": "A fejezet túl rövid számítógépes elemzéshez.", + "studyOnlyContributorsCanRequestAnalysis": "Csak a tanulmány szerkesztői kérhetnek számítógépes elemzést.", + "studyGetAFullComputerAnalysis": "Teljes szerveroldali számítógépes elemzés kérése a főváltozatról.", + "studyMakeSureTheChapterIsComplete": "Ellenőrizd, hogy a fejezet elkészült-e. Csak egyszer kérhető számítógépes elemzés.", + "studyAllSyncMembersRemainOnTheSamePosition": "Minden szinkronizált tag ugyanazt az állást látja", + "studyShareChanges": "A módosítások láthatóak a nézők számára, és mentésre kerülnek a szerveren", + "studyPlaying": "Folyamatban", + "studyFirst": "Első", + "studyPrevious": "Előző", + "studyNext": "Következő", + "studyLast": "Utolsó", "studyShareAndExport": "Megosztás és exportálás", - "studyStart": "Mehet" + "studyCloneStudy": "Klónozás", + "studyStudyPgn": "PGN a tanulmányról", + "studyDownloadAllGames": "Az összes játszma letöltése", + "studyChapterPgn": "PGN a fejezetről", + "studyCopyChapterPgn": "PGN másolása", + "studyDownloadGame": "Játszma letöltése", + "studyStudyUrl": "Tanulmány URL", + "studyCurrentChapterUrl": "URL erre a fejezetre", + "studyYouCanPasteThisInTheForumToEmbed": "Ezzel a linkkel beágyazhatod a fejezetet a Lichess blogodban vagy a fórumon", + "studyStartAtInitialPosition": "Kezdés a kiinduló állásból", + "studyStartAtX": "Kezdés innen: {param}", + "studyEmbedInYourWebsite": "Beágyazás saját weboldalba", + "studyReadMoreAboutEmbedding": "A beágyazásról bővebben", + "studyOnlyPublicStudiesCanBeEmbedded": "Csak nyilvános tanulmányokat lehet beágyazni!", + "studyOpen": "Megnyitás", + "studyXBroughtToYouByY": "{param1}, a {param2} jóvoltából", + "studyStudyNotFound": "Tanulmány nem található", + "studyEditChapter": "Fejezet szerkesztése", + "studyNewChapter": "Új fejezet", + "studyImportFromChapterX": "Importálás innen: {param}", + "studyOrientation": "Szemszög", + "studyAnalysisMode": "Elemzés típusa", + "studyPinnedChapterComment": "Rögzített megjegyzés a fejezethez", + "studySaveChapter": "Fejezet mentése", + "studyClearAnnotations": "Megjegyzések törlése", + "studyClearVariations": "Változatok törlése", + "studyDeleteChapter": "Fejezet törlése", + "studyDeleteThisChapter": "Törlöd a fejezetet? Ezt nem lehet visszavonni!", + "studyClearAllCommentsInThisChapter": "Minden megjegyzés, lépésjelölés és rajz törlése a fejezetből", + "studyRightUnderTheBoard": "Közvetlenül a tábla alatt", + "studyNoPinnedComment": "Nincs", + "studyNormalAnalysis": "Normál elemzés", + "studyHideNextMoves": "Következő lépések elrejtése", + "studyInteractiveLesson": "Interaktív lecke", + "studyChapterX": "{param}. fejezet", + "studyEmpty": "Üres", + "studyStartFromInitialPosition": "Kezdés az alapállásból", + "studyEditor": "Szerkesztő", + "studyStartFromCustomPosition": "Kezdés tetszőleges állásból", + "studyLoadAGameByUrl": "Játszmák betöltése linkkel", + "studyLoadAPositionFromFen": "Állás betöltése FEN-ből", + "studyLoadAGameFromPgn": "Játszmák betöltése PGN-ből", + "studyAutomatic": "Automatikus", + "studyUrlOfTheGame": "Játszmák linkje, soronként egy", + "studyLoadAGameFromXOrY": "Játszmák betöltése {param1} vagy {param2} szerverről", + "studyCreateChapter": "Fejezet létrehozása", + "studyCreateStudy": "Tanulmány létrehozása", + "studyEditStudy": "Tanulmány szerkesztése", + "studyVisibility": "Láthatóság", + "studyPublic": "Nyilvános", + "studyUnlisted": "Nincs listázva", + "studyInviteOnly": "Csak meghívással", + "studyAllowCloning": "Klónozható", + "studyNobody": "Senki", + "studyOnlyMe": "Csak én", + "studyContributors": "Szerkesztők", + "studyMembers": "Tagok", + "studyEveryone": "Mindenki", + "studyEnableSync": "Sync engedélyezése", + "studyYesKeepEveryoneOnTheSamePosition": "Igen: mindenki ugyanazt az állást látja", + "studyNoLetPeopleBrowseFreely": "Nem: szabadon böngészhető", + "studyPinnedStudyComment": "Rögzített megjegyzés a tanulmányhoz", + "studyStart": "Mehet", + "studySave": "Mentés", + "studyClearChat": "Chat törlése", + "studyDeleteTheStudyChatHistory": "Biztosan törlöd a chat előzményeket a tanulmányból? Ezt nem lehet visszavonni!", + "studyDeleteStudy": "Tanulmány törlése", + "studyConfirmDeleteStudy": "Törlöd a teljes tanulmányt? Ezt nem lehet visszavonni! Gépeld be a tanulmány nevét a megerősítéshez: {param}", + "studyWhereDoYouWantToStudyThat": "Melyik tanulmányba kerüljön?", + "studyGoodMove": "Jó lépés", + "studyMistake": "Hiba", + "studyBrilliantMove": "Kiváló lépés", + "studyBlunder": "Durva hiba", + "studyInterestingMove": "Érdekes lépés", + "studyDubiousMove": "Szokatlan lépés", + "studyOnlyMove": "Egyetlen lépés", + "studyZugzwang": "Lépéskényszer", + "studyEqualPosition": "Egyenlő állás", + "studyUnclearPosition": "Zavaros állás", + "studyWhiteIsSlightlyBetter": "Világos kicsit jobban áll", + "studyBlackIsSlightlyBetter": "Sötét kicsit jobban áll", + "studyWhiteIsBetter": "Világos jobban áll", + "studyBlackIsBetter": "Sötét jobban áll", + "studyWhiteIsWinning": "Világos nyerésre áll", + "studyBlackIsWinning": "Sötét nyerésre áll", + "studyNovelty": "Újítás", + "studyDevelopment": "Fejlődés", + "studyInitiative": "Kezdeményezés", + "studyAttack": "Támadás", + "studyCounterplay": "Ellenjáték", + "studyTimeTrouble": "Időzavar", + "studyWithCompensation": "Kompenzáció", + "studyWithTheIdea": "Elképzelés", + "studyNextChapter": "Következő fejezet", + "studyPrevChapter": "Előző fejezet", + "studyStudyActions": "Műveletek a tanulmányban", + "studyTopics": "Témakörök", + "studyMyTopics": "Témaköreim", + "studyPopularTopics": "Népszerű témakörök", + "studyManageTopics": "Témakörök kezelése", + "studyBack": "Vissza", + "studyPlayAgain": "Újra", + "studyWhatWouldYouPlay": "Mit lépnél ebben az állásban?", + "studyYouCompletedThisLesson": "Gratulálok! A fejezet végére értél.", + "studyNbChapters": "{count, plural, =1{{count} Fejezet} other{{count} Fejezet}}", + "studyNbGames": "{count, plural, =1{{count} Játszma} other{{count} Játszma}}", + "studyNbMembers": "{count, plural, =1{{count} Tag} other{{count} Tag}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Illeszd be a PGN szövegét legfeljebb {count} játszmáig} other{Illeszd be a PGN szövegét (legfeljebb {count} játszma)}}", + "timeagoJustNow": "épp most", + "timeagoRightNow": "épp most", + "timeagoCompleted": "befejeződött", + "timeagoInNbSeconds": "{count, plural, =1{{count} másodperc múlva} other{{count} másodperc múlva}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} perc múlva} other{{count} perc múlva}}", + "timeagoInNbHours": "{count, plural, =1{{count} óra múlva} other{{count} óra múlva}}", + "timeagoInNbDays": "{count, plural, =1{{count} nap múlva} other{{count} nap múlva}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} hét múlva} other{{count} hét múlva}}", + "timeagoInNbMonths": "{count, plural, =1{{count} hónap múlva} other{{count} hónap múlva}}", + "timeagoInNbYears": "{count, plural, =1{{count} év múlva} other{{count} év múlva}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} perce} other{{count} perce}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} órája} other{{count} órája}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} napja} other{{count} napja}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} hete} other{{count} hete}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} hónapja} other{{count} hónapja}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} éve} other{{count} éve}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} perc van hátra} other{{count} perc van hátra}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} óra van hátra} other{{count} óra van hátra}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hy.arb b/lib/l10n/lila_hy.arb index 29fe4d3dc0..517a41f5ae 100644 --- a/lib/l10n/lila_hy.arb +++ b/lib/l10n/lila_hy.arb @@ -12,7 +12,27 @@ "activityRankedInTournament": "{count, plural, =1{{count} տեղ ({param2} % լավագույն)՝ {param4} մրցաշարում {param3} խաղերի արդյունքով} other{{count} տեղ ({param2} % լավագույն)՝ {param4} մրցաշարում {param3} խաղերի արդյունքով}}", "activityJoinedNbTeams": "{count, plural, =1{Ընդունվել է {count} թիմ} other{Ընդունվել է {count} թիմ}}", "broadcastBroadcasts": "Հեռարձակումներ", + "broadcastMyBroadcasts": "Իմ հեռարձակումները", "broadcastLiveBroadcasts": "Մրցաշարի ուղիղ հեռարձակումներ", + "broadcastNewBroadcast": "Նոր ուղիղ հեռարձակում", + "broadcastSubscribedBroadcasts": "Բաժանորդագրված հեռարձակումներ", + "broadcastAddRound": "Ավելացնել խաղափուլ", + "broadcastOngoing": "Ընթացիկ", + "broadcastUpcoming": "Առաջիկայում սպասվող", + "broadcastCompleted": "Ավարտված", + "broadcastRoundName": "Խաղափուլի անվանում", + "broadcastRoundNumber": "Խաղափուլի համար", + "broadcastTournamentName": "Մրցաշարի անվանում", + "broadcastTournamentDescription": "Իրադարձության համառոտ նկարագրություն", + "broadcastFullDescription": "Իրադարձության ամբողջական նկարագրություն", + "broadcastStartDateHelp": "Լրացուցիչ, եթե գիտեք, թե երբ է սկսվելու իրադարձությունը", + "broadcastCurrentGameUrl": "Ընթացիկ պարտիայի URL-հասցեն", + "broadcastDownloadAllRounds": "Բեռնել բոլոր խաղափուլերը", + "broadcastResetRound": "Հեռացնել այս խաղափուլը", + "broadcastDeleteRound": "Հեռացնել այս խաղափուլը", + "broadcastEditRoundStudy": "Խմբագրել խաղափուլի ստուդիան", + "broadcastDeleteTournament": "Հեռացնել այս մրցաշարը", + "broadcastDefinitivelyDeleteTournament": "Վերջնականապես հեռացնել ամբողջ մրցաշարը, նրա խաղափուլերը և պարտիաները։", "challengeChallengesX": "Մարտահրավերներ: {param1}", "challengeChallengeToPlay": "Ուղարկել խաղի մարտահրավեր", "challengeChallengeDeclined": "Մարտահրավերը մերժված է", @@ -322,8 +342,8 @@ "puzzleThemeXRayAttackDescription": "Իրավիճակ, երբ հեռահար խաղաքարի հարձակման կամ պաշտպանության գծին կանգնած է մրցակցի խաղաքարը։", "puzzleThemeZugzwang": "Ցուգցվանգ", "puzzleThemeZugzwangDescription": "Մրցակիցը ստիպված է անել հնարավոր փոքրաթիվ քայլերից մեկը, բայց քայլերից ցանկացածը տանում է դիրքի վատացման։", - "puzzleThemeHealthyMix": "Խառը խնդիրներ", - "puzzleThemeHealthyMixDescription": "Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։", + "puzzleThemeMix": "Խառը խնդիրներ", + "puzzleThemeMixDescription": "Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։", "puzzleThemePlayerGames": "Խաղացողի պարտիաները", "puzzleThemePlayerGamesDescription": "Գտնել խնդիրներ, որոնք ստեղծվել են Ձեր պարտիաներից, կամ այլ խաղացողների պարտիաներից։", "puzzleThemePuzzleDownloadInformation": "Այս խնդիրները հանրության սեփականությունն են, և Դուք կարող եք ներբեռնել դրանք՝ {param}։", @@ -440,7 +460,6 @@ "replayMode": "Դիտել կրկնապատկերը", "realtimeReplay": "Ինչպես պարտիայում", "byCPL": "Ըստ սխալների", - "openStudy": "Բացել ուսուցումը", "enable": "Միացնել", "bestMoveArrow": "Լավագույն քայլի սլաքը", "showVariationArrows": "Ցուցադրել տարբերակների սլաքները", @@ -450,7 +469,6 @@ "memory": "Հիշողություն", "infiniteAnalysis": "Անվերջ վերլուծություն", "removesTheDepthLimit": "Վերացնում է խորության սահմանափակումը և տաք պահում ձեր համակարգիչը", - "engineManager": "Շարժիչի մենեջեր", "blunder": "Վրիպում", "mistake": "Սխալ", "inaccuracy": "Անճշտություն", @@ -645,7 +663,6 @@ "block": "Արգելափակել", "blocked": "Արգելափակված է", "unblock": "Հանել արգելափակումը", - "followsYou": "Հետևում են ձեզ", "xStartedFollowingY": "{param1}-ը այժմ հետևում է {param2}-ին", "more": "Ավելին", "memberSince": "Անդամ է՝ սկսած", @@ -742,7 +759,6 @@ "cheat": "խաբեբա", "troll": "Թրոլինգ", "other": "այլ", - "reportDescriptionHelp": "Կիսվեք մեզ հետ Այն խաղերի հղումներով, որտեղ կարծում եք, որ կանոնները խախտվել են և նկարագրեք, թե ինչն է սխալ: Բավական չէ պարզապես գրել \"Նա խարդախում է\", խնդրում ենք նկարագրել, թե ինչպես եք եկել այս եզրակացության: Մենք ավելի արագ կաշխատենք, եթե գրեք անգլերեն:", "error_provideOneCheatedGameLink": "Խնդրում ենք ավելացնել առնվազն մեկ խաղի հղում, որտեղ ձեր կարծիքով խախտվել են կանոնները:", "by": "ըստ {param}", "importedByX": "Ներմուծվել է {param}-ի կողմից", @@ -1210,6 +1226,174 @@ "stormXRuns": "{count, plural, =1{1 փորձ} other{{count} փորձ}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Խաղացվել է մեկ շարք {param2}-ում} other{Խաղացվել է {count} շարք {param2}-ում}}", "streamerLichessStreamers": "Lichess-ի հեռարձակողներ", + "studyPrivate": "Անձնական", + "studyMyStudies": "Իմ ստուդիաները", + "studyStudiesIContributeTo": "Իմ մասնակցությամբ ստուդիաները", + "studyMyPublicStudies": "Իմ հանրային ստուդիաները", + "studyMyPrivateStudies": "Իմ անձնական ստուդիաները", + "studyMyFavoriteStudies": "Իմ սիրելի ստուդիաները", + "studyWhatAreStudies": "Ի՞նչ են «ստուդիաները»", + "studyAllStudies": "Բոլոր ստուդիաները", + "studyStudiesCreatedByX": "{param}-ի ստեղծած ստուդիաները", + "studyNoneYet": "Առայժմ ոչինչ։", + "studyHot": "Ամենաակտիվները", + "studyDateAddedNewest": "Վերջերս ավելացվածները", + "studyDateAddedOldest": "Վաղուց ավելացվածները", + "studyRecentlyUpdated": "Վերջերս թարմացվածները", + "studyMostPopular": "Ամենահայտնիները", + "studyAlphabetical": "Այբբենական կարգով", + "studyAddNewChapter": "Ավելացնել նոր գլուխ", + "studyAddMembers": "Ավելացնել մասնակիցների", + "studyInviteToTheStudy": "Հրավիրել ստուդիա", + "studyPleaseOnlyInvitePeopleYouKnow": "Հրավիրեք միայն այն մասնակիցներին, որոնց ճանաչում եք, և որոնք ակտիվորեն ցանկանում են միանալ այս ստուդիային։", + "studySearchByUsername": "Որոնում ըստ մասնակցային անվան", + "studySpectator": "Հանդիսատես", + "studyContributor": "Խմբագիր", + "studyKick": "Վռնդել", + "studyLeaveTheStudy": "Լքել ստուդիան", + "studyYouAreNowAContributor": "Այժմ Դուք խմբագիր եք", + "studyYouAreNowASpectator": "Այժմ Դուք հանդիսական եք", + "studyPgnTags": "PGN-ի թեգերը", + "studyLike": "Հավանել", + "studyUnlike": "Չեմ հավանում", + "studyNewTag": "Նոր թեգ", + "studyCommentThisPosition": "Մեկնաբանել այս դիրքը", + "studyCommentThisMove": "Մեկնաբանել այս քայլը", + "studyAnnotateWithGlyphs": "Ավելացնել սիմվոլներով անոտացիա", + "studyTheChapterIsTooShortToBeAnalysed": "Վերլուծության համար գլուխը չափազանց կարճ է։", + "studyOnlyContributorsCanRequestAnalysis": "Միայն ստուդիայի խմբագիրները կարող են խնդրել համակարգչային վերլուծություն։", + "studyGetAFullComputerAnalysis": "Սերվերից ստանալ գլխավոր գծի ամբողջական համակարգչային վերլուծություն։", + "studyMakeSureTheChapterIsComplete": "Համոզվեք, որ գլուխն ավարտված է։ Համակարգչային վերլուծություն կարող եք խնդրել միայն մեկ անգամ։", + "studyAllSyncMembersRemainOnTheSamePosition": "Բոլոր սինքրոնիզացված մասնակիցները մնում են նույն դիրքում", + "studyShareChanges": "Փոփոխությունները տարածել հանդիսականների շրջանում և դրանք պահպանել սերվերում", + "studyPlaying": "Ակտիվ", + "studyFirst": "Առաջինը", + "studyPrevious": "Նախորդը", + "studyNext": "Հաջորդը", + "studyLast": "Վերջինը", "studyShareAndExport": "Տարածել & և արտահանել", - "studyStart": "Սկսել" + "studyCloneStudy": "Կլոնավորել", + "studyStudyPgn": "Ստուդիայի PGN-ն", + "studyDownloadAllGames": "Ներբեռնել բոլոր պարտիաները", + "studyChapterPgn": "Գլխի PGN-ը", + "studyCopyChapterPgn": "Պատճենել PGN-ը", + "studyDownloadGame": "Ներբեռնել պարտիան", + "studyStudyUrl": "Ստուդիայի հղումը", + "studyCurrentChapterUrl": "Այս գլխի հղումը", + "studyYouCanPasteThisInTheForumToEmbed": "Ֆորումում կամ Lichess-ի բլոգում ներդնելու համար տեղադրեք այս կոդը", + "studyStartAtInitialPosition": "Բացել սկզբնական դիրքում", + "studyStartAtX": "Սկսել {param}-ից", + "studyEmbedInYourWebsite": "Ներդնել սեփական կայքում կամ բլոգում", + "studyReadMoreAboutEmbedding": "Մանրամասն կայքում ներդնելու մասին", + "studyOnlyPublicStudiesCanBeEmbedded": "Կայքում կարելի է ներդնել միայն հրապարակային ստուդիաները։", + "studyOpen": "Բացել", + "studyXBroughtToYouByY": "{param1}-ը {param2}-ից", + "studyStudyNotFound": "Ստուդիան չի գտնվել", + "studyEditChapter": "Խմբագրել գլուխը", + "studyNewChapter": "Նոր գլուխ", + "studyImportFromChapterX": "Ներդնել {param}-ից", + "studyOrientation": "Կողմնորոշում", + "studyAnalysisMode": "Վերլուծության ռեժիմ", + "studyPinnedChapterComment": "Գլխի ամրակցված մեկնաբանություն", + "studySaveChapter": "Պահպանել գլուխը", + "studyClearAnnotations": "Հեռացնել անոտացիան", + "studyClearVariations": "Հեռացնել տարբերակները", + "studyDeleteChapter": "Հեռացնել գլուխը", + "studyDeleteThisChapter": "Հեռացնե՞լ գլուխը։ Վերականգնել հնարավոր չի լինի։", + "studyClearAllCommentsInThisChapter": "Մաքրե՞լ այս գլխի բոլոր մեկնաբանություններն ու նշումները", + "studyRightUnderTheBoard": "Անմիջապես տախտակի տակ", + "studyNoPinnedComment": "Ոչ", + "studyNormalAnalysis": "Սովորական վերլուծություն", + "studyHideNextMoves": "Թաքցնել հետագա քայլերը", + "studyInteractiveLesson": "Ինտերակտիվ դասընթաց", + "studyChapterX": "Գլուխ {param}", + "studyEmpty": "Դատարկ է", + "studyStartFromInitialPosition": "Սկսել նախնական դիրքից", + "studyEditor": "Խմբագիր", + "studyStartFromCustomPosition": "Սկսել սեփական դիրքից", + "studyLoadAGameByUrl": "Բեռնել պարտիան ըստ URL-ի", + "studyLoadAPositionFromFen": "Բեռնել դիրքը FEN-ով", + "studyLoadAGameFromPgn": "Բեռնել դիրքն ըստ PGN-ի", + "studyAutomatic": "Ինքնաբերաբար", + "studyUrlOfTheGame": "Պարտիայի URL-ը, մեկ տողով", + "studyLoadAGameFromXOrY": "Ներբեռնել խաղը {param1}-ից կամ {param2}-ից", + "studyCreateChapter": "Ստեղծել գլուխը", + "studyCreateStudy": "Ստեղծել ստուդիա", + "studyEditStudy": "Խմբագրել ստուդիան", + "studyVisibility": "Հասանելի է դիտման համար", + "studyPublic": "Հրապարակային", + "studyUnlisted": "Հղումով", + "studyInviteOnly": "Միայն հրավերով", + "studyAllowCloning": "Թույլատրել պատճենումը", + "studyNobody": "Ոչ ոք", + "studyOnlyMe": "Միայն ես", + "studyContributors": "Համահեղինակներ", + "studyMembers": "Անդամները", + "studyEveryone": "Բոլորը", + "studyEnableSync": "Միացնել սինքրոնացումը", + "studyYesKeepEveryoneOnTheSamePosition": "Այո. բոլորի համար դնել միևնույն դիրքը", + "studyNoLetPeopleBrowseFreely": "Ոչ. թույլատրել մասնակիցներին ազատ ուսումնասիրել բոլոր դիրքերը", + "studyPinnedStudyComment": "Ստուդիայի ամրակցված մեկնաբանություն", + "studyStart": "Սկսել", + "studySave": "Պահպանել", + "studyClearChat": "Մաքրել զրուցարանը", + "studyDeleteTheStudyChatHistory": "Հեռացնե՞լ ստուդիայի զրուցարանը։ Վերականգնել հնարավոր չի լինի։", + "studyDeleteStudy": "Հեռացնել ստուդիան", + "studyConfirmDeleteStudy": "Հեռացնե՞լ ամբողջ ստուդիան։ Հեռացումն անդառնալի կլինի։ Հաստատելու համար մուտքագրեք ստուդիայի անվանումը՝ {param}", + "studyWhereDoYouWantToStudyThat": "Որտե՞ղ եք ցանկանում ստեղծել ստուդիան։", + "studyGoodMove": "Լավ քայլ է", + "studyMistake": "Սխալ", + "studyBrilliantMove": "Գերազանց քայլ է", + "studyBlunder": "Վրիպում", + "studyInterestingMove": "Հետաքրքիր քայլ է", + "studyDubiousMove": "Կասկածելի քայլ", + "studyOnlyMove": "Միակ քայլ", + "studyZugzwang": "Ցուգցվանգ", + "studyEqualPosition": "Հավասար դիրք", + "studyUnclearPosition": "Անորոշ դիրք", + "studyWhiteIsSlightlyBetter": "Սպիտակները մի քիչ լավ են", + "studyBlackIsSlightlyBetter": "Սևերը մի քիչ լավ են", + "studyWhiteIsBetter": "Սպիտակները լավ են", + "studyBlackIsBetter": "Սևերը լավ են", + "studyWhiteIsWinning": "Սպիտակները հաղթում են", + "studyBlackIsWinning": "Սևերը հաղթում են", + "studyNovelty": "Նորույթ", + "studyDevelopment": "Զարգացում", + "studyInitiative": "Նախաձեռնություն", + "studyAttack": "Գրոհ", + "studyCounterplay": "Հակախաղ", + "studyTimeTrouble": "Ցայտնոտ", + "studyWithCompensation": "Փոխհատուցմամբ", + "studyWithTheIdea": "Մտահղացմամբ", + "studyNextChapter": "Հաջորդ գլուխը", + "studyPrevChapter": "Նախորդ գլուխը", + "studyStudyActions": "Գործողությունները ստուդիայում", + "studyTopics": "Թեմաներ", + "studyMyTopics": "Իմ թեմաները", + "studyPopularTopics": "Շատ դիտվող թեմաներ", + "studyManageTopics": "Թեմաների կառավարում", + "studyBack": "Հետ", + "studyPlayAgain": "Կրկին խաղալ", + "studyWhatWouldYouPlay": "Ինչպե՞ս կխաղայիք այս դիրքում", + "studyYouCompletedThisLesson": "Շնորհավորո՜ւմ ենք։ Դուք ավարեցիք այս դասը։", + "studyNbChapters": "{count, plural, =1{{count} գլուխ} other{{count} գլուխ}}", + "studyNbGames": "{count, plural, =1{{count} պարտիա} other{{count} պարտիա}}", + "studyNbMembers": "{count, plural, =1{{count} մասնակից} other{{count} մասնակից}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի} other{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի}}", + "timeagoJustNow": "հենց հիմա", + "timeagoRightNow": "հենց հիմա", + "timeagoCompleted": "ավարտված", + "timeagoInNbSeconds": "{count, plural, =1{{count} վայրկյան հետո} other{{count} վայրկյաններ հետո}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} րոպե հետո} other{{count} րոպեներ հետո}}", + "timeagoInNbHours": "{count, plural, =1{{count} ժամ հետո} other{{count} ժամ հետո}}", + "timeagoInNbDays": "{count, plural, =1{{count} օր հետո} other{{count} օր հետո}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} շաբաթ հետո} other{{count} շաբաթ հետո}}", + "timeagoInNbMonths": "{count, plural, =1{{count} ամիս հետո} other{{count} ամիս հետո}}", + "timeagoInNbYears": "{count, plural, =1{{count} տարի հետո} other{{count} տարի հետո}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} րոպե առաջ} other{{count} րոպե առաջ}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ժամ առաջ} other{{count} ժամ առաջ}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} օր առաջ} other{{count} օր առաջ}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} շաբաթ առաջ} other{{count} շաբաթ առաջ}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ամիս առաջ} other{{count} ամիս առաջ}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} տարի առաջ} other{{count} տարի առաջ}}" } \ No newline at end of file diff --git a/lib/l10n/lila_id.arb b/lib/l10n/lila_id.arb index 56bf104148..d8179e729a 100644 --- a/lib/l10n/lila_id.arb +++ b/lib/l10n/lila_id.arb @@ -1,4 +1,21 @@ { + "mobileAllGames": "Semua permainan", + "mobileAreYouSure": "Apa kamu yakin?", + "mobileFeedbackButton": "Ulas balik", + "mobileGreeting": "Halo, {param}", + "mobileGreetingWithoutName": "Halo", + "mobileHideVariation": "Sembunyikan variasi", + "mobileHomeTab": "Beranda", + "mobileOkButton": "Oke", + "mobilePuzzlesTab": "Teka-teki", + "mobileSettingsTab": "Pengaturan", + "mobileShareGamePGN": "Bagikan GPN", + "mobileShareGameURL": "Bagikan URL permainan", + "mobileSharePuzzle": "Bagikan teka-teki ini", + "mobileShowComments": "Tampilkan komentar", + "mobileShowResult": "Tampilkan hasil", + "mobileShowVariations": "Tampilkan variasi", + "mobileWatchTab": "Tontonan", "activityActivity": "Aktivitas", "activityHostedALiveStream": "Host streaming langsung", "activityRankedInSwissTournament": "Peringkat #{param1} di {param2}", @@ -22,6 +39,23 @@ "activityJoinedNbTeams": "{count, plural, other{Bergabung {count} tim}}", "broadcastBroadcasts": "Siaran", "broadcastLiveBroadcasts": "Siaran turnamen langsung", + "broadcastNewBroadcast": "Siaran langsung baru", + "broadcastAddRound": "Tambakan ronde", + "broadcastOngoing": "Sedang berlangsung", + "broadcastUpcoming": "Akan datang", + "broadcastCompleted": "Telah selesai", + "broadcastRoundName": "Nama ronde", + "broadcastRoundNumber": "Babak ronde", + "broadcastTournamentName": "Nama turnamen", + "broadcastTournamentDescription": "Deskripsi singkat turnamen", + "broadcastFullDescription": "Keterangan acara secara penuh", + "broadcastFullDescriptionHelp": "Deskripsi panjang opsional dari siaran. {param1} tersedia. Panjangnya harus kurang dari {param2} karakter.", + "broadcastSourceUrlHelp": "URL yang akan di-polling oleh Lichess untuk mendapatkan pembaruan PGN. Itu harus dapat diakses publik dari Internet.", + "broadcastStartDateHelp": "Opsional, jika Anda tahu kapan acara dimulai", + "broadcastCurrentGameUrl": "Tautan permainan ini", + "broadcastDownloadAllRounds": "Unduh semua ronde", + "broadcastResetRound": "Atur ulang ronde ini", + "broadcastDeleteRound": "Hapus ronde ini", "challengeChallengeToPlay": "Menantang ke permainan", "challengeChallengeDeclined": "Tantangan ditolak", "challengeChallengeAccepted": "Tantangan diterima!", @@ -95,6 +129,7 @@ "preferencesExplainShowPlayerRatings": "Ini dapat menyembunyikan rating dari website, agar membantu fokus ke catur. Permainan tetap dapat dinilai, ini hanya untuk apa yang Anda lihat.", "preferencesDisplayBoardResizeHandle": "Tampilkan pengubah ukuran papan catur", "preferencesOnlyOnInitialPosition": "Hanya di posisi awal", + "preferencesInGameOnly": "Hanya di dalam permainan", "preferencesChessClock": "Jam catur", "preferencesTenthsOfSeconds": "Sepersepuluh detik", "preferencesWhenTimeRemainingLessThanTenSeconds": "Ketika waktu tersisa < 10 detik", @@ -328,8 +363,8 @@ "puzzleThemeXRayAttackDescription": "Bidak menyerang atau mempertahankan kotak, melalui bidak musuh.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Musuh dibatasi gerakan yang dapat mereka lakukan, dan semua gerakan memperburuk posisi mereka.", - "puzzleThemeHealthyMix": "Campuran baik", - "puzzleThemeHealthyMixDescription": "Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.", + "puzzleThemeMix": "Campuran baik", + "puzzleThemeMixDescription": "Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.", "puzzleThemePlayerGames": "Permainan pemain", "puzzleThemePlayerGamesDescription": "Mengambil taktik yang dihasilkan dari permainan anda, atau pemain lain.", "puzzleThemePuzzleDownloadInformation": "Taktik-taktik ini ada di domain publik, dan dapat di download dari {param}.", @@ -374,7 +409,7 @@ "threeChecks": "Tiga kali skak", "raceFinished": "Balapan telah berakhir", "variantEnding": "Akhir sesuai aturan variasi", - "newOpponent": "Penantang baru", + "newOpponent": "Permainan yang baru", "yourOpponentWantsToPlayANewGameWithYou": "Lawan Anda ingin bermain lagi dengan Anda", "joinTheGame": "Ikuti permainan", "whitePlays": "Putih melangkah", @@ -444,7 +479,6 @@ "replayMode": "Mode Putar Ulang", "realtimeReplay": "Langsung", "byCPL": "Secara CPL", - "openStudy": "Buka studi", "enable": "Aktifkan", "bestMoveArrow": "Panah Langkah terbaik", "showVariationArrows": "Tampilkan variasi panah", @@ -454,7 +488,6 @@ "memory": "Memori", "infiniteAnalysis": "Menganalisa tanpa ada batasan", "removesTheDepthLimit": "Menghapus batas kedalaman, dan membuat komputer Anda hangat", - "engineManager": "Pengaturan komputer", "blunder": "Blunder", "mistake": "Kesalahan", "inaccuracy": "Ketidaktelitian", @@ -480,6 +513,7 @@ "latestForumPosts": "Pesan forum terbaru", "players": "Pemain", "friends": "Teman", + "otherPlayers": "pemain lainnya", "discussions": "Diskusi", "today": "Hari ini", "yesterday": "Kemarin", @@ -651,7 +685,6 @@ "block": "Blokir", "blocked": "Diblokir", "unblock": "Buka blokir", - "followsYou": "Mengikuti anda", "xStartedFollowingY": "{param1} mulai mengikuti {param2}", "more": "Lainnya", "memberSince": "Anggota sejak", @@ -710,6 +743,7 @@ "ifNoneLeaveEmpty": "Jika tidak ada, biarkan kosong", "profile": "Profil", "editProfile": "Ubah profil", + "realName": "Nama asli", "setFlair": "Sunting flair anda", "flair": "Flair", "youCanHideFlair": "Terdapat setting untuk menyembunyikan semua flair pengguna pada seluruh situs.", @@ -735,6 +769,7 @@ "descPrivateHelp": "Teks yang hanya bisa dilihat oleh anggota tim. Jika ditetapkan, akan menggantikan deskripsi publik untuk anggota tim.", "no": "Tidak", "yes": "Ya", + "website": "Situs Web", "help": "Bantuan:", "createANewTopic": "Buat topik baru", "topics": "Topik", @@ -753,7 +788,6 @@ "cheat": "Cheat", "troll": "Jebakan", "other": "Lainnya", - "reportDescriptionHelp": "Paste link berikut ke dalam permainan dan jelaskan apa masalah tentang pengguna ini.", "error_provideOneCheatedGameLink": "Harap berikan setidaknya satu tautan ke permainan yang curang.", "by": "oleh {param}", "importedByX": "Diimpor oleh {param}", @@ -956,6 +990,12 @@ "transparent": "Tembus pandang", "deviceTheme": "Tema perangkat", "backgroundImageUrl": "URL gambar latar belakang:", + "board": "Papan", + "size": "Ukuran", + "opacity": "Transparansi", + "brightness": "Kecerahan", + "hue": "Rona", + "boardReset": "Kembalikan warna ke pengaturan semula", "pieceSet": "Susunan buah catur", "embedInYourWebsite": "Salin dalam website Anda", "usernameAlreadyUsed": "Nama pengguna ini sudah digunakan, coba yang lain.", @@ -1015,6 +1055,8 @@ "playVariationToCreateConditionalPremoves": "Memainkan sebuah variasi untuk membuat syarat pra-langkah", "noConditionalPremoves": "Tidak ada syarat pra-langkah", "playX": "Memainkan {param}", + "showUnreadLichessMessage": "Anda telah menerima pesan pribadi dari Lichess.", + "clickHereToReadIt": "Klik di sini untuk membaca", "sorry": "Maaf :(", "weHadToTimeYouOutForAWhile": "Kami harus mengatur waktu Anda untuk sementara waktu.", "why": "Mengapa?", @@ -1036,6 +1078,8 @@ "agreementPolicy": "Saya menyetujui bahwa saya akan mengikuti semua kebijakan yang ada di Lichess.", "searchOrStartNewDiscussion": "Cari atau mulai diskusi baru", "edit": "Ubah", + "bullet": "Peluru", + "blitz": "Blitz", "rapid": "Cepat", "classical": "Klasikal", "ultraBulletDesc": "Sangat-sangat cepat: kurang dari 30 detik", @@ -1126,7 +1170,10 @@ "switchSides": "Tukar Sisi", "closingAccountWithdrawAppeal": "Menutup akun anda akan menarik permohonan banding", "ourEventTips": "Tips dari kami terkait penyelenggaraan acara", + "instructions": "Instruksi", + "showMeEverything": "Tunjukkan semuanya", "lichessPatronInfo": "Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas.\nSemua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna.", + "nothingToSeeHere": "Tidak ada yang bisa dilihat untuk saat ini.", "opponentLeftCounter": "{count, plural, other{Lawanmu telah meninggalkan permainan. Anda dapat mengklaim kemenangan dalam {count} detik.}}", "mateInXHalfMoves": "{count, plural, other{Mati {count} di pertengahan langkah}}", "nbBlunders": "{count, plural, other{{count} blunder}}", @@ -1223,6 +1270,175 @@ "stormXRuns": "{count, plural, other{{count} berjalan}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{Memainkan {count} kali {param2}}}", "streamerLichessStreamers": "Streamer Lichess", + "studyPrivate": "Pribadi", + "studyMyStudies": "Studi saya", + "studyStudiesIContributeTo": "Studi yang saya ikut berkontribusi", + "studyMyPublicStudies": "Studi publik saya", + "studyMyPrivateStudies": "Studi pribadi saya", + "studyMyFavoriteStudies": "Studi favorit saya", + "studyWhatAreStudies": "Apa itu studi?", + "studyAllStudies": "Semua studi", + "studyStudiesCreatedByX": "Studi dibuat oleh {param}", + "studyNoneYet": "Tidak ada.", + "studyHot": "Terhangat", + "studyDateAddedNewest": "Tanggal ditambahkan (terbaru)", + "studyDateAddedOldest": "Tanggal ditambahkan (terlama)", + "studyRecentlyUpdated": "Baru saja diperbarui", + "studyMostPopular": "Paling populer", + "studyAlphabetical": "Menurut abjad", + "studyAddNewChapter": "Tambahkan bab baru", + "studyAddMembers": "Tambahkan anggota", + "studyInviteToTheStudy": "Ajak untuk studi", + "studyPleaseOnlyInvitePeopleYouKnow": "Harap hanya mengundang orang yang Anda kenal, dan yang secara aktif ingin bergabung dengan studi ini.", + "studySearchByUsername": "Cari berdasarkan nama pengguna", + "studySpectator": "Penonton", + "studyContributor": "Kontributor", + "studyKick": "Diusir", + "studyLeaveTheStudy": "Tinggalkan studi", + "studyYouAreNowAContributor": "Sekarang Anda menjadi kontributor", + "studyYouAreNowASpectator": "Sekarang Anda adalah penonton", + "studyPgnTags": "Tagar PGN", + "studyLike": "Suka", + "studyUnlike": "Batal Suka", + "studyNewTag": "Tagar baru", + "studyCommentThisPosition": "Komentar di posisi ini", + "studyCommentThisMove": "Komentari langkah ini", + "studyAnnotateWithGlyphs": "Anotasikan dengan glif", + "studyTheChapterIsTooShortToBeAnalysed": "Bab ini terlalu pendek untuk di analisa.", + "studyOnlyContributorsCanRequestAnalysis": "Hanya kontributor yang dapat meminta analisa komputer.", + "studyGetAFullComputerAnalysis": "Dapatkan analisis komputer penuh di pihak server dari jalur utama.", + "studyMakeSureTheChapterIsComplete": "Pastikan bab ini selesai. Anda hanya dapat meminta analisis satu kali.", + "studyAllSyncMembersRemainOnTheSamePosition": "Semua anggota yang ter-sinkron tetap pada posisi yang sama", + "studyShareChanges": "Bagikan perubahan dengan penonton dan simpan di server", + "studyPlaying": "Memainkan", + "studyFirst": "Pertama", + "studyPrevious": "Sebelumnya", + "studyNext": "Berikutnya", + "studyLast": "Terakhir", "studyShareAndExport": "Bagikan & ekspor", - "studyStart": "Mulai" + "studyCloneStudy": "Gandakan", + "studyStudyPgn": "Studi PGN", + "studyDownloadAllGames": "Unduh semua permainan", + "studyChapterPgn": "Bab PGN", + "studyCopyChapterPgn": "Salin PGN", + "studyDownloadGame": "Unduh permainan", + "studyStudyUrl": "URL studi", + "studyCurrentChapterUrl": "URL Bab saat ini", + "studyYouCanPasteThisInTheForumToEmbed": "Anda dapat menempelkan ini di forum untuk disematkan", + "studyStartAtInitialPosition": "Mulai saat posisi awal", + "studyStartAtX": "Mulai dari {param}", + "studyEmbedInYourWebsite": "Sematkan di blog atau website Anda", + "studyReadMoreAboutEmbedding": "Baca lebih tentang penyematan", + "studyOnlyPublicStudiesCanBeEmbedded": "Hanya pelajaran publik yang dapat di sematkan!", + "studyOpen": "Buka", + "studyXBroughtToYouByY": "{param1} dibawakan kepadamu dari {param2}", + "studyStudyNotFound": "Studi tidak ditemukan", + "studyEditChapter": "Ubah bab", + "studyNewChapter": "Bab baru", + "studyImportFromChapterX": "Impor dari {param}", + "studyOrientation": "Orientasi", + "studyAnalysisMode": "Mode analisa", + "studyPinnedChapterComment": "Sematkan komentar bagian bab", + "studySaveChapter": "Simpan bab", + "studyClearAnnotations": "Hapus anotasi", + "studyClearVariations": "Hapus variasi", + "studyDeleteChapter": "Hapus bab", + "studyDeleteThisChapter": "Hapus bab ini? Ini tidak akan dapat mengulangkan kembali!", + "studyClearAllCommentsInThisChapter": "Hapus semua komentar dan bentuk di bab ini?", + "studyRightUnderTheBoard": "Kanan dibawah papan", + "studyNoPinnedComment": "Tidak ada", + "studyNormalAnalysis": "Analisa biasa", + "studyHideNextMoves": "Sembunyikan langkah selanjutnya", + "studyInteractiveLesson": "Pelajaran interaktif", + "studyChapterX": "Bab {param}", + "studyEmpty": "Kosong", + "studyStartFromInitialPosition": "Mulai dari posisi awal", + "studyEditor": "Penyunting", + "studyStartFromCustomPosition": "Mulai dari posisi yang disesuaikan", + "studyLoadAGameByUrl": "Muat permainan dari URL", + "studyLoadAPositionFromFen": "Muat posisi dari FEN", + "studyLoadAGameFromPgn": "Muat permainan dari PGN", + "studyAutomatic": "Otomatis", + "studyUrlOfTheGame": "URL permainan", + "studyLoadAGameFromXOrY": "Muat permainan dari {param1} atau {param2}", + "studyCreateChapter": "Buat bab", + "studyCreateStudy": "Buat studi", + "studyEditStudy": "Ubah studi", + "studyVisibility": "Visibilitas", + "studyPublic": "Publik", + "studyUnlisted": "Tidak terdaftar", + "studyInviteOnly": "Hanya yang diundang", + "studyAllowCloning": "Perbolehkan kloning", + "studyNobody": "Tidak ada seorangpun", + "studyOnlyMe": "Hanya saya", + "studyContributors": "Kontributor", + "studyMembers": "Anggota", + "studyEveryone": "Semua orang", + "studyEnableSync": "Aktifkan sinkronisasi", + "studyYesKeepEveryoneOnTheSamePosition": "Ya: atur semua orang dalam posisi yang sama", + "studyNoLetPeopleBrowseFreely": "Tidak: Bolehkan untuk menjelajah dengan bebas", + "studyPinnedStudyComment": "Sematkan komentar studi", + "studyStart": "Mulai", + "studySave": "Simpan", + "studyClearChat": "Bersihkan obrolan", + "studyDeleteTheStudyChatHistory": "Hapus riwayat obrolan studi? Ini tidak akan dapat mengulangkan kembali!", + "studyDeleteStudy": "Hapus studi", + "studyConfirmDeleteStudy": "Hapus seluruh studi? Tidak dapat kembal lagi! Tuliskan nama studi untuk konfirmasi: {param}", + "studyWhereDoYouWantToStudyThat": "Dimana Anda ingin mempelajarinya?", + "studyGoodMove": "Langkah bagus", + "studyMistake": "Kesalahan", + "studyBrilliantMove": "Langkah Brilian", + "studyBlunder": "Blunder", + "studyInterestingMove": "Langkah menarik", + "studyDubiousMove": "Langkah meragukan", + "studyOnlyMove": "Langkah satu-satunya", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posisi imbang", + "studyUnclearPosition": "Posisi tidak jelas", + "studyWhiteIsSlightlyBetter": "Putih sedikit lebih unggul", + "studyBlackIsSlightlyBetter": "Hitam sedikit lebih unggul", + "studyWhiteIsBetter": "Putih lebih unggul", + "studyBlackIsBetter": "Hitam lebih unggul", + "studyWhiteIsWinning": "Putih menang telak", + "studyBlackIsWinning": "Hitam menang telak", + "studyNovelty": "Langkah baru", + "studyDevelopment": "Pengembangan", + "studyInitiative": "Inisiatif", + "studyAttack": "Serangan", + "studyCounterplay": "Serangan balik", + "studyTimeTrouble": "Tekanan waktu", + "studyWithCompensation": "Dengan kompensasi", + "studyWithTheIdea": "Dengan ide", + "studyNextChapter": "Bab selanjutnya", + "studyPrevChapter": "Bab sebelumnya", + "studyStudyActions": "Pembelajaran", + "studyTopics": "Topik", + "studyMyTopics": "Topik saya", + "studyPopularTopics": "Topik populer", + "studyManageTopics": "Kelola topik", + "studyBack": "Kembali", + "studyPlayAgain": "Main lagi", + "studyYouCompletedThisLesson": "Selamat. Anda telah menyelesaikan pelajaran ini.", + "studyNbChapters": "{count, plural, other{{count} Bab}}", + "studyNbGames": "{count, plural, other{{count} Permainan}}", + "studyNbMembers": "{count, plural, other{{count} Anggota}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Tempelkan PGN kamu disini, lebih dari {count} permainan}}", + "timeagoJustNow": "baru saja", + "timeagoRightNow": "sekarang", + "timeagoCompleted": "telah selesai", + "timeagoInNbSeconds": "{count, plural, other{dalam {count} detik}}", + "timeagoInNbMinutes": "{count, plural, other{dalam {count} menit}}", + "timeagoInNbHours": "{count, plural, other{dalam {count} jam}}", + "timeagoInNbDays": "{count, plural, other{dalam {count} hari}}", + "timeagoInNbWeeks": "{count, plural, other{dalam {count} minggu}}", + "timeagoInNbMonths": "{count, plural, other{dalam {count} bulan}}", + "timeagoInNbYears": "{count, plural, other{dalam {count} tahun}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} menit yang lalu}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} jam yang lalu}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} hari yang lalu}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} minggu yang lalu}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} bulan yang lalu}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} tahun yang lalu}}", + "timeagoNbMinutesRemaining": "{count, plural, other{{count} menit tersisa}}", + "timeagoNbHoursRemaining": "{count, plural, other{{count} jam tersisa}}" } \ No newline at end of file diff --git a/lib/l10n/lila_it.arb b/lib/l10n/lila_it.arb index 820667bdd2..0f01dfa7ff 100644 --- a/lib/l10n/lila_it.arb +++ b/lib/l10n/lila_it.arb @@ -1,42 +1,48 @@ { + "mobileAllGames": "Tutte le partite", + "mobileAreYouSure": "Sei sicuro?", + "mobileBlindfoldMode": "Alla cieca", + "mobileCancelTakebackOffer": "Annulla richiesta di ritiro mossa", + "mobileClearButton": "Elimina", + "mobileCorrespondenceClearSavedMove": "Cancella mossa salvata", + "mobileCustomGameJoinAGame": "Unisciti a una partita", + "mobileFeedbackButton": "Suggerimenti", + "mobileGreeting": "Ciao, {param}", + "mobileGreetingWithoutName": "Ciao", + "mobileHideVariation": "Nascondi variante", "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Tattiche", - "mobileToolsTab": "Strumenti", - "mobileWatchTab": "Guarda", - "mobileSettingsTab": "Settaggi", + "mobileLiveStreamers": "Streamer in diretta", "mobileMustBeLoggedIn": "Devi aver effettuato l'accesso per visualizzare questa pagina.", - "mobileSystemColors": "Tema app", - "mobileFeedbackButton": "Suggerimenti", + "mobileNoSearchResults": "Nessun risultato", + "mobileNotFollowingAnyUser": "Non stai seguendo nessun utente.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Giocatori con \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ingrandisci il pezzo trascinato", + "mobilePuzzleStormConfirmEndRun": "Vuoi terminare questa serie?", + "mobilePuzzleStormFilterNothingToShow": "Nessun risultato, per favore modifica i filtri", + "mobilePuzzleStormNothingToShow": "Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Risolvi il maggior numero di puzzle in tre minuti.", + "mobilePuzzleStreakAbortWarning": "Perderai la tua serie corrente e il tuo punteggio verrà salvato.", + "mobilePuzzleThemesSubtitle": ".", + "mobilePuzzlesTab": "Tattiche", + "mobileRecentSearches": "Ricerche recenti", "mobileSettingsHapticFeedback": "Feedback tattile", "mobileSettingsImmersiveMode": "Modalità immersiva", "mobileSettingsImmersiveModeSubtitle": "Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.", - "mobileNotFollowingAnyUser": "Non stai seguendo nessun utente.", - "mobileAllGames": "Tutte le partite", - "mobileRecentSearches": "Ricerche recenti", - "mobileClearButton": "Elimina", - "mobilePlayersMatchingSearchTerm": "Giocatori con \"{param}\"", - "mobileNoSearchResults": "Nessun risultato", - "mobileAreYouSure": "Sei sicuro?", - "mobilePuzzleStreakAbortWarning": "Perderai la tua serie corrente e il tuo punteggio verrà salvato.", - "mobilePuzzleStormNothingToShow": "Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.", - "mobileSharePuzzle": "Condividi questa tattica", - "mobileShareGameURL": "Condividi URL della partita", + "mobileSettingsTab": "Settaggi", "mobileShareGamePGN": "Condividi PGN", + "mobileShareGameURL": "Condividi URL della partita", "mobileSharePositionAsFEN": "Condividi posizione come FEN", - "mobileShowVariations": "Mostra varianti", - "mobileHideVariation": "Nascondi variante", + "mobileSharePuzzle": "Condividi questa tattica", "mobileShowComments": "Mostra commenti", - "mobilePuzzleStormConfirmEndRun": "Vuoi terminare questa serie?", - "mobilePuzzleStormFilterNothingToShow": "Nessun risultato, per favore modifica i filtri", - "mobileCancelTakebackOffer": "Annulla richiesta di ritiro mossa", - "mobileCancelDrawOffer": "Annulla richiesta di patta", - "mobileWaitingForOpponentToJoin": "In attesa dell'avversario...", - "mobileBlindfoldMode": "Alla cieca", - "mobileLiveStreamers": "Streamer in diretta", - "mobileCustomGameJoinAGame": "Unisciti a una partita", - "mobileCorrespondenceClearSavedMove": "Cancella mossa salvata", + "mobileShowResult": "Mostra il risultato", + "mobileShowVariations": "Mostra varianti", "mobileSomethingWentWrong": "Si è verificato un errore.", + "mobileSystemColors": "Tema app", + "mobileTheme": "Tema", + "mobileToolsTab": "Strumenti", + "mobileWaitingForOpponentToJoin": "In attesa dell'avversario...", + "mobileWatchTab": "Guarda", "activityActivity": "Attività", "activityHostedALiveStream": "Ha ospitato una diretta", "activityRankedInSwissTournament": "Classificato #{param1} in {param2}", @@ -49,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Ha giocato {count} mossa} other{Ha giocato {count} mosse}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} partita per corrispondenza} other{in {count} partite per corrispondenza}}", "activityCompletedNbGames": "{count, plural, =1{Ha concluso {count} partita per corrispondenza} other{Ha concluso {count} partite per corrispondenza}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Partita {count} {param2} per corrispondenza completata} other{Partite {count} {param2} per corrispondenza completate}}", "activityFollowedNbPlayers": "{count, plural, =1{Ha iniziato a seguire {count} giocatore} other{Ha iniziato a seguire {count} giocatori}}", "activityGainedNbFollowers": "{count, plural, =1{Ha guadagnato {count} nuovo follower} other{Ha guadagnato {count} nuovi follower}}", "activityHostedNbSimuls": "{count, plural, =1{Ha ospitato {count} esibizione simultanea} other{Ha ospitato {count} esibizioni simultanee}}", @@ -59,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha gareggiato in {count} torneo Svizzero} other{Ha gareggiato in {count} tornei Svizzeri}}", "activityJoinedNbTeams": "{count, plural, =1{Si è unito ad {count} squadra} other{Si è unito a {count} squadre}}", "broadcastBroadcasts": "Dirette", + "broadcastMyBroadcasts": "Le mie trasmissioni", "broadcastLiveBroadcasts": "Tornei in diretta", + "broadcastBroadcastCalendar": "Calendario trasmissioni", + "broadcastNewBroadcast": "Nuova diretta", + "broadcastSubscribedBroadcasts": "Trasmissioni abbonate", + "broadcastAboutBroadcasts": "Informazioni sulle trasmissioni", + "broadcastHowToUseLichessBroadcasts": "Istruzioni delle trasmissioni Lichess.", + "broadcastTheNewRoundHelp": "Il nuovo turno avrà gli stessi membri e contributori del precedente.", + "broadcastAddRound": "Aggiungi un turno", + "broadcastOngoing": "In corso", + "broadcastUpcoming": "Prossimamente", + "broadcastCompleted": "Conclusa", + "broadcastCompletedHelp": "Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine.", + "broadcastRoundName": "Nome turno", + "broadcastRoundNumber": "Turno numero", + "broadcastTournamentName": "Nome del torneo", + "broadcastTournamentDescription": "Breve descrizione dell'evento", + "broadcastFullDescription": "Descrizione completa dell'evento", + "broadcastFullDescriptionHelp": "(Facoltativo) Descrizione completa dell'evento. {param1} è disponibile. La lunghezza deve essere inferiore a {param2} caratteri.", + "broadcastSourceSingleUrl": "Sorgente URL PGN", + "broadcastSourceUrlHelp": "L'URL che Lichess utilizzerà per ottenere gli aggiornamenti dei PGN. Deve essere accessibile pubblicamente su Internet.", + "broadcastSourceGameIds": "Fino a 64 ID di partite Lichess, separati da spazi.", + "broadcastStartDateTimeZone": "Data d'inizio nel fuso orario locale del torneo: {param}", + "broadcastStartDateHelp": "Facoltativo, se sai quando inizia l'evento", + "broadcastCurrentGameUrl": "URL della partita corrente", + "broadcastDownloadAllRounds": "Scarica tutti i round", + "broadcastResetRound": "Reimposta questo turno", + "broadcastDeleteRound": "Elimina questo turno", + "broadcastDefinitivelyDeleteRound": "Elimina definitivamente il turno e le sue partite.", + "broadcastDeleteAllGamesOfThisRound": "Elimina tutte le partite di questo turno. L'emittente dovrà essere attiva per poterli ricreare.", + "broadcastEditRoundStudy": "Modifica lo studio del turno", + "broadcastDeleteTournament": "Elimina questo torneo", + "broadcastDefinitivelyDeleteTournament": "Elimina definitivamente l'intero torneo, tutti i turni e tutte le partite.", + "broadcastShowScores": "Mostra i punteggi dei giocatori in base ai risultati del gioco", + "broadcastReplacePlayerTags": "Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli", + "broadcastFideFederations": "Federazioni FIDE", + "broadcastTop10Rating": "Migliori 10 punteggi", + "broadcastFidePlayers": "Giocatori FIDE", + "broadcastFidePlayerNotFound": "Giocatore FIDE non trovato", + "broadcastFideProfile": "Profilo FIDE", + "broadcastFederation": "Federazione", + "broadcastAgeThisYear": "Età quest'anno", + "broadcastUnrated": "Non classificato", + "broadcastRecentTournaments": "Tornei recenti", + "broadcastOpenLichess": "Apri con Lichess", + "broadcastTeams": "Squadre", + "broadcastBoards": "Scacchiere", + "broadcastOverview": "Panoramica", + "broadcastSubscribeTitle": "Iscriviti per ricevere notifiche sull'inizio di ogni round. Puoi attivare o disattivare la campanella o le notifiche push per le dirette nelle preferenze del tuo account.", + "broadcastUploadImage": "Carica immagine del torneo", + "broadcastNoBoardsYet": "Non sono ancora presenti scacchiere. Esse compariranno non appena i giochi saranno stati caricati.", + "broadcastBoardsCanBeLoaded": "Le scacchiere possono essere caricate con una sorgente o tramite {param}", + "broadcastStartsAfter": "Inizia tra {param}", + "broadcastStartVerySoon": "Questa trasmissione inizierà a breve.", + "broadcastNotYetStarted": "Questa trasmissione non è ancora cominciata.", + "broadcastOfficialWebsite": "Sito web ufficiale", + "broadcastStandings": "Classifica", + "broadcastOfficialStandings": "Classifica Ufficiale", + "broadcastIframeHelp": "Altre opzioni si trovano nella {param}", + "broadcastWebmastersPage": "pagina dei gestori web", + "broadcastPgnSourceHelp": "Una sorgente PGN pubblica per questo round. Viene offerta anche un'{param} per una sincronizzazione più rapida ed efficiente.", + "broadcastEmbedThisBroadcast": "Incorpora questa trasmissione nel tuo sito web", + "broadcastEmbedThisRound": "Incorpora {param} nel tuo sito web", + "broadcastRatingDiff": "Differenza di punteggio", + "broadcastGamesThisTournament": "Partite in questo torneo", + "broadcastScore": "Punteggio", + "broadcastAllTeams": "Tutte le squadre", + "broadcastTournamentFormat": "Formato del torneo", + "broadcastTournamentLocation": "Luogo del Torneo", + "broadcastTopPlayers": "Giocatori migliori", + "broadcastTimezone": "Fuso orario", + "broadcastFideRatingCategory": "Categoria di punteggio FIDE", + "broadcastOptionalDetails": "Dettagli facoltativi", + "broadcastPastBroadcasts": "Trasmissioni precedenti", + "broadcastAllBroadcastsByMonth": "Visualizza tutte le trasmissioni per mese", + "broadcastNbBroadcasts": "{count, plural, =1{{count} diretta} other{{count} dirette}}", "challengeChallengesX": "Sfide: {param1}", "challengeChallengeToPlay": "Sfida a una partita", "challengeChallengeDeclined": "Sfida rifiutata", @@ -183,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Tono notifica", + "preferencesBlindfold": "Alla cieca", "puzzlePuzzles": "Problemi", "puzzlePuzzleThemes": "Problemi a tema", "puzzleRecommended": "Consigliati", @@ -378,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Un pezzo che attacca o difende una casa attraverso un pezzo nemico.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "L'avversario è limitato nella sua scelta della mossa, e tutte le mosse possibili peggiorano la sua posizione.", - "puzzleThemeHealthyMix": "Mix generale", - "puzzleThemeHealthyMixDescription": "Un po' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.", + "puzzleThemeMix": "Mix generale", + "puzzleThemeMixDescription": "Un po' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.", "puzzleThemePlayerGames": "Partite tra giocatori", "puzzleThemePlayerGamesDescription": "Trova problemi tratti dalle tue partite o dalle partite di altri giocatori.", "puzzleThemePuzzleDownloadInformation": "Questi problemi sono nel pubblico dominio e possono essere scaricati da {param}.", @@ -500,7 +583,6 @@ "replayMode": "Modalità replay", "realtimeReplay": "In tempo reale", "byCPL": "Per CPL", - "openStudy": "Apri studio", "enable": "Abilita", "bestMoveArrow": "Freccia per la mossa migliore", "showVariationArrows": "Mostra le frecce delle varianti", @@ -510,7 +592,6 @@ "memory": "Memoria", "infiniteAnalysis": "Analisi infinita", "removesTheDepthLimit": "Rimuove il limite di profondità di analisi, ma può surriscaldare il tuo computer", - "engineManager": "Gestore del motore", "blunder": "Errore grave", "mistake": "Errore", "inaccuracy": "Imprecisione", @@ -536,6 +617,7 @@ "latestForumPosts": "Ultimi interventi nel forum", "players": "Giocatori", "friends": "Amici", + "otherPlayers": "altri giocatori", "discussions": "Conversazioni", "today": "Oggi", "yesterday": "Ieri", @@ -591,6 +673,7 @@ "rank": "Posizione", "rankX": "Posizione: {param}", "gamesPlayed": "Partite giocate", + "ok": "OK", "cancel": "Annulla", "whiteTimeOut": "Il bianco ha esaurito il tempo", "blackTimeOut": "Il nero ha esaurito il tempo", @@ -707,7 +790,6 @@ "block": "Blocca", "blocked": "Bloccato", "unblock": "Sblocca", - "followsYou": "Ti segue", "xStartedFollowingY": "{param1} ha iniziato a seguire {param2}", "more": "Altro", "memberSince": "Membro dal", @@ -793,6 +875,8 @@ "descPrivateHelp": "Il testo che solo i membri del team vedranno. Se impostato sostituisce la descrizione pubblica per i membri del team.", "no": "No", "yes": "Sì", + "website": "Sito", + "mobile": "Cellulare", "help": "Aiuto:", "createANewTopic": "Crea una nuova discussione", "topics": "Discussioni", @@ -811,7 +895,9 @@ "cheat": "Imbrogli", "troll": "Provocazioni", "other": "Altro", - "reportDescriptionHelp": "Incolla il link della partita/e e spiega cosa non va con questo giocatore. Non dire soltanto \"ha imbrogliato\", ma specifica come sei arrivato a questa conclusione. Il tuo report verrà processato più velocemente se scritto in lingua inglese.", + "reportCheatBoostHelp": "Incolla il link della partita(o partite) e spiega cosa non va sul comportamento di questo utente. Non dire solamente \"ha barato\", ma invece dici come sei arrivato a questa conclusione.", + "reportUsernameHelp": "Spiegaci cosa vi è di offensivo in questo nome utente. Non dire solamente \"è offensivo/inappopriato\", ma invece dici come sei arrivato a questa conclusione, soprattutto se l'insulto è offuscato, non in inglese, in linguaggio giovanile, oppure se è un riferimento storico/culturale.", + "reportProcessedFasterInEnglish": "La tua segnalazione sarà processata più velocemente se scritta in Inglese.", "error_provideOneCheatedGameLink": "Si prega di fornire almeno un collegamento link di una partita in cui il giocatore ha imbrogliato.", "by": "di {param}", "importedByX": "Importato da {param}", @@ -1209,6 +1295,7 @@ "showMeEverything": "Mostra tutto", "lichessPatronInfo": "Lichess è un software open source completamente gratuito e libero\nTutti i costi operativi, lo sviluppo e i contenuti sono finanziati esclusivamente dalle donazioni degli utenti.", "nothingToSeeHere": "Niente da vedere qui al momento.", + "stats": "Statistiche", "opponentLeftCounter": "{count, plural, =1{Il tuo avversario ha lasciato la partita. Puoi reclamare la vittoria fra {count} secondo.} other{Il tuo avversario ha lasciato la partita. Puoi reclamare la vittoria fra {count} secondi.}}", "mateInXHalfMoves": "{count, plural, =1{Matto in {count} semi-mossa} other{Matto in {count} semi-mosse}}", "nbBlunders": "{count, plural, =1{{count} errore grave} other{{count} errori gravi}}", @@ -1305,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 tentativo} other{{count} tentativi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ha fatto un tentativo di {param2}} other{Ha fatto {count} tentativi di {param2}}}", "streamerLichessStreamers": "Lichess streamer", + "studyPrivate": "Privato", + "studyMyStudies": "I miei studi", + "studyStudiesIContributeTo": "Studi a cui collaboro", + "studyMyPublicStudies": "I miei studi pubblici", + "studyMyPrivateStudies": "I miei studi privati", + "studyMyFavoriteStudies": "I miei studi preferiti", + "studyWhatAreStudies": "Cosa sono gli \"studi\"?", + "studyAllStudies": "Tutti gli studi", + "studyStudiesCreatedByX": "Studi creati da {param}", + "studyNoneYet": "Vuoto.", + "studyHot": "Hot", + "studyDateAddedNewest": "Data di pubblicazione (dalla più recente)", + "studyDateAddedOldest": "Data di pubblicazione (dalla meno recente)", + "studyRecentlyUpdated": "Data di aggiornamento (dalla più recente)", + "studyMostPopular": "Più popolari", + "studyAlphabetical": "Alfabetico", + "studyAddNewChapter": "Aggiungi un nuovo capitolo", + "studyAddMembers": "Aggiungi membri", + "studyInviteToTheStudy": "Invita allo studio", + "studyPleaseOnlyInvitePeopleYouKnow": "Invita solo persone che conosci e che desiderano partecipare attivamente a questo studio.", + "studySearchByUsername": "Cerca per nome utente", + "studySpectator": "Spettatore", + "studyContributor": "Partecipante", + "studyKick": "Espelli", + "studyLeaveTheStudy": "Abbandona lo studio", + "studyYouAreNowAContributor": "Ora sei un partecipante", + "studyYouAreNowASpectator": "Ora sei uno spettatore", + "studyPgnTags": "Tag PGN", + "studyLike": "Mi piace", + "studyUnlike": "Non mi Piace", + "studyNewTag": "Nuovo tag", + "studyCommentThisPosition": "Commenta questa posizione", + "studyCommentThisMove": "Commenta questa mossa", + "studyAnnotateWithGlyphs": "Commenta con segni convenzionali", + "studyTheChapterIsTooShortToBeAnalysed": "Il capitolo è troppo breve per essere analizzato.", + "studyOnlyContributorsCanRequestAnalysis": "Solo i partecipanti allo studio possono richiedere un'analisi del computer.", + "studyGetAFullComputerAnalysis": "Richiedi un'analisi completa del computer della variante principale.", + "studyMakeSureTheChapterIsComplete": "Assicurati che il capitolo sia completo. Puoi richiedere l'analisi solo una volta.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tutti i membri in SYNC rimangono sulla stessa posizione", + "studyShareChanges": "Condividi le modifiche con gli spettatori e salvale sul server", + "studyPlaying": "In corso", + "studyShowEvalBar": "Barre di valutazione", + "studyFirst": "Primo", + "studyPrevious": "Precedente", + "studyNext": "Successivo", + "studyLast": "Ultimo", "studyShareAndExport": "Condividi & esporta", - "studyStart": "Inizia" + "studyCloneStudy": "Duplica", + "studyStudyPgn": "PGN dello studio", + "studyDownloadAllGames": "Scarica tutte le partite", + "studyChapterPgn": "PGN del capitolo", + "studyCopyChapterPgn": "Copia in PGN", + "studyDownloadGame": "Scarica partita", + "studyStudyUrl": "URL dello studio", + "studyCurrentChapterUrl": "URL del capitolo corrente", + "studyYouCanPasteThisInTheForumToEmbed": "Puoi incollare questo URL nel forum per creare un rimando", + "studyStartAtInitialPosition": "Inizia dalla prima mossa", + "studyStartAtX": "Inizia a: {param}", + "studyEmbedInYourWebsite": "Incorpora nel tuo sito Web o Blog", + "studyReadMoreAboutEmbedding": "Per saperne di più su come incorporare", + "studyOnlyPublicStudiesCanBeEmbedded": "Solo gli studi pubblici possono essere incorporati!", + "studyOpen": "Apri", + "studyXBroughtToYouByY": "{param1} fornito da {param2}", + "studyStudyNotFound": "Studio non trovato", + "studyEditChapter": "Modifica il capitolo", + "studyNewChapter": "Nuovo capitolo", + "studyImportFromChapterX": "Importa da {param}", + "studyOrientation": "Orientamento", + "studyAnalysisMode": "Modalità analisi", + "studyPinnedChapterComment": "Commento del capitolo", + "studySaveChapter": "Salva capitolo", + "studyClearAnnotations": "Cancella annotazioni", + "studyClearVariations": "Elimina le varianti", + "studyDeleteChapter": "Elimina capitolo", + "studyDeleteThisChapter": "Vuoi davvero eliminare questo capitolo? Sarà perso per sempre!", + "studyClearAllCommentsInThisChapter": "Cancellare tutti i commenti, le annotazioni e i disegni in questo capitolo?", + "studyRightUnderTheBoard": "Sotto la scacchiera", + "studyNoPinnedComment": "Nessun commento", + "studyNormalAnalysis": "Analisi normale", + "studyHideNextMoves": "Nascondi le mosse successive", + "studyInteractiveLesson": "Lezione interattiva", + "studyChapterX": "Capitolo {param}", + "studyEmpty": "Semplice", + "studyStartFromInitialPosition": "Parti dalla posizione iniziale", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Inizia da una posizione personalizzata", + "studyLoadAGameByUrl": "Carica una partita da URL", + "studyLoadAPositionFromFen": "Carica una posizione da FEN", + "studyLoadAGameFromPgn": "Carica una partita da PGN", + "studyAutomatic": "Automatica", + "studyUrlOfTheGame": "URL della partita", + "studyLoadAGameFromXOrY": "Carica una partita da {param1} o {param2}", + "studyCreateChapter": "Crea capitolo", + "studyCreateStudy": "Crea studio", + "studyEditStudy": "Modifica studio", + "studyVisibility": "Visibilità", + "studyPublic": "Pubblico", + "studyUnlisted": "Non elencato", + "studyInviteOnly": "Solo su invito", + "studyAllowCloning": "Permetti la clonazione", + "studyNobody": "Nessuno", + "studyOnlyMe": "Solo io", + "studyContributors": "Collaboratori", + "studyMembers": "Membri", + "studyEveryone": "Tutti", + "studyEnableSync": "Abilita sincronizzazione", + "studyYesKeepEveryoneOnTheSamePosition": "Sì: tutti vedranno la stessa posizione", + "studyNoLetPeopleBrowseFreely": "No: ognuno potrà scorrere i capitoli indipendentemente", + "studyPinnedStudyComment": "Commento dello studio", + "studyStart": "Inizia", + "studySave": "Salva", + "studyClearChat": "Cancella chat", + "studyDeleteTheStudyChatHistory": "Vuoi davvero eliminare la cronologia della chat? Sarà persa per sempre!", + "studyDeleteStudy": "Elimina studio", + "studyConfirmDeleteStudy": "Eliminare l'intero studio? Non sarà possibile annullare l'operazione! Digitare il nome dello studio per confermare: {param}", + "studyWhereDoYouWantToStudyThat": "Dove vuoi creare lo studio?", + "studyGoodMove": "Bella mossa", + "studyMistake": "Errore", + "studyBrilliantMove": "Mossa geniale", + "studyBlunder": "Errore grave", + "studyInterestingMove": "Mossa interessante", + "studyDubiousMove": "Mossa dubbia", + "studyOnlyMove": "Unica mossa", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posizione equivalente", + "studyUnclearPosition": "Posizione non chiara", + "studyWhiteIsSlightlyBetter": "Il bianco è in lieve vantaggio", + "studyBlackIsSlightlyBetter": "Il nero è in lieve vantaggio", + "studyWhiteIsBetter": "Il bianco è in vantaggio", + "studyBlackIsBetter": "Il nero è in vantaggio", + "studyWhiteIsWinning": "Il bianco sta vincendo", + "studyBlackIsWinning": "Il nero sta vincendo", + "studyNovelty": "Novità", + "studyDevelopment": "Sviluppo", + "studyInitiative": "Iniziativa", + "studyAttack": "Attacco", + "studyCounterplay": "Contrattacco", + "studyTimeTrouble": "Prolemi di tempo", + "studyWithCompensation": "Con compenso", + "studyWithTheIdea": "Con l'idea", + "studyNextChapter": "Prossimo capitolo", + "studyPrevChapter": "Capitolo precedente", + "studyStudyActions": "Studia azioni", + "studyTopics": "Discussioni", + "studyMyTopics": "Le mie discussioni", + "studyPopularTopics": "Argomenti popolari", + "studyManageTopics": "Gestisci discussioni", + "studyBack": "Indietro", + "studyPlayAgain": "Gioca di nuovo", + "studyWhatWouldYouPlay": "Cosa giocheresti in questa posizione?", + "studyYouCompletedThisLesson": "Congratulazioni! Hai completato questa lezione.", + "studyPerPage": "{param} per pagina", + "studyNbChapters": "{count, plural, =1{{count} capitolo} other{{count} capitoli}}", + "studyNbGames": "{count, plural, =1{{count} partita} other{{count} partite}}", + "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membri}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Incolla qui il testo PGN, massimo {count} partita} other{Incolla qui i testi PGN, massimo {count} partite}}", + "timeagoJustNow": "adesso", + "timeagoRightNow": "adesso", + "timeagoCompleted": "completato", + "timeagoInNbSeconds": "{count, plural, =1{tra {count} secondo} other{tra {count} secondi}}", + "timeagoInNbMinutes": "{count, plural, =1{tra {count} minuto} other{tra {count} minuti}}", + "timeagoInNbHours": "{count, plural, =1{tra {count} ora} other{tra {count} ore}}", + "timeagoInNbDays": "{count, plural, =1{tra {count} giorno} other{tra {count} giorni}}", + "timeagoInNbWeeks": "{count, plural, =1{tra {count} settimana} other{tra {count} settimane}}", + "timeagoInNbMonths": "{count, plural, =1{tra {count} mese} other{tra {count} mesi}}", + "timeagoInNbYears": "{count, plural, =1{tra {count} anno} other{tra {count} anni}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuto fa} other{{count} minuti fa}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ora fa} other{{count} ore fa}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} giorno fa} other{{count} giorni fa}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} settimana fa} other{{count} settimane fa}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} mese fa} other{{count} mesi fa}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} anno fa} other{{count} anni fa}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto rimanente} other{{count} minuti rimanenti}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ora rimanente} other{{count} ore rimanenti}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ja.arb b/lib/l10n/lila_ja.arb index 8e717358c8..2754e0ec51 100644 --- a/lib/l10n/lila_ja.arb +++ b/lib/l10n/lila_ja.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "すべて", + "mobileAreYouSure": "本当にいいですか?", + "mobileBlindfoldMode": "めかくしモード", + "mobileCancelTakebackOffer": "待ったをキャンセル", + "mobileClearButton": "クリア", + "mobileCorrespondenceClearSavedMove": "保存した手を削除", + "mobileCustomGameJoinAGame": "ゲームに参加", + "mobileFeedbackButton": "フィードバック", + "mobileGreeting": "こんにちは {param} さん", + "mobileGreetingWithoutName": "こんにちは", + "mobileHideVariation": "変化手順を隠す", "mobileHomeTab": "ホーム", - "mobilePuzzlesTab": "問題", - "mobileToolsTab": "ツール", - "mobileWatchTab": "見る", - "mobileSettingsTab": "設定", + "mobileLiveStreamers": "ライブ配信者", "mobileMustBeLoggedIn": "このページを見るにはログインが必要です。", - "mobileSystemColors": "OS と同じ色設定", - "mobileFeedbackButton": "フィードバック", + "mobileNoSearchResults": "検索結果なし", + "mobileNotFollowingAnyUser": "誰もフォローしていません。", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "「{param}」を含むプレイヤー", + "mobilePrefMagnifyDraggedPiece": "ドラッグ中の駒を拡大", + "mobilePuzzleStormConfirmEndRun": "このストームを終了しますか?", + "mobilePuzzleStormFilterNothingToShow": "条件に合う問題がありません。フィルターを変更してください", + "mobilePuzzleStormNothingToShow": "データがありません。まず問題ストームをプレイして。", + "mobilePuzzleStormSubtitle": "3 分間でできるだけ多くの問題を解いてください。", + "mobilePuzzleStreakAbortWarning": "現在の連続正解が終わり、スコアが保存されます。", + "mobilePuzzleThemesSubtitle": "お気に入りのオープニングやテーマの問題が選べます。", + "mobilePuzzlesTab": "問題", + "mobileRecentSearches": "最近の検索", "mobileSettingsHapticFeedback": "振動フィードバック", "mobileSettingsImmersiveMode": "没入モード", "mobileSettingsImmersiveModeSubtitle": "対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。", - "mobileNotFollowingAnyUser": "誰もフォローしていません。", - "mobileAllGames": "すべて", - "mobileRecentSearches": "最近の検索", - "mobileClearButton": "クリア", - "mobilePlayersMatchingSearchTerm": "「{param}」を含むプレイヤー", - "mobileNoSearchResults": "検索結果なし", - "mobileAreYouSure": "本当にいいですか?", - "mobilePuzzleStreakAbortWarning": "現在の連続正解が終わり、スコアが保存されます。", - "mobilePuzzleStormNothingToShow": "データがありません。まず問題ストームをプレイして。", - "mobileSharePuzzle": "この問題を共有する", - "mobileShareGameURL": "ゲーム URLを共有", + "mobileSettingsTab": "設定", "mobileShareGamePGN": "PGN を共有", + "mobileShareGameURL": "ゲーム URLを共有", "mobileSharePositionAsFEN": "局面を FEN で共有", - "mobileShowVariations": "変化手順を表示", - "mobileHideVariation": "変化手順を隠す", + "mobileSharePuzzle": "この問題を共有する", "mobileShowComments": "コメントを表示", - "mobilePuzzleStormConfirmEndRun": "このストームを終了しますか?", - "mobilePuzzleStormFilterNothingToShow": "条件に合う問題がありません。フィルターを変更してください", - "mobileCancelTakebackOffer": "待ったをキャンセル", - "mobileCancelDrawOffer": "ドロー提案をキャンセル", - "mobileWaitingForOpponentToJoin": "対戦相手の参加を待っています…", - "mobileBlindfoldMode": "めかくしモード", - "mobileLiveStreamers": "ライブ配信者", - "mobileCustomGameJoinAGame": "ゲームに参加", - "mobileCorrespondenceClearSavedMove": "保存した手を削除", - "mobileSomethingWentWrong": "問題が発生しました。", "mobileShowResult": "結果を表示", - "mobilePuzzleThemesSubtitle": "お気に入りのオープニングやテーマの問題が選べます。", - "mobilePuzzleStormSubtitle": "3 分間でできるだけ多くの問題を解いてください。", - "mobileGreeting": "こんにちは {param} さん", - "mobileGreetingWithoutName": "こんにちは", + "mobileShowVariations": "変化手順を表示", + "mobileSomethingWentWrong": "問題が発生しました。", + "mobileSystemColors": "OS と同じ色設定", + "mobileToolsTab": "ツール", + "mobileWaitingForOpponentToJoin": "対戦相手の参加を待っています…", + "mobileWatchTab": "見る", "activityActivity": "活動", "activityHostedALiveStream": "ライブ配信", "activityRankedInSwissTournament": "{param1} 位({param2})", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, other{{count} 手をプレイ}}", "activityInNbCorrespondenceGames": "{count, plural, other{(通信戦 {count} 局で)}}", "activityCompletedNbGames": "{count, plural, other{{count} 局の通信戦を完了}}", + "activityCompletedNbVariantGames": "{count, plural, other{{count} 局の {param2} 通信戦を完了しました}}", "activityFollowedNbPlayers": "{count, plural, other{{count} 人をフォロー開始}}", "activityGainedNbFollowers": "{count, plural, other{{count} 人の新規フォロワーを獲得}}", "activityHostedNbSimuls": "{count, plural, other{{count} 回の同時対局を主催}}", @@ -64,7 +65,81 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{{count} 回のスイス式トーナメントに参加}}", "activityJoinedNbTeams": "{count, plural, other{{count} チームに参加}}", "broadcastBroadcasts": "イベント中継", + "broadcastMyBroadcasts": "自分の配信", "broadcastLiveBroadcasts": "実戦トーナメントのライブ中継", + "broadcastBroadcastCalendar": "中継カレンダー", + "broadcastNewBroadcast": "新しいライブ中継", + "broadcastSubscribedBroadcasts": "登録した配信", + "broadcastAboutBroadcasts": "中継について", + "broadcastHowToUseLichessBroadcasts": "Lichess 中継の使い方。", + "broadcastTheNewRoundHelp": "新ラウンドには前回と同じメンバーと投稿者が参加します。", + "broadcastAddRound": "ラウンドを追加", + "broadcastOngoing": "配信中", + "broadcastUpcoming": "予定", + "broadcastCompleted": "終了", + "broadcastCompletedHelp": "Lichess は元になる対局に基づいてラウンド終了を検出します。元になる対局がない時はこのトグルを使ってください。", + "broadcastRoundName": "ラウンド名", + "broadcastRoundNumber": "ラウンド", + "broadcastTournamentName": "大会名", + "broadcastTournamentDescription": "大会の短い説明", + "broadcastFullDescription": "長い説明", + "broadcastFullDescriptionHelp": "内容の詳しい説明(オプション)。{param1} が利用できます。長さは [欧文換算で] {param2} 字まで。", + "broadcastSourceSingleUrl": "PGN のソース URL", + "broadcastSourceUrlHelp": "Lichess が PGN を取得するための URL。インターネット上に公表されているもののみ。", + "broadcastSourceGameIds": "Lichess ゲーム ID、半角スペースで区切って最大 64 個まで。", + "broadcastStartDateHelp": "イベント開始時刻(オプション)", + "broadcastCurrentGameUrl": "現在のゲームの URL", + "broadcastDownloadAllRounds": "全ラウンドをダウンロード", + "broadcastResetRound": "このラウンドをリセット", + "broadcastDeleteRound": "このラウンドを削除", + "broadcastDefinitivelyDeleteRound": "このラウンドのゲームをすべて削除する。", + "broadcastDeleteAllGamesOfThisRound": "このラウンドのすべてのゲームを削除します。復活させるには情報源がアクティブでなくてはなりません。", + "broadcastEditRoundStudy": "ラウンドの研究を編集", + "broadcastDeleteTournament": "このトーナメントを削除", + "broadcastDefinitivelyDeleteTournament": "トーナメント全体(全ラウンド、全ゲーム)を削除する。", + "broadcastShowScores": "ゲーム結果に応じてプレイヤーのスコアを表示", + "broadcastReplacePlayerTags": "オプション:プレイヤーの名前、レーティング、タイトルの変更", + "broadcastFideFederations": "FIDE 加盟協会", + "broadcastTop10Rating": "レーティング トップ10", + "broadcastFidePlayers": "FIDE 選手", + "broadcastFidePlayerNotFound": "FIDE 選手が見つかりません", + "broadcastFideProfile": "FIDE プロフィール", + "broadcastFederation": "所属協会", + "broadcastAgeThisYear": "今年時点の年齢", + "broadcastUnrated": "レーティングなし", + "broadcastRecentTournaments": "最近のトーナメント", + "broadcastOpenLichess": "Lichess で開く", + "broadcastTeams": "チーム", + "broadcastBoards": "ボード", + "broadcastOverview": "概要", + "broadcastSubscribeTitle": "登録しておくと各ラウンドの開始時に通知が来ます。アカウント設定でベルやプッシュ通知の切り替えができます。", + "broadcastUploadImage": "トーナメントの画像をアップロード", + "broadcastNoBoardsYet": "ボードはまだありません。棋譜がアップロードされると表示されます。", + "broadcastBoardsCanBeLoaded": "ボードはソースまたは {param} 経由で読み込めます", + "broadcastStartsAfter": "{param} 後に開始", + "broadcastStartVerySoon": "中継はまもなく始まります。", + "broadcastNotYetStarted": "中継はまだ始まっていません。", + "broadcastOfficialWebsite": "公式サイト", + "broadcastStandings": "順位", + "broadcastOfficialStandings": "公式順位", + "broadcastIframeHelp": "他のオプションは {param} にあります", + "broadcastWebmastersPage": "ウェブ管理者のページ", + "broadcastPgnSourceHelp": "このラウンドについて公表されたリアルタイムの PGN です。{param} も利用でき、高速かつ高効率の同期が行なえます。", + "broadcastEmbedThisBroadcast": "この中継をウェブサイトに埋め込む", + "broadcastEmbedThisRound": "{param} をウェブサイトに埋め込む", + "broadcastRatingDiff": "レーティングの差", + "broadcastGamesThisTournament": "このトーナメントの対局", + "broadcastScore": "スコア", + "broadcastAllTeams": "すべてのチーム", + "broadcastTournamentFormat": "トーナメント形式", + "broadcastTournamentLocation": "開催地", + "broadcastTopPlayers": "トッププレイヤー", + "broadcastTimezone": "タイムゾーン", + "broadcastFideRatingCategory": "FIDE レーティング カテゴリー", + "broadcastOptionalDetails": "その他詳細(オプション)", + "broadcastPastBroadcasts": "過去の中継", + "broadcastAllBroadcastsByMonth": "すべての中継を月別に表示", + "broadcastNbBroadcasts": "{count, plural, other{{count} ブロードキャスト}}", "challengeChallengesX": "チャレンジ:{param1}", "challengeChallengeToPlay": "対局を申し込む", "challengeChallengeDeclined": "挑戦が拒否されました。", @@ -188,6 +263,7 @@ "preferencesNotifyWeb": "ブラウザ", "preferencesNotifyDevice": "デバイス", "preferencesBellNotificationSound": "ベル通知の音", + "preferencesBlindfold": "めかくしモード", "puzzlePuzzles": "タクティクス問題", "puzzlePuzzleThemes": "問題のテーマ", "puzzleRecommended": "おすすめ", @@ -383,8 +459,8 @@ "puzzleThemeXRayAttackDescription": "相手の駒の向こうにあるマスを間接的に攻撃(または防御)する。", "puzzleThemeZugzwang": "ツークツワンク", "puzzleThemeZugzwangDescription": "相手の指せる手が、どれを選んでも局面を悪くしてしまう形。", - "puzzleThemeHealthyMix": "混合", - "puzzleThemeHealthyMixDescription": "いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。", + "puzzleThemeMix": "混合", + "puzzleThemeMixDescription": "いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。", "puzzleThemePlayerGames": "プレイヤーの対局", "puzzleThemePlayerGamesDescription": "自分の対局、他のプレイヤーの対局から取られた問題を検索します。", "puzzleThemePuzzleDownloadInformation": "これらの問題はパブリックドメインにあり、{param} でダウンロードできます。", @@ -505,7 +581,6 @@ "replayMode": "再現の方式", "realtimeReplay": "リアルタイム", "byCPL": "評価値で", - "openStudy": "研究を開く", "enable": "解析する", "bestMoveArrow": "最善手を表示", "showVariationArrows": "変化手順の矢印を表示", @@ -515,7 +590,6 @@ "memory": "メモリ", "infiniteAnalysis": "無限解析", "removesTheDepthLimit": "探索手数の制限をなくし最大限の解析を行なう", - "engineManager": "解析エンジンの管理", "blunder": "大悪手", "mistake": "悪手", "inaccuracy": "疑問手", @@ -597,6 +671,7 @@ "rank": "ランク", "rankX": "ランク: {param}", "gamesPlayed": "対局数", + "ok": "OK", "cancel": "キャンセル", "whiteTimeOut": "白時間切れ", "blackTimeOut": "黒時間切れ", @@ -713,7 +788,6 @@ "block": "ブロックする", "blocked": "ブロック済", "unblock": "ブロックを外す", - "followsYou": "あなたをフォローしています", "xStartedFollowingY": "{param1} が {param2} のフォローを開始", "more": "さらに表示", "memberSince": "登録日", @@ -819,7 +893,9 @@ "cheat": "ソフト使用", "troll": "荒らし", "other": "その他", - "reportDescriptionHelp": "問題のゲームへのリンクを貼って、相手ユーザーの問題点を説明してください。ただ「イカサマだ」と言うのではなく、なぜそう思うか理由を書いてください。英語で書くと対応が早くできます。", + "reportCheatBoostHelp": "ゲームへのリンクを張って、このユーザーの行動のどこが問題かを説明してください。ただ「チート」と言うのではなく、あなたがなぜそう思ったのか教えてください。", + "reportUsernameHelp": "このユーザー名のどこが攻撃的かを説明してください。ただ「攻撃的」「不適切」と言うのではなく、あなたがなぜそう思ったのか教えてください。中でも綴りの変更、英語以外の言語、俗語、歴史・文化的要因に関係した場合は特に説明が必要です。", + "reportProcessedFasterInEnglish": "英語で書いていただくと通報への対応が早くなります。", "error_provideOneCheatedGameLink": "不正のあった対局 1 局以上へのリンクを添えてください。", "by": "{param} によって", "importedByX": "{param} がインポート", @@ -1216,6 +1292,7 @@ "showMeEverything": "すべてを表示", "lichessPatronInfo": "Lichess は非営利組織であり、完全に無料/自由なオープンソースソフトウェアです。\n運営費、開発、コンテンツを支えているのはすべてユーザーの寄付です。", "nothingToSeeHere": "今は何もありません。", + "stats": "統計", "opponentLeftCounter": "{count, plural, other{相手がいなくなりました。後 {count} 秒で勝ちにできます。}}", "mateInXHalfMoves": "{count, plural, other{{count} プライでメイト}}", "nbBlunders": "{count, plural, other{{count} 大悪手}}", @@ -1312,6 +1389,178 @@ "stormXRuns": "{count, plural, other{{count} 回}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{{param2} を {count} 回プレイ}}", "streamerLichessStreamers": "Lichess 配信者", + "studyPrivate": "非公開", + "studyMyStudies": "自分の研究", + "studyStudiesIContributeTo": "参加した研究", + "studyMyPublicStudies": "自分の公開研究", + "studyMyPrivateStudies": "自分の非公開研究", + "studyMyFavoriteStudies": "お気に入りの研究", + "studyWhatAreStudies": "研究(study)とは?", + "studyAllStudies": "すべての研究", + "studyStudiesCreatedByX": "{param} による研究", + "studyNoneYet": "まだなし", + "studyHot": "注目", + "studyDateAddedNewest": "投稿日(新しい順)", + "studyDateAddedOldest": "投稿日(古い順)", + "studyRecentlyUpdated": "更新順", + "studyMostPopular": "人気順", + "studyAlphabetical": "アルファベット順", + "studyAddNewChapter": "新たな章を追加", + "studyAddMembers": "メンバーを追加する", + "studyInviteToTheStudy": "この研究に招待する", + "studyPleaseOnlyInvitePeopleYouKnow": "招待する相手は、あなたが知っていて参加したい人だけにしてください。", + "studySearchByUsername": "ユーザー名で検索", + "studySpectator": "観戦者", + "studyContributor": "投稿参加者", + "studyKick": "追放", + "studyLeaveTheStudy": "この研究から出る", + "studyYouAreNowAContributor": "投稿参加者になりました", + "studyYouAreNowASpectator": "観戦者になりました", + "studyPgnTags": "PGN タグ", + "studyLike": "いいね", + "studyUnlike": "いいね解除", + "studyNewTag": "新しいタグ", + "studyCommentThisPosition": "この局面にコメントする", + "studyCommentThisMove": "この手にコメント", + "studyAnnotateWithGlyphs": "解説記号を入れる", + "studyTheChapterIsTooShortToBeAnalysed": "章が短すぎて解析できません。", + "studyOnlyContributorsCanRequestAnalysis": "コンピュータ解析を要請できるのは投稿参加者だけです。", + "studyGetAFullComputerAnalysis": "主手順についてサーバ上でのコンピュータ解析を行なう。", + "studyMakeSureTheChapterIsComplete": "章が完成したか確認してください。解析の要請は 1 回だけです。", + "studyAllSyncMembersRemainOnTheSamePosition": "同期したメンバーは同じ局面に留まります", + "studyShareChanges": "変更を観戦者と共有し、サーバに保存する", + "studyPlaying": "プレイ中", + "studyShowEvalBar": "評価値バー", + "studyFirst": "最初", + "studyPrevious": "前", + "studyNext": "次", + "studyLast": "最後", "studyShareAndExport": "共有とエクスポート", - "studyStart": "開始" + "studyCloneStudy": "研究をコピー", + "studyStudyPgn": "研究の PGN", + "studyDownloadAllGames": "全局をダウンロード", + "studyChapterPgn": "章の PGN", + "studyCopyChapterPgn": "PGN をコピー", + "studyDownloadGame": "1 局をダウンロード", + "studyStudyUrl": "研究の URL", + "studyCurrentChapterUrl": "現在の章の URL", + "studyYouCanPasteThisInTheForumToEmbed": "これをフォーラムにペーストすれば埋め込み表示できます", + "studyStartAtInitialPosition": "開始局面から", + "studyStartAtX": "{param} に開始", + "studyEmbedInYourWebsite": "自分のウェブサイト/ブログに埋め込む", + "studyReadMoreAboutEmbedding": "埋め込み(embedding)の説明", + "studyOnlyPublicStudiesCanBeEmbedded": "埋め込みできるのは公開研究だけです!", + "studyOpen": "開く", + "studyXBroughtToYouByY": "{param1} を {param2} がお届けします", + "studyStudyNotFound": "研究が見つかりません", + "studyEditChapter": "章を編集", + "studyNewChapter": "新しい章", + "studyImportFromChapterX": "{param} からインポート", + "studyOrientation": "盤の上下", + "studyAnalysisMode": "解析モード", + "studyPinnedChapterComment": "章の優先表示コメント", + "studySaveChapter": "章を保存", + "studyClearAnnotations": "注釈をクリア", + "studyClearVariations": "手順をクリア", + "studyDeleteChapter": "章を削除", + "studyDeleteThisChapter": "ほんとうに削除しますか? 戻せませんよ!", + "studyClearAllCommentsInThisChapter": "この章のコメントと図形をすべて削除しますか?", + "studyRightUnderTheBoard": "盤のすぐ下に", + "studyNoPinnedComment": "なし", + "studyNormalAnalysis": "通常解析", + "studyHideNextMoves": "次の手順をかくす", + "studyInteractiveLesson": "対話形式のレッスン", + "studyChapterX": "章 {param}", + "studyEmpty": "空白", + "studyStartFromInitialPosition": "開始局面から", + "studyEditor": "エディタ", + "studyStartFromCustomPosition": "指定した局面から", + "studyLoadAGameByUrl": "棋譜を URL で読み込み", + "studyLoadAPositionFromFen": "局面を FEN で読み込み", + "studyLoadAGameFromPgn": "棋譜を PGN で読み込み", + "studyAutomatic": "自動", + "studyUrlOfTheGame": "棋譜の URL", + "studyLoadAGameFromXOrY": "{param1} か {param2} から棋譜を読み込み", + "studyCreateChapter": "章を作成", + "studyCreateStudy": "研究を作成", + "studyEditStudy": "研究を編集", + "studyVisibility": "公開範囲", + "studyPublic": "公開", + "studyUnlisted": "非公開", + "studyInviteOnly": "招待のみ", + "studyAllowCloning": "コピーの許可", + "studyNobody": "不許可", + "studyOnlyMe": "自分のみ", + "studyContributors": "参加者のみ", + "studyMembers": "メンバー", + "studyEveryone": "全員", + "studyEnableSync": "同期", + "studyYesKeepEveryoneOnTheSamePosition": "同期する=全員が同じ局面を見る", + "studyNoLetPeopleBrowseFreely": "同期しない=各人が自由に閲覧", + "studyPinnedStudyComment": "優先表示コメント", + "studyStart": "開始", + "studySave": "保存", + "studyClearChat": "チャットを消去", + "studyDeleteTheStudyChatHistory": "ほんとうに削除しますか? 戻せませんよ!", + "studyDeleteStudy": "研究を削除", + "studyConfirmDeleteStudy": "研究全体を削除しますか? 戻せませんよ! 削除なら研究の名称を入力: {param}", + "studyWhereDoYouWantToStudyThat": "どこで研究しますか?", + "studyGoodMove": "好手", + "studyMistake": "悪手", + "studyBrilliantMove": "妙手", + "studyBlunder": "大悪手", + "studyInterestingMove": "面白い手", + "studyDubiousMove": "疑問手", + "studyOnlyMove": "絶対手", + "studyZugzwang": "ツークツワンク", + "studyEqualPosition": "互角", + "studyUnclearPosition": "形勢不明", + "studyWhiteIsSlightlyBetter": "白やや優勢", + "studyBlackIsSlightlyBetter": "黒やや優勢", + "studyWhiteIsBetter": "白優勢", + "studyBlackIsBetter": "黒優勢", + "studyWhiteIsWinning": "白勝勢", + "studyBlackIsWinning": "黒勝勢", + "studyNovelty": "新手", + "studyDevelopment": "展開", + "studyInitiative": "主導権", + "studyAttack": "攻撃", + "studyCounterplay": "反撃", + "studyTimeTrouble": "時間切迫", + "studyWithCompensation": "駒損だが代償あり", + "studyWithTheIdea": "狙い", + "studyNextChapter": "次の章", + "studyPrevChapter": "前の章", + "studyStudyActions": "研究の操作", + "studyTopics": "トピック", + "studyMyTopics": "自分のトピック", + "studyPopularTopics": "人気のトピック", + "studyManageTopics": "トピックの管理", + "studyBack": "戻る", + "studyPlayAgain": "もう一度プレイ", + "studyWhatWouldYouPlay": "この局面、あなたならどう指す?", + "studyYouCompletedThisLesson": "おめでとう ! このレッスンを修了しました。", + "studyPerPage": "{param} 件/ページ", + "studyNbChapters": "{count, plural, other{{count} 章}}", + "studyNbGames": "{count, plural, other{{count} 局}}", + "studyNbMembers": "{count, plural, other{{count} メンバー}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{ここに PGN をペースト({count} 局まで)}}", + "timeagoJustNow": "たった今", + "timeagoRightNow": "たった今", + "timeagoCompleted": "完了", + "timeagoInNbSeconds": "{count, plural, other{{count} 秒後}}", + "timeagoInNbMinutes": "{count, plural, other{{count} 分後}}", + "timeagoInNbHours": "{count, plural, other{{count} 時間後}}", + "timeagoInNbDays": "{count, plural, other{{count} 日後}}", + "timeagoInNbWeeks": "{count, plural, other{{count} 週後}}", + "timeagoInNbMonths": "{count, plural, other{{count} か月後}}", + "timeagoInNbYears": "{count, plural, other{{count} 年後}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} 分前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} 時間前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} 日前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} 週前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} か月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} 年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{残り {count} 分}}", + "timeagoNbHoursRemaining": "{count, plural, other{残り {count} 時間}}" } \ No newline at end of file diff --git a/lib/l10n/lila_kk.arb b/lib/l10n/lila_kk.arb index d8767323ef..d002d954fe 100644 --- a/lib/l10n/lila_kk.arb +++ b/lib/l10n/lila_kk.arb @@ -1,33 +1,33 @@ { + "mobileAllGames": "Барлық ойындар", + "mobileAreYouSure": "Растайсыз ба?", + "mobileClearButton": "Өшіру", + "mobileFeedbackButton": "Пікір айту", + "mobileGreeting": "Ассаламу ғалейкүм, {param}", + "mobileGreetingWithoutName": "Ассаламу ғалейкүм", "mobileHomeTab": "Үйге", - "mobilePuzzlesTab": "Жұмбақ", - "mobileToolsTab": "Құрал", - "mobileWatchTab": "Бақылау", - "mobileSettingsTab": "Баптау", "mobileMustBeLoggedIn": "Бұл бетті көру үшін тіркелгіге кіріңіз.", - "mobileSystemColors": "Жүйе түстері", - "mobileFeedbackButton": "Пікір айту", + "mobileNoSearchResults": "Нәтиже жоқ", + "mobileNotFollowingAnyUser": "Сіз әзір ешкіге серік емессіз.", "mobileOkButton": "Иә", + "mobilePlayersMatchingSearchTerm": "Атауында \"{param}\" бар ойыншылар", + "mobilePuzzleStormNothingToShow": "Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.", + "mobilePuzzleStormSubtitle": "3 минутта барынша көп жұмбақ шешіп көр.", + "mobilePuzzleStreakAbortWarning": "Қазіргі тізбектен айрыласыз, нәтиже сақталады.", + "mobilePuzzleThemesSubtitle": "Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.", + "mobilePuzzlesTab": "Жұмбақ", + "mobileRecentSearches": "Кейінгі іздеулер", "mobileSettingsHapticFeedback": "Дірілмен білдіру", "mobileSettingsImmersiveMode": "Оқшау көрініс", "mobileSettingsImmersiveModeSubtitle": "Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.", - "mobileNotFollowingAnyUser": "Сіз әзір ешкіге серік емессіз.", - "mobileAllGames": "Барлық ойындар", - "mobileRecentSearches": "Кейінгі іздеулер", - "mobileClearButton": "Өшіру", - "mobilePlayersMatchingSearchTerm": "Атауында \"{param}\" бар ойыншылар", - "mobileNoSearchResults": "Нәтиже жоқ", - "mobileAreYouSure": "Растайсыз ба?", - "mobilePuzzleStreakAbortWarning": "Қазіргі тізбектен айрыласыз, нәтиже сақталады.", - "mobilePuzzleStormNothingToShow": "Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.", - "mobileSharePuzzle": "Бұл жұмбақты тарату", - "mobileShareGameURL": "Ойын сілтемесін тарату", + "mobileSettingsTab": "Баптау", "mobileShareGamePGN": "PGN тарату", + "mobileShareGameURL": "Ойын сілтемесін тарату", + "mobileSharePuzzle": "Бұл жұмбақты тарату", "mobileShowResult": "Нәтижесін көру", - "mobilePuzzleThemesSubtitle": "Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.", - "mobilePuzzleStormSubtitle": "3 минутта барынша көп жұмбақ шешіп көр.", - "mobileGreeting": "Ассаламу ғалейкүм, {param}", - "mobileGreetingWithoutName": "Ассаламу ғалейкүм", + "mobileSystemColors": "Жүйе түстері", + "mobileToolsTab": "Құрал", + "mobileWatchTab": "Бақылау", "activityActivity": "Белсенділігі", "activityHostedALiveStream": "Стрим бастады", "activityRankedInSwissTournament": "{param2}-да {param1}-нші орында", @@ -50,7 +50,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} швейцарлық жарысқа қатысты} other{{count} швейцарлық жарысқа қатысты}}", "activityJoinedNbTeams": "{count, plural, =1{{count} топқа қосылды} other{{count} топқа қосылды}}", "broadcastBroadcasts": "Көрсетілімдер", + "broadcastMyBroadcasts": "Менің көрсетілімдерім", "broadcastLiveBroadcasts": "Жарыстың тікелей көрсетілімдері", + "broadcastNewBroadcast": "Жаңа тікелей көрсетілім", + "broadcastAddRound": "Айналым қосу", + "broadcastOngoing": "Болып жатқан", + "broadcastUpcoming": "Келе жатқан", + "broadcastCompleted": "Аяқталған", + "broadcastRoundName": "Айналым атауы", + "broadcastRoundNumber": "Раунд нөмірі", + "broadcastTournamentName": "Жарыс атауы", + "broadcastTournamentDescription": "Жарыстың қысқа сипаттамасы", + "broadcastFullDescription": "Оқиғаның толық сипаттамасы", + "broadcastFullDescriptionHelp": "Көрсетілімнің қосымша үлкен сипаттамасы. {param1} қолданысқа ашық. Ұзындығы {param2} таңбадан кем болуы керек.", + "broadcastSourceUrlHelp": "PGN жаңартуларын алу үшін Личес тексеретін сілтеме. Ол интернетте баршалыққа ашық болуы керек.", + "broadcastStartDateHelp": "Міндетті емес, егер күнін біліп тұрсаңыз", + "broadcastCurrentGameUrl": "Қазіргі ойын сілтемесі", + "broadcastDownloadAllRounds": "Барлық айналымдарды жүктеп алу", + "broadcastResetRound": "Бұл айналымды жаңарту", + "broadcastDeleteRound": "Бұл айналымды жою", + "broadcastDefinitivelyDeleteRound": "Айналым мен оның ойындарын толығымен жою.", + "broadcastDeleteAllGamesOfThisRound": "Айналымның бүкіл ойындарын жою. Оларды қайта құру үшін қайнар көзі белсенді болуы керек.", + "broadcastEditRoundStudy": "Айналымның зертханасын өзгерту", + "broadcastDeleteTournament": "Бұл жарысты жою", + "broadcastDefinitivelyDeleteTournament": "Жарысты айналым мен ойындарымен бірге толығымен жою.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} көрсетілім} other{{count} көрсетілім}}", "challengeChallengesX": "Шақырулар: {param1}", "challengeChallengeToPlay": "Ойынға шақыру", "challengeChallengeDeclined": "Шақыруды қабылдамады", @@ -368,8 +392,8 @@ "puzzleThemeXRayAttackDescription": "Бір тастың қарсылас тасын аттай бір шаршыны қорғауы не шабуылдауы.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Жүрісі шектелген тастардың әр жүрісі жалпы жағдайдың нашарлауына әкеп соқтыратын кез.", - "puzzleThemeHealthyMix": "Аралас дастархан", - "puzzleThemeHealthyMixDescription": "Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!", + "puzzleThemeMix": "Аралас дастархан", + "puzzleThemeMixDescription": "Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!", "puzzleThemePlayerGames": "Ойыншылардан", "puzzleThemePlayerGamesDescription": "Сіздің не басқаның ойындарынан құрылған жұмбақтарды табу.", "puzzleThemePuzzleDownloadInformation": "Осы жұмбақтар көпшілікке ашық доменде сақтаулы, оларды осы жерден жүктеп алуға болады: {param}", @@ -487,7 +511,6 @@ "replayMode": "Ойнату тәртібі", "realtimeReplay": "Өз қарқыны", "byCPL": "CPL сәйкес", - "openStudy": "Зерттеуді ашу", "enable": "Қосу", "bestMoveArrow": "Үздік жүрісті нұсқағыш", "showVariationArrows": "Тармақта нұсқағышты көрсету", @@ -497,7 +520,6 @@ "memory": "Жад", "infiniteAnalysis": "Шектеусіз талдау", "removesTheDepthLimit": "Тереңдік шектеулерін жояды, әрі компьютеріңізді қыздырады", - "engineManager": "Есептеуіш басқарушысы", "blunder": "Өрескел қателік", "mistake": "Қателік", "inaccuracy": "Жеңіл қате", @@ -691,7 +713,6 @@ "block": "Бұғаттау", "blocked": "Бұғатталған", "unblock": "Бұғаттан шығару", - "followsYou": "Сізге серік", "xStartedFollowingY": "{param1} {param2} серігі болды", "more": "Жаю", "memberSince": "Тіркелген күні", @@ -788,7 +809,6 @@ "cheat": "Чит, алдап ойнау", "troll": "Троль, кемсіту", "other": "Басқа", - "reportDescriptionHelp": "Ойынның (-дардың) сілтемесін қойып, осы ойыншының қай әрекеті орынсыз болғанын түсіндіріп беріңіз. Жай ғана \"ол алдап ойнады\" деп жаза салмай, осы ойға қалай келгеніңізді айтып беріңіз. Сіздің шағымыңыз ағылшын тілінде жазылса, тезірек тексеруден өтеді.", "error_provideOneCheatedGameLink": "Кемі бір ойынның сілтемесін беруіңізді сұраймыз.", "by": "жасаған – {param}", "importedByX": "Жүктеп салған – {param}", @@ -965,8 +985,8 @@ "revokeAllSessions": "бәрін ажырату", "playChessEverywhere": "Шахматты кез-келген жерде ойнаңыз", "asFreeAsLichess": "Личес-тей тегін", - "builtForTheLoveOfChessNotMoney": "Ақша қуып емес, шахматты қызыққаннан жасап отырмыз", - "everybodyGetsAllFeaturesForFree": "Барлық құралдары бүкіл адам үшін тегін", + "builtForTheLoveOfChessNotMoney": "Ақша қуып емес, шахматты жақсы көргеннен жасап отырмыз", + "everybodyGetsAllFeaturesForFree": "Барлық құралдары барлық адам үшін тегін", "zeroAdvertisement": "Еш жарнамасыз", "fullFeatured": "Толыққанды жабдықталған", "phoneAndTablet": "Телепон мен планшет", @@ -1201,7 +1221,7 @@ "blocks": "{count, plural, =1{{count} бұғатталды} other{{count} бұғатталды}}", "nbForumPosts": "{count, plural, =1{{count} форумдағы жазба} other{{count} форумдағы жазба}}", "nbPerfTypePlayersThisWeek": "{count, plural, =1{Осы аптада {count} {param2}-ойыншы.} other{Осы аптада {count} {param2}-ойыншы.}}", - "availableInNbLanguages": "{count, plural, =1{{count} тіліндегі нұсқасы бар!} other{{count} тіліндегі нұсқасы бар!}}", + "availableInNbLanguages": "{count, plural, =1{{count} тілде нұсқасы бар!} other{{count} тіліндегі нұсқасы бар!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{Бірінші жүріске {count} секунд бар} other{Бірінші жүріске {count} секунд бар}}", "nbSeconds": "{count, plural, =1{{count} секунд} other{{count} секунд}}", "andSaveNbPremoveLines": "{count, plural, =1{әрі ертелі жүрістердің {count} жолын сақтау} other{әрі ертелі жүрістердің {count} жолын сақтау}}", @@ -1259,6 +1279,173 @@ "stormXRuns": "{count, plural, =1{1 кезең} other{{count} кезең}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{{param2} кезеңнің бірі ойналды} other{{param2} кезеңнің {count} ойналды}}", "streamerLichessStreamers": "Личес стримерлері", + "studyPrivate": "Жеке", + "studyMyStudies": "Менің зерттеулерім", + "studyStudiesIContributeTo": "Қолдауымдағы зерттеулер", + "studyMyPublicStudies": "Жалпыға ашық зерттеулерім", + "studyMyPrivateStudies": "Жеке зерттеулерім", + "studyMyFavoriteStudies": "Қалаулы зерттеулерім", + "studyWhatAreStudies": "Зерттеулер деген не?", + "studyAllStudies": "Бүкіл зерттеулер", + "studyStudiesCreatedByX": "{param} жасаған зерттеулер", + "studyNoneYet": "Әзірге жоқ.", + "studyHot": "Тренд", + "studyDateAddedNewest": "Құрылған күні (жаңадан)", + "studyDateAddedOldest": "Құрылған күні (ескіден)", + "studyRecentlyUpdated": "Жақында құрылған", + "studyMostPopular": "Ең танымалдары", + "studyAlphabetical": "Әліппе ретімен", + "studyAddNewChapter": "Жаңа бөлім құру", + "studyAddMembers": "Мүшелерді қосу", + "studyInviteToTheStudy": "Зерттеуге шақыру", + "studyPleaseOnlyInvitePeopleYouKnow": "Тек таныстарды әрі зерттеуге қосылуға шын ниетті адамдарды ғана шықырыңыз.", + "studySearchByUsername": "Тіркеулі атымен іздеу", + "studySpectator": "Көрермен", + "studyContributor": "Қолдаушы", + "studyKick": "Шығару", + "studyLeaveTheStudy": "Зерттеуден шығу", + "studyYouAreNowAContributor": "Сіз енді қолдаушысыз", + "studyYouAreNowASpectator": "Сіз енді көрерменсіз", + "studyPgnTags": "PGN тэгтері", + "studyLike": "Ұнату", + "studyUnlike": "Ұнатпаймын", + "studyNewTag": "Жаңа тэг", + "studyCommentThisPosition": "Осы тақта күйі туралы пікір қалдыру", + "studyCommentThisMove": "Осы жүріс туралы пікір қалдыру", + "studyAnnotateWithGlyphs": "Глифтермен түсіндірме жазуу", + "studyTheChapterIsTooShortToBeAnalysed": "Бөлім талдауға жарамды болу үшін тым қысқа.", + "studyOnlyContributorsCanRequestAnalysis": "Зерттеу қолдаушылары ғана компьютерлік талдауды сұрай алады.", + "studyGetAFullComputerAnalysis": "Сервер-жақты компьютер осы негізгі жолға толық талдау жасайтын болады.", + "studyMakeSureTheChapterIsComplete": "Талдауды бір рет қана сұрай аласыз, сондықтан бөлімді аяқтауды ұмытпаңыз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Барлық үйлескен мүшелер өз күйінде қалады", + "studyShareChanges": "Көрермендермен өзгертулерді бөлісіңіз әрі серверде сақтап қойыңыз", + "studyPlaying": "Қазір ойында", + "studyFirst": "Бірінші", + "studyPrevious": "Алдыңғы", + "studyNext": "Келесі", + "studyLast": "Соңғы", "studyShareAndExport": "Бөлісу мен Жүктеп алу", - "studyStart": "Бастау" + "studyCloneStudy": "Көшірме", + "studyStudyPgn": "Зерттеудің PGN", + "studyDownloadAllGames": "Барлық ойындарды жүктеп алу", + "studyChapterPgn": "Бөлімнің PGN", + "studyCopyChapterPgn": "PGN-ді көшіру", + "studyDownloadGame": "Ойынды жүктеп алу", + "studyStudyUrl": "Зерттеудің сілтемесі", + "studyCurrentChapterUrl": "Қазіргі бөлімнің сілтемесі", + "studyYouCanPasteThisInTheForumToEmbed": "Сіз бұны форумға не Личес блогыңызға қоя аласыз", + "studyStartAtInitialPosition": "Басталуы: бастапқы күйден", + "studyStartAtX": "Басталуы: {param}", + "studyEmbedInYourWebsite": "Сіздің сайт не блогыңызға арналған енгізу сілтемесі", + "studyReadMoreAboutEmbedding": "Енгізу туралы оқыңыз", + "studyOnlyPublicStudiesCanBeEmbedded": "Тек жалпыға ашық зерттеулер енгізуге жарамды!", + "studyOpen": "Ашу", + "studyXBroughtToYouByY": "{param1}, оны сізге {param2} ұсынды", + "studyStudyNotFound": "Зерттеу табылмады", + "studyEditChapter": "Бөлімді өңдеу", + "studyNewChapter": "Жаңа бөлім", + "studyImportFromChapterX": "{param}-нан жүктеп алу", + "studyOrientation": "Бағыты", + "studyAnalysisMode": "Талдау нұсқасы", + "studyPinnedChapterComment": "Қадаулы бөлім пікірі", + "studySaveChapter": "Бөлімді сақтау", + "studyClearAnnotations": "Түсіндірмені өшіру", + "studyClearVariations": "Тармақты өшіру", + "studyDeleteChapter": "Бөлімді жою", + "studyDeleteThisChapter": "Бөлімді жоясыз ба? Кері жол жоқ!", + "studyClearAllCommentsInThisChapter": "Бөлімдегі бүкіл пікір, глиф пен сызбаларды өшіресіз бе?", + "studyRightUnderTheBoard": "Тура тақтаның астына", + "studyNoPinnedComment": "Жоқ", + "studyNormalAnalysis": "Қалыпты талдау", + "studyHideNextMoves": "Келесі жүрістерді жасыру", + "studyInteractiveLesson": "Интерактивті сабақ", + "studyChapterX": "{param}-ші бөлім", + "studyEmpty": "Бос", + "studyStartFromInitialPosition": "Басталуы: бастапқы күйден", + "studyEditor": "Өңдеуші", + "studyStartFromCustomPosition": "Басталуы: белгілі күйден", + "studyLoadAGameByUrl": "Сілтеме арқылы ойындарды жүктеп салу", + "studyLoadAPositionFromFen": "FEN арқылы ойындарды жүктеп салу", + "studyLoadAGameFromPgn": "PGN арқылы ойындарды жүктеп салу", + "studyAutomatic": "Автоматты түрде", + "studyUrlOfTheGame": "Ойындардың сілтемесі, әр жолға бір-бірден", + "studyLoadAGameFromXOrY": "{param1} не {param2} ойындарын жүктеп салу", + "studyCreateChapter": "Бөлім құру", + "studyCreateStudy": "Зерттеуді құру", + "studyEditStudy": "Зерттеуді өңдеу", + "studyVisibility": "Көрінуі", + "studyPublic": "Жалпыға ашық", + "studyUnlisted": "Жасырын", + "studyInviteOnly": "Шақырумен ғана", + "studyAllowCloning": "Көшірмеге рұқсат беру", + "studyNobody": "Ешкім", + "studyOnlyMe": "Өзім ғана", + "studyContributors": "Қолдаушылар", + "studyMembers": "Мүшелер", + "studyEveryone": "Барлығы", + "studyEnableSync": "Үйлесуді қосу", + "studyYesKeepEveryoneOnTheSamePosition": "Иә: бәрі бірдей күйде болады", + "studyNoLetPeopleBrowseFreely": "Жоқ: бәріне еркін шолуға рұқсат ету", + "studyPinnedStudyComment": "Қадаулы зерттеу пікірі", + "studyStart": "Бастау", + "studySave": "Сақтау", + "studyClearChat": "Чатты өшіру", + "studyDeleteTheStudyChatHistory": "Зерттеудің чат тарихын өшіресіз бе? Кері жол жоқ!", + "studyDeleteStudy": "Зерттеуді жою", + "studyConfirmDeleteStudy": "Бүкіл зерттеуді жоясыз ба? Қайтар жол жоқ. Растау үшін зерттеу атауын жазыңыз: {param}", + "studyWhereDoYouWantToStudyThat": "Бұл күйдің зерттеуін қай жерде бастайсыз?", + "studyGoodMove": "Жақсы жүріс", + "studyMistake": "Қате", + "studyBrilliantMove": "Әдемі жүріс", + "studyBlunder": "Өрескел қателік", + "studyInterestingMove": "Қызық жүріс", + "studyDubiousMove": "Күмәнді жүріс", + "studyOnlyMove": "Жалғыз жүріс", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Күйлері шамалас", + "studyUnclearPosition": "Күйі анық емес", + "studyWhiteIsSlightlyBetter": "Ақ сәл күштірек", + "studyBlackIsSlightlyBetter": "Қара сәл күштірек", + "studyWhiteIsBetter": "Ақтың жағдайы жақсы", + "studyBlackIsBetter": "Қараның жағдайы жақсы", + "studyWhiteIsWinning": "Ақ жеңеді", + "studyBlackIsWinning": "Қара жеңеді", + "studyNovelty": "Жаңашылдық", + "studyDevelopment": "Дамыту", + "studyInitiative": "Белсенді", + "studyAttack": "Шабуыл", + "studyCounterplay": "Қарсы шабуыл", + "studyTimeTrouble": "Уақыт қаупі", + "studyWithCompensation": "Өтеумен", + "studyWithTheIdea": "Бір оймен", + "studyNextChapter": "Келесі бөлім", + "studyPrevChapter": "Алдыңғы бөлім", + "studyStudyActions": "Зерттеу әрекеттері", + "studyTopics": "Тақырыптар", + "studyMyTopics": "Менің тақырыптарым", + "studyPopularTopics": "Белгілі тақырыптар", + "studyManageTopics": "Тақырыптарды басқару", + "studyBack": "Кері қайту", + "studyPlayAgain": "Қайта ойнау", + "studyWhatWouldYouPlay": "Осы күйде не ойнамақсыз?", + "studyYouCompletedThisLesson": "Құтты болсын! Сіз бұл сабақты бітірдіңіз.", + "studyNbChapters": "{count, plural, =1{{count} бөлім} other{{count} бөлім}}", + "studyNbGames": "{count, plural, =1{{count} ойын} other{{count} ойын}}", + "studyNbMembers": "{count, plural, =1{{count} мүше} other{{count} мүше}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN мәтінін осында қойыңыз, {count} ойын ғана} other{PGN мәтінін осында қойыңыз, {count} ойынға дейін}}", + "timeagoJustNow": "жаңа ғана", + "timeagoRightNow": "дәл қазір", + "timeagoInNbSeconds": "{count, plural, =1{{count} секундта} other{{count} секундта}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} минутта} other{{count} минутта}}", + "timeagoInNbHours": "{count, plural, =1{{count} сағатта} other{{count} сағатта}}", + "timeagoInNbDays": "{count, plural, =1{{count} күннен кейін} other{{count} күннен кейін}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} аптадан кейін} other{{count} аптадан кейін}}", + "timeagoInNbMonths": "{count, plural, =1{{count} айдан кейін} other{{count} айдан кейін}}", + "timeagoInNbYears": "{count, plural, =1{{count} жылдан кейін} other{{count} жылдан кейін}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} минут бұрын} other{{count} минут бұрын}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} сағат бұрын} other{{count} сағат бұрын}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} күн бұрын} other{{count} күн бұрын}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} апта бұрын} other{{count} апта бұрын}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ай бұрын} other{{count} ай бұрын}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} жыл бұрын} other{{count} жыл бұрын}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ko.arb b/lib/l10n/lila_ko.arb index 704fad8b14..1d80dc93cc 100644 --- a/lib/l10n/lila_ko.arb +++ b/lib/l10n/lila_ko.arb @@ -1,62 +1,147 @@ { + "mobileAllGames": "모든 대국", + "mobileAreYouSure": "확실하십니까?", + "mobileBlindfoldMode": "기물 가리기", + "mobileCancelTakebackOffer": "무르기 요청 취소", + "mobileClearButton": "지우기", + "mobileCorrespondenceClearSavedMove": "저장된 수 삭제", + "mobileCustomGameJoinAGame": "게임 참가", + "mobileFeedbackButton": "피드백", + "mobileGreeting": "안녕하세요, {param}", + "mobileGreetingWithoutName": "안녕하세요", + "mobileHideVariation": "바리에이션 숨기기", "mobileHomeTab": "홈", - "mobilePuzzlesTab": "퍼즐", - "mobileToolsTab": "도구", - "mobileWatchTab": "관람", - "mobileSettingsTab": "설정", + "mobileLiveStreamers": "방송 중인 스트리머", "mobileMustBeLoggedIn": "이 페이지를 보려면 로그인해야 합니다.", - "mobileSystemColors": "시스템 색상", - "mobileFeedbackButton": "피드백", + "mobileNoSearchResults": "결과 없음", + "mobileNotFollowingAnyUser": "팔로우한 사용자가 없습니다.", "mobileOkButton": "확인", + "mobilePlayersMatchingSearchTerm": "닉네임에 \"{param}\"가 포함된 플레이어", + "mobilePrefMagnifyDraggedPiece": "드래그한 기물 확대하기", + "mobilePuzzleStormConfirmEndRun": "이 도전을 종료하시겠습니까?", + "mobilePuzzleStormFilterNothingToShow": "표시할 것이 없습니다. 필터를 변경해 주세요", + "mobilePuzzleStormNothingToShow": "표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.", + "mobilePuzzleStormSubtitle": "3분 이내에 최대한 많은 퍼즐을 해결하십시오.", + "mobilePuzzleStreakAbortWarning": "현재 연속 해결 기록을 잃고 점수는 저장됩니다.", + "mobilePuzzleThemesSubtitle": "당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.", + "mobilePuzzlesTab": "퍼즐", + "mobileRecentSearches": "최근 검색어", "mobileSettingsHapticFeedback": "터치 시 진동", "mobileSettingsImmersiveMode": "전체 화면 모드", "mobileSettingsImmersiveModeSubtitle": "플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.", - "mobileNotFollowingAnyUser": "팔로우한 사용자가 없습니다.", - "mobileAllGames": "모든 대국", - "mobileRecentSearches": "최근 검색어", - "mobileClearButton": "지우기", - "mobilePlayersMatchingSearchTerm": "닉네임에 \"{param}\"가 포함된 플레이어", - "mobileNoSearchResults": "결과 없음", - "mobileAreYouSure": "확실하십니까?", - "mobilePuzzleStreakAbortWarning": "현재 연속 해결 기록을 잃고 점수는 저장됩니다.", - "mobilePuzzleStormNothingToShow": "표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.", - "mobileSharePuzzle": "이 퍼즐 공유", - "mobileShareGameURL": "게임 URL 공유", + "mobileSettingsTab": "설정", "mobileShareGamePGN": "PGN 공유", + "mobileShareGameURL": "게임 URL 공유", "mobileSharePositionAsFEN": "FEN으로 공유", + "mobileSharePuzzle": "이 퍼즐 공유", "mobileShowComments": "댓글 보기", - "mobilePuzzleStormConfirmEndRun": "이 도전을 종료하시겠습니까?", - "mobilePuzzleStormFilterNothingToShow": "표시할 것이 없습니다. 필터를 변경해 주세요", - "mobileCancelTakebackOffer": "무르기 요청 취소", - "mobileCancelDrawOffer": "무승부 요청 취소", - "mobileWaitingForOpponentToJoin": "상대 참가를 기다리는 중...", - "mobileLiveStreamers": "방송 중인 스트리머", - "mobileCustomGameJoinAGame": "게임 참가", - "mobileCorrespondenceClearSavedMove": "저장된 수 삭제", + "mobileShowResult": "결과 표시", + "mobileShowVariations": "바리에이션 보이기", "mobileSomethingWentWrong": "문제가 발생했습니다.", + "mobileSystemColors": "시스템 색상", + "mobileTheme": "테마", + "mobileToolsTab": "도구", + "mobileWaitingForOpponentToJoin": "상대 참가를 기다리는 중...", + "mobileWatchTab": "관람", "activityActivity": "활동", "activityHostedALiveStream": "라이브 스트리밍을 함", "activityRankedInSwissTournament": "{param2}에서 {param1}등", "activitySignedUp": "Lichess에 회원가입함", - "activitySupportedNbMonths": "{count, plural, other{{count} 개월 동안 {param2} 에서 lichess.org 을 후원하였습니다.}}", - "activityPracticedNbPositions": "{count, plural, other{{param2} 에서 총 {count} 개의 포지션을 연습하였습니다.}}", - "activitySolvedNbPuzzles": "{count, plural, other{전술 문제 {count} 개를 해결하였습니다.}}", + "activitySupportedNbMonths": "{count, plural, other{{count}개월 동안 {param2}으로 lichess.org를 후원함}}", + "activityPracticedNbPositions": "{count, plural, other{{param2}에서 총 {count}개의 포지션을 연습함}}", + "activitySolvedNbPuzzles": "{count, plural, other{전술 문제 {count}개를 해결함}}", "activityPlayedNbGames": "{count, plural, other{총 {count} 회의 {param2} 게임을 하였습니다.}}", "activityPostedNbMessages": "{count, plural, other{{param2} 에 총 {count} 개의 글을 게시하였습니다.}}", - "activityPlayedNbMoves": "{count, plural, other{{count} 수를 둠}}", - "activityInNbCorrespondenceGames": "{count, plural, other{{count}개의 통신전에서}}", - "activityCompletedNbGames": "{count, plural, other{{count} 번의 통신전을 완료하셨습니다.}}", + "activityPlayedNbMoves": "{count, plural, other{수 {count}개를 둠}}", + "activityInNbCorrespondenceGames": "{count, plural, other{{count}개의 통신 대국에서}}", + "activityCompletedNbGames": "{count, plural, other{{count}번의 통신 대국을 완료함}}", + "activityCompletedNbVariantGames": "{count, plural, other{{count}번의 {param2} 통신 대국을 완료함}}", "activityFollowedNbPlayers": "{count, plural, other{{count} 명을 팔로우 개시}}", "activityGainedNbFollowers": "{count, plural, other{{count} 명의 신규 팔로워를 얻음}}", "activityHostedNbSimuls": "{count, plural, other{{count} 번의 동시대국을 주최함}}", "activityJoinedNbSimuls": "{count, plural, other{{count} 번의 동시대국에 참가함}}", - "activityCreatedNbStudies": "{count, plural, other{{count} 건의 연구를 작성함}}", + "activityCreatedNbStudies": "{count, plural, other{새 연구 {count}개 작성함}}", "activityCompetedInNbTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", "activityRankedInTournament": "{count, plural, other{{count} 위 (상위 {param2}%) ({param4} 에서 {param3} 국)}}", "activityCompetedInNbSwissTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", "activityJoinedNbTeams": "{count, plural, other{{count} 팀에 참가함}}", "broadcastBroadcasts": "방송", + "broadcastMyBroadcasts": "내 방송", "broadcastLiveBroadcasts": "실시간 대회 방송", + "broadcastBroadcastCalendar": "방송 달력", + "broadcastNewBroadcast": "새 실시간 방송", + "broadcastSubscribedBroadcasts": "구독 중인 방송", + "broadcastAboutBroadcasts": "방송에 대해서", + "broadcastHowToUseLichessBroadcasts": "Lichess 방송을 사용하는 방법.", + "broadcastTheNewRoundHelp": "새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.", + "broadcastAddRound": "라운드 추가", + "broadcastOngoing": "진행중", + "broadcastUpcoming": "방영 예정", + "broadcastCompleted": "종료됨", + "broadcastCompletedHelp": "Lichess는 경기 완료를 감지하지만, 잘못될 때가 있을 수 있습니다. 수동으로 설정하기 위해 이걸 사용하세요.", + "broadcastRoundName": "라운드 이름", + "broadcastRoundNumber": "라운드 숫자", + "broadcastTournamentName": "토너먼트 이름", + "broadcastTournamentDescription": "짧은 토너먼트 설명", + "broadcastFullDescription": "전체 이벤트 설명", + "broadcastFullDescriptionHelp": "(선택) 방송에 대한 긴 설명입니다. {param1} 사용이 가능합니다. 길이는 {param2} 글자보다 짧아야 합니다.", + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "Lichess가 PGN 업데이트를 받기 위해 확인할 URL입니다. 인터넷에서 공개적으로 액세스 할 수 있어야 합니다.", + "broadcastSourceGameIds": "공간으로 나눠진 64개까지의 Lichess 경기 ID.", + "broadcastStartDateTimeZone": "내 시간대의 토너먼트 시작 날짜: {param}", + "broadcastStartDateHelp": "선택 사항, 언제 이벤트가 시작되는지 알고 있는 경우", + "broadcastCurrentGameUrl": "현재 게임 URL", + "broadcastDownloadAllRounds": "모든 라운드 다운로드받기", + "broadcastResetRound": "라운드 초기화", + "broadcastDeleteRound": "라운드 삭제", + "broadcastDefinitivelyDeleteRound": "라운드와 해당 게임을 완전히 삭제합니다.", + "broadcastDeleteAllGamesOfThisRound": "이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.", + "broadcastEditRoundStudy": "경기 연구 편집", + "broadcastDeleteTournament": "이 토너먼트 삭제", + "broadcastDefinitivelyDeleteTournament": "토너먼트 전체의 모든 라운드와 게임을 완전히 삭제합니다.", + "broadcastShowScores": "게임 결과에 따라 플레이어 점수 표시", + "broadcastReplacePlayerTags": "선택 사항: 플레이어 이름, 레이팅 및 타이틀 바꾸기", + "broadcastFideFederations": "FIDE 연맹", + "broadcastTop10Rating": "Top 10 레이팅", + "broadcastFidePlayers": "FIDE 선수들", + "broadcastFidePlayerNotFound": "FIDE 선수 찾지 못함", + "broadcastFideProfile": "FIDE 프로필", + "broadcastFederation": "연맹", + "broadcastAgeThisYear": "올해 나이", + "broadcastUnrated": "비레이팅", + "broadcastRecentTournaments": "최근 토너먼트", + "broadcastOpenLichess": "Lichess에서 열기", + "broadcastTeams": "팀", + "broadcastBoards": "보드", + "broadcastOverview": "개요", + "broadcastSubscribeTitle": "라운드가 시작될 때 알림을 받으려면 구독하세요. 계정 설정에서 방송을 위한 벨이나 알림 푸시를 토글할 수 있습니다.", + "broadcastUploadImage": "토너먼트 사진 업로드", + "broadcastNoBoardsYet": "아직 보드가 없습니다. 게임들이 업로드되면 나타납니다.", + "broadcastBoardsCanBeLoaded": "보드들은 소스나 {param}(으)로 로드될 수 있습니다", + "broadcastStartsAfter": "{param} 후 시작", + "broadcastStartVerySoon": "방송이 곧 시작됩니다.", + "broadcastNotYetStarted": "아직 방송이 시작을 하지 않았습니다.", + "broadcastOfficialWebsite": "공식 웹사이트", + "broadcastStandings": "순위", + "broadcastOfficialStandings": "공식 순위", + "broadcastIframeHelp": "{param}에서 더 많은 정보를 확인하실 수 있습니다", + "broadcastWebmastersPage": "웹마스터 페이지", + "broadcastPgnSourceHelp": "이 라운드의 공개된, 실시간 PGN 소스 입니다. 보다 더 빠르고 효율적인 동기화를 위해 {param}도 제공됩니다.", + "broadcastEmbedThisBroadcast": "이 방송을 웹사이트에 삽입하세요", + "broadcastEmbedThisRound": "{param}을(를) 웹사이트에 삽입하세요", + "broadcastRatingDiff": "레이팅 차이", + "broadcastGamesThisTournament": "이 토너먼트의 게임들", + "broadcastScore": "점수", + "broadcastAllTeams": "모든 팀", + "broadcastTournamentFormat": "토너먼트 형식", + "broadcastTournamentLocation": "토너먼트 장소", + "broadcastTopPlayers": "상위 플레이어들", + "broadcastTimezone": "시간대", + "broadcastFideRatingCategory": "FIDE 레이팅 범주", + "broadcastOptionalDetails": "선택적 세부 정보", + "broadcastPastBroadcasts": "과거 방송들", + "broadcastAllBroadcastsByMonth": "월별 방송들 모두 보기", + "broadcastNbBroadcasts": "{count, plural, other{{count} 방송}}", "challengeChallengesX": "도전: {param1}", "challengeChallengeToPlay": "도전 신청", "challengeChallengeDeclined": "도전 거절됨", @@ -70,9 +155,9 @@ "challengeXOnlyAcceptsChallengesFromFriends": "{param}님은 친구인 상대만 도전을 받아들입니다.", "challengeDeclineGeneric": "지금 도전을 받지 않습니다.", "challengeDeclineLater": "시간이 맞지 않습니다. 나중에 다시 요청해주세요.", - "challengeDeclineTooFast": "시간이 너무 짧습니다. 더 긴 게임으로 신청해주세요.", - "challengeDeclineTooSlow": "시간이 너무 깁니다. 더 빠른 게임으로 신청해주세요.", - "challengeDeclineTimeControl": "이 시간으로는 도전을 받지 않습니다.", + "challengeDeclineTooFast": "시간이 너무 짧습니다. 더 느린 게임으로 신청해주세요.", + "challengeDeclineTooSlow": "시간이 너무 깁니다. 더 빠른 게임으로 다시 신청해주세요.", + "challengeDeclineTimeControl": "이 시간 제한으로는 도전을 받지 않겠습니다.", "challengeDeclineRated": "대신 레이팅 대전을 신청해주세요.", "challengeDeclineCasual": "대신 캐주얼 대전을 신청해주세요.", "challengeDeclineStandard": "지금은 변형 체스 도전을 받지 않고 있습니다.", @@ -91,44 +176,44 @@ "perfStatProgressOverLastXGames": "최근 {param} 게임 동안:", "perfStatRatingDeviation": "레이팅 편차: {param}.", "perfStatRatingDeviationTooltip": "낮은 값일 수록 레이팅이 안정적입니다. {param1} 이상이라면 임시 레이팅으로 간주합니다. 랭킹에 들기 위해서는 이 값이 {param2} (스탠다드 체스) 또는 {param3} (변형 체스) 보다 낮아야 합니다.", - "perfStatTotalGames": "총 게임", + "perfStatTotalGames": "총 대국", "perfStatRatedGames": "레이팅 게임", "perfStatTournamentGames": "토너먼트 게임", "perfStatBerserkedGames": "버서크 게임", "perfStatTimeSpentPlaying": "플레이한 시간", - "perfStatAverageOpponent": "상대의 평균 레이팅", - "perfStatVictories": "승리", - "perfStatDefeats": "패배", + "perfStatAverageOpponent": "상대 평균", + "perfStatVictories": "승", + "perfStatDefeats": "패", "perfStatDisconnections": "연결 끊김", "perfStatNotEnoughGames": "충분한 게임을 하지 않으셨습니다", "perfStatHighestRating": "최고 레이팅: {param}", "perfStatLowestRating": "최저 레이팅: {param}", - "perfStatFromXToY": "{param1}에서 {param2}", + "perfStatFromXToY": "{param1}에서 {param2}까지", "perfStatWinningStreak": "연승", "perfStatLosingStreak": "연패", "perfStatLongestStreak": "최고 기록: {param}", "perfStatCurrentStreak": "현재 기록: {param}", - "perfStatBestRated": "승리한 최고 레이팅", - "perfStatGamesInARow": "연속 게임 플레이", + "perfStatBestRated": "최고 레이팅 승리", + "perfStatGamesInARow": "연속 대국", "perfStatLessThanOneHour": "게임 사이가 1시간 미만인 경우", "perfStatMaxTimePlaying": "게임을 한 최대 시간", "perfStatNow": "지금", "preferencesPreferences": "설정", "preferencesDisplay": "화면", - "preferencesPrivacy": "프라이버시", - "preferencesNotifications": "공지 사항", + "preferencesPrivacy": "보안", + "preferencesNotifications": "알림", "preferencesPieceAnimation": "기물 움직임 애니메이션", "preferencesMaterialDifference": "기물 차이", "preferencesBoardHighlights": "보드 하이라이트 (마지막 수 및 체크)", "preferencesPieceDestinations": "기물 착지점 (유효한 움직임 및 미리두기)", "preferencesBoardCoordinates": "보드 좌표 (A-H, 1-8)", - "preferencesMoveListWhilePlaying": "피스 움직임 기록", + "preferencesMoveListWhilePlaying": "기물 움직임 기록", "preferencesPgnPieceNotation": "PGN 기물표기방식", - "preferencesChessPieceSymbol": "체스 말 기호", + "preferencesChessPieceSymbol": "체스 기물 기호", "preferencesPgnLetter": "알파벳 (K, Q, R, B, N)", "preferencesZenMode": "젠 모드", "preferencesShowPlayerRatings": "플레이어 레이팅 보기", - "preferencesShowFlairs": "플레이어 레이팅 보기", + "preferencesShowFlairs": "플레이어 아이콘 보기", "preferencesExplainShowPlayerRatings": "체스에 집중할 수 있도록 웹사이트에서 레이팅을 모두 숨깁니다. 경기는 여전히 레이팅에 반영될 것이며, 눈으로 보이는 정보에만 영향을 줍니다.", "preferencesDisplayBoardResizeHandle": "보드 크기 재조정 핸들 보이기", "preferencesOnlyOnInitialPosition": "초기 상태에서만", @@ -144,20 +229,21 @@ "preferencesClickTwoSquares": "현재 위치와 원하는 위치에 클릭하기", "preferencesDragPiece": "드래그", "preferencesBothClicksAndDrag": "아무 방법으로", - "preferencesPremovesPlayingDuringOpponentTurn": "미리두기 (상대 턴일 때 수를 두기)", + "preferencesPremovesPlayingDuringOpponentTurn": "미리두기 (상대 차례일 때 수를 두기)", "preferencesTakebacksWithOpponentApproval": "무르기 (상대 승인과 함께)", "preferencesInCasualGamesOnly": "캐주얼 모드에서만", "preferencesPromoteToQueenAutomatically": "퀸으로 자동 승진", "preferencesExplainPromoteToQueenAutomatically": "일시적으로 자동 승진을 끄기 위해 승진하는 동안 를 누르세요", - "preferencesWhenPremoving": "미리둘 때만", + "preferencesWhenPremoving": "미리두기 때만", "preferencesClaimDrawOnThreefoldRepetitionAutomatically": "3회 동형반복시 자동으로 무승부 요청", "preferencesWhenTimeRemainingLessThanThirtySeconds": "남은 시간이 30초 미만일 때만", - "preferencesMoveConfirmation": "피스를 움직이기 전에 물음", - "preferencesInCorrespondenceGames": "긴 대국에서만", - "preferencesCorrespondenceAndUnlimited": "긴 대국과 무제한", + "preferencesMoveConfirmation": "수 확인", + "preferencesExplainCanThenBeTemporarilyDisabled": "경기 도중 보드 메뉴에서 비활성화될 수 있습니다.", + "preferencesInCorrespondenceGames": "통신 대국", + "preferencesCorrespondenceAndUnlimited": "통신 대국과 무제한", "preferencesConfirmResignationAndDrawOffers": "기권 또는 무승부 제안시 물음", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "캐슬링 방법", - "preferencesCastleByMovingTwoSquares": "왕을 2칸 옮기기", + "preferencesCastleByMovingTwoSquares": "킹을 2칸 옮기기", "preferencesCastleByMovingOntoTheRook": "킹을 룩한테 이동", "preferencesInputMovesWithTheKeyboard": "키보드 입력", "preferencesInputMovesWithVoice": "음성으로 기물 이동", @@ -169,16 +255,17 @@ "preferencesNotifyStreamStart": "스트리머가 생방송 시작", "preferencesNotifyInboxMsg": "새로운 받은 편지함 메시지", "preferencesNotifyForumMention": "포럼 댓글에서 당신이 언급됨", - "preferencesNotifyInvitedStudy": "스터디 초대", - "preferencesNotifyGameEvent": "긴 대국 업데이트", + "preferencesNotifyInvitedStudy": "연구 초대", + "preferencesNotifyGameEvent": "통신 대국 업데이트", "preferencesNotifyChallenge": "도전 과제", "preferencesNotifyTournamentSoon": "곧 토너먼트 시작할 때", - "preferencesNotifyTimeAlarm": "긴 대국 시간 초과", - "preferencesNotifyBell": "리체스 내에서 벨 알림", - "preferencesNotifyPush": "리체스를 사용하지 않을 때 기기 알림", + "preferencesNotifyTimeAlarm": "통신 대국 시간 곧 만료됨", + "preferencesNotifyBell": "Lichess 내에서 벨 알림", + "preferencesNotifyPush": "Lichess를 사용하지 않을 때 기기 알림", "preferencesNotifyWeb": "브라우저", "preferencesNotifyDevice": "기기 정보", "preferencesBellNotificationSound": "벨 알림 음", + "preferencesBlindfold": "기물 가리기", "puzzlePuzzles": "퍼즐", "puzzlePuzzleThemes": "퍼즐 테마", "puzzleRecommended": "추천", @@ -194,9 +281,9 @@ "puzzleVoteToLoadNextOne": "다음 퍼즐을 위해 투표해주세요!", "puzzleUpVote": "퍼즐 추천", "puzzleDownVote": "퍼즐 비추천", - "puzzleYourPuzzleRatingWillNotChange": "당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 스킬에 맞는 퍼즐을 선택하도록 돕습니다.", - "puzzleFindTheBestMoveForWhite": "백의 최고의 수를 찾아보세요.", - "puzzleFindTheBestMoveForBlack": "흑의 최고의 수를 찾아보세요.", + "puzzleYourPuzzleRatingWillNotChange": "당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.", + "puzzleFindTheBestMoveForWhite": "백의 최선 수를 찾아보세요.", + "puzzleFindTheBestMoveForBlack": "흑의 최선 수를 찾아보세요.", "puzzleToGetPersonalizedPuzzles": "개인화된 퍼즐을 위해선:", "puzzlePuzzleId": "퍼즐 {param}", "puzzlePuzzleOfTheDay": "오늘의 퍼즐", @@ -212,7 +299,7 @@ "puzzleOpeningsYouPlayedTheMost": "레이팅 게임에서 가장 많이 플레이한 오프닝", "puzzleUseFindInPage": "브라우저의 \"페이지에서 찾기\" 메뉴를 이용해 가장 좋아하는 오프닝을 찾으세요!", "puzzleUseCtrlF": "Ctrl+f를 사용해서 가장 좋아하는 오프닝을 찾으세요!", - "puzzleNotTheMove": "답이 아닙니다!", + "puzzleNotTheMove": "그 수가 아닙니다!", "puzzleTrySomethingElse": "다른 것 시도하기", "puzzleRatingX": "레이팅: {param}", "puzzleHidden": "숨겨짐", @@ -291,7 +378,7 @@ "puzzleThemeClearanceDescription": "이어지는 전술적 아이디어를 위해 칸, 파일 또는 대각선을 비우는 수입니다.", "puzzleThemeDefensiveMove": "방어적인 수", "puzzleThemeDefensiveMoveDescription": "기물을 잃거나 다른 손실을 피하기 위해 필요한 정확한 수입니다.", - "puzzleThemeDeflection": "유인", + "puzzleThemeDeflection": "굴절", "puzzleThemeDeflectionDescription": "중요한 칸을 수비하는 등 다른 역할을 수행하는 상대 기물의 주의를 분산시키는 수입니다. \"과부하\"라고도 불립니다.", "puzzleThemeDiscoveredAttack": "디스커버드 어택", "puzzleThemeDiscoveredAttackDescription": "장거리 기물(예: 룩)의 길을 막고 있는 기물(예: 나이트)을 이동시켜 공격합니다.", @@ -374,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "기물이 상대 기물 너머의 칸을 공격 또는 방어합니다.", "puzzleThemeZugzwang": "추크추방", "puzzleThemeZugzwangDescription": "상대가 둘 수 있는 수는 제한되어 있으며, 모든 수가 포지션을 악화시킵니다.", - "puzzleThemeHealthyMix": "골고루 섞기", - "puzzleThemeHealthyMixDescription": "전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.", + "puzzleThemeMix": "골고루 섞기", + "puzzleThemeMixDescription": "전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.", "puzzleThemePlayerGames": "플레이어 게임", "puzzleThemePlayerGamesDescription": "당신의 게임이나 다른 플레이어의 게임에서 나온 퍼즐을 찾아보세요.", "puzzleThemePuzzleDownloadInformation": "이 퍼즐들은 퍼블릭 도메인이며, {param}에서 다운로드할 수 있습니다.", @@ -389,9 +476,9 @@ "settingsCloseAccountExplanation": "정말로 계정을 닫고 싶으신가요? 계정 폐쇄는 되돌릴 수 없습니다. 절대로 다시 로그인 할 수 없습니다.", "settingsThisAccountIsClosed": "계정이 폐쇄되었습니다.", "playWithAFriend": "친구와 게임하기", - "playWithTheMachine": "체스 엔진과 게임하기", + "playWithTheMachine": "체스 엔진과 붙기", "toInviteSomeoneToPlayGiveThisUrl": "이 URL로 친구를 초대하세요", - "gameOver": "게임 종료", + "gameOver": "게임 오버", "waitingForOpponent": "상대를 기다리는 중", "orLetYourOpponentScanQrCode": "또는 상대방에게 이 QR 코드를 스캔하게 하세요", "waiting": "기다리는 중", @@ -404,8 +491,8 @@ "resign": "기권", "checkmate": "체크메이트", "stalemate": "스테일메이트", - "white": "백색", - "black": "흑색", + "white": "백", + "black": "흑", "asWhite": "백일때", "asBlack": "흑일때", "randomColor": "무작위", @@ -422,59 +509,61 @@ "variantEnding": "변형 게임 엔딩", "newOpponent": "새 상대", "yourOpponentWantsToPlayANewGameWithYou": "상대가 재대결을 원합니다", - "joinTheGame": "게임 참가", - "whitePlays": "백색 차례", - "blackPlays": "흑색 차례", + "joinTheGame": "대국 참가", + "whitePlays": "백 차례", + "blackPlays": "흑 차례", "opponentLeftChoices": "당신의 상대가 게임을 나갔습니다. 상대를 기다리거나 승리 또는 무승부 처리할 수 있습니다.", - "forceResignation": "승리 처리", - "forceDraw": "무승부 처리", + "forceResignation": "승리 취하기", + "forceDraw": "무승부 선언", "talkInChat": "건전한 채팅을 해주세요!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.", - "whiteResigned": "백색 기권", - "blackResigned": "흑색 기권", - "whiteLeftTheGame": "백색이 게임을 나갔습니다", - "blackLeftTheGame": "흑색이 게임을 나갔습니다", - "whiteDidntMove": "백이 두지 않음", - "blackDidntMove": "흑이 두지 않음", - "requestAComputerAnalysis": "컴퓨터 분석 요청", + "whiteResigned": "백이 기권하였습니다", + "blackResigned": "흑이 기권하였습니다", + "whiteLeftTheGame": "백이 게임을 나갔습니다", + "blackLeftTheGame": "흑이 게임을 나갔습니다", + "whiteDidntMove": "백이 수를 두지 않음", + "blackDidntMove": "흑이 수를 두지 않음", + "requestAComputerAnalysis": "컴퓨터 분석 요청하기", "computerAnalysis": "컴퓨터 분석", - "computerAnalysisAvailable": "컴퓨터 분석이 가능합니다.", - "computerAnalysisDisabled": "컴퓨터 분석 꺼짐", + "computerAnalysisAvailable": "컴퓨터 분석 가능", + "computerAnalysisDisabled": "컴퓨터 분석 비활성화됨", "analysis": "분석", "depthX": "{param} 수까지 탐색", "usingServerAnalysis": "서버 분석 사용하기", - "loadingEngine": "엔진 로드 중 ...", + "loadingEngine": "엔진 로드 중...", "calculatingMoves": "수 계산 중...", - "engineFailed": "엔진 로딩 에러", + "engineFailed": "엔진 불러오는 도중 오류 발생", "cloudAnalysis": "클라우드 분석", "goDeeper": "더 깊게 분석하기", - "showThreat": "위험요소 표시하기", + "showThreat": "위험요소 표시", "inLocalBrowser": "브라우저에서", - "toggleLocalEvaluation": "개인 컴퓨터에서 분석하기", - "promoteVariation": "게임 분석 후에 어떤 수에 대한 예상결과들을 확인하고 싶다면", + "toggleLocalEvaluation": "로컬 분석 전환", + "promoteVariation": "바리에이션 승격하기", "makeMainLine": "주 라인으로 하기", "deleteFromHere": "여기서부터 삭제", - "forceVariation": "변화 강제하기", - "copyVariationPgn": "변동 PGN 복사", + "collapseVariations": "바리에이션 축소하기", + "expandVariations": "바리에이션 확장하기", + "forceVariation": "바리에이션 강제하기", + "copyVariationPgn": "바리에이션 PGN 복사", "move": "수", "variantLoss": "변형 체스에서 패배", "variantWin": "변형 체스에서 승리", - "insufficientMaterial": "기물 부족으로 무승부입니다.", + "insufficientMaterial": "기물 부족", "pawnMove": "폰 이동", - "capture": "Capture", + "capture": "기물 잡기", "close": "닫기", "winning": "이기는 수", "losing": "지는 수", "drawn": "무승부", "unknown": "알 수 없음", "database": "데이터베이스", - "whiteDrawBlack": "백 : 무승부 : 흑", + "whiteDrawBlack": "백 / 무승부 / 흑", "averageRatingX": "평균 레이팅: {param}", "recentGames": "최근 게임", "topGames": "최고 레이팅 게임", "masterDbExplanation": "{param2}년과 {param3}년 사이 FIDE 레이팅이 최소 {param1}였던 선수들의 기보가 약 2백만개 있습니다.", "dtzWithRounding": "다음 포획 혹은 폰 수까지 남은 반수를 반올림후 나타낸 DTZ50\" 수치", - "noGameFound": "게임을 찾을 수 없습니다.", + "noGameFound": "게임을 찾을 수 없습니다", "maxDepthReached": "최대 깊이 도달!", "maybeIncludeMoreGamesFromThePreferencesMenu": "설정에서 더 많은 게임을 포함하세요.", "openings": "오프닝", @@ -482,32 +571,30 @@ "openingEndgameExplorer": "오프닝/엔드게임 탐색기", "xOpeningExplorer": "{param} 오프닝 탐색기", "playFirstOpeningEndgameExplorerMove": "첫 번째 오프닝/엔드게임 탐색기 수 두기", - "winPreventedBy50MoveRule": "50수 규칙에 의하여 승리가 불가능합니다.", - "lossSavedBy50MoveRule": "50수 규칙에 의하여 패배가 불가능합니다.", + "winPreventedBy50MoveRule": "50수 규칙에 의하여 승리가 불가능합니다", + "lossSavedBy50MoveRule": "50수 규칙에 의하여 패배가 불가능합니다", "winOr50MovesByPriorMistake": "승리 혹은 이전의 실수로 인한 50수 규칙 무승부", "lossOr50MovesByPriorMistake": "패배 혹은 이전의 실수로 인한 50수 규칙 무승부", "unknownDueToRounding": "DTZ 수치의 반올림 때문에 추천된 테이블베이스 라인을 따라야만 승리 및 패배가 보장됩니다.", - "allSet": "모든 설정 완료!", + "allSet": "모두 완료!", "importPgn": "PGN 가져오기", "delete": "삭제", "deleteThisImportedGame": "가져온 게임을 삭제할까요?", "replayMode": "게임 다시보기", "realtimeReplay": "실시간", - "byCPL": "평가치변화", - "openStudy": "연구를 시작하기", + "byCPL": "센티폰 손실", "enable": "활성화", "bestMoveArrow": "최선의 수 화살표", "showVariationArrows": "바리에이션 화살표 표시하기", - "evaluationGauge": "평가치 게이지", - "multipleLines": "다중 분석 수", + "evaluationGauge": "평가 게이지", + "multipleLines": "다중 라인 수", "cpus": "CPU 수", "memory": "메모리", "infiniteAnalysis": "무한 분석", "removesTheDepthLimit": "탐색 깊이 제한을 없애고 컴퓨터를 따뜻하게 해줍니다", - "engineManager": "엔진 매니저", - "blunder": "심각한 실수", + "blunder": "블런더", "mistake": "실수", - "inaccuracy": "사소한 실수", + "inaccuracy": "부정확한 수", "moveTimes": "이동 시간", "flipBoard": "보드 돌리기", "threefoldRepetition": "3회 동형반복", @@ -515,54 +602,55 @@ "offerDraw": "무승부 요청", "draw": "무승부", "drawByMutualAgreement": "상호 동의에 의한 무승부", - "fiftyMovesWithoutProgress": "진전이 없이 50수 소모", - "currentGames": "진행 중인 게임", + "fiftyMovesWithoutProgress": "진전 없이 50수 소모", + "currentGames": "진행 중인 게임들", "viewInFullSize": "크게 보기", "logOut": "로그아웃", "signIn": "로그인", "rememberMe": "로그인 유지", - "youNeedAnAccountToDoThat": "회원만이 접근할 수 있습니다.", + "youNeedAnAccountToDoThat": "회원이어야 가능합니다", "signUp": "회원 가입", - "computersAreNotAllowedToPlay": "컴퓨터나 컴퓨터의 도움을 받는 플레이어는 대국이 금지되어 있습니다. 대국할 때 체스 엔진이나 관련 자료, 또는 주변 플레이어로부터 도움을 받지 마십시오. 또한, 다중 계정 사용은 권장하지 않으며 지나치게 많은 다중 계정을 사용할 시 계정이 차단될 수 있습니다.", + "computersAreNotAllowedToPlay": "컴퓨터나 컴퓨터 지원을 받는 플레이어들은 게임 참가가 금지되어 있습니다. 게임 중 체스 엔진이나, 데이터베이스나, 주변 플레이어들로부터 도움을 받지 마십시오. 이와 더불어 다중 계정 소유는 권장하지 않으며 지나치게 많은 계정들을 사용할 시 계정들이 차단될 수 있습니다.", "games": "게임", "forum": "포럼", - "xPostedInForumY": "{param1}(이)가 {param2} 쓰레드에 글을 씀", + "xPostedInForumY": "{param1}(이)가 {param2} 주제에 글을 씀", "latestForumPosts": "최근 포럼 글", "players": "플레이어", "friends": "친구들", - "discussions": "토론", + "otherPlayers": "다른 플레이어들", + "discussions": "대화", "today": "오늘", "yesterday": "어제", - "minutesPerSide": "주어진 시간(분)", + "minutesPerSide": "제한 시간(분)", "variant": "게임 종류", "variants": "변형", "timeControl": "시간 제한", - "realTime": "짧은 대국", - "correspondence": "긴 대국", - "daysPerTurn": "한 수에 걸리는 일수", + "realTime": "실시간", + "correspondence": "통신 대국", + "daysPerTurn": "수당 일수", "oneDay": "1일", "time": "시간", "rating": "레이팅", "ratingStats": "레이팅 통계", - "username": "아이디", + "username": "사용자 이름", "usernameOrEmail": "사용자 이름이나 이메일 주소", - "changeUsername": "사용자명 변경", - "changeUsernameNotSame": "글자의 대소문자 변경만 가능합니다 예: \"johndoe\" to \"JohnDoe\".", - "changeUsernameDescription": "닉네임 변경하기: 대/소문자의 변경만이 허용되며, 단 한번만 가능한 작업입니다.", + "changeUsername": "사용자 이름 변경", + "changeUsernameNotSame": "글자의 대소문자 변경만 가능합니다 예: \"chulsoo\"에서 \"ChulSoo\"로.", + "changeUsernameDescription": "사용자명 변경하기: 대/소문자만의 변경이 허용되며, 단 한번만 가능합니다.", "signupUsernameHint": "사용자 이름이 어린이를 포함해 모두에게 적절한지 확인하세요. 나중에 변경할 수 없으며 부적절한 사용자 이름을 가진 계정은 폐쇄됩니다!", "signupEmailHint": "비밀번호 초기화를 위해서만 사용됩니다.", "password": "비밀번호", "changePassword": "비밀번호 변경", - "changeEmail": "메일 주소 변경", - "email": "메일", + "changeEmail": "이메일 주소 변경", + "email": "이메일", "passwordReset": "비밀번호 초기화", "forgotPassword": "비밀번호를 잊어버리셨나요?", - "error_weakPassword": "이 비밀번호는 매우 일반적이고 추측하기 쉽습니다.", - "error_namePassword": "사용자 아이디를 비밀번호로 사용하지 마세요.", - "blankedPassword": "다른 사이트에서 동일한 비밀번호를 사용했으며 해당 사이트가 유출된 경우. 라이선스 계정의 안전을 위해 새 비밀번호를 설정해 주셔야 합니다. 양해해 주셔서 감사합니다.", - "youAreLeavingLichess": "리체스에서 나갑니다", - "neverTypeYourPassword": "다른 사이트에서는 절대로 리체스 비밀번호를 입력하지 마세요!", - "proceedToX": "{param} 진행", + "error_weakPassword": "이 비밀번호는 매우 흔하며 추측하기 쉽습니다.", + "error_namePassword": "사용자 이름을 비밀번호로 사용하지 마세요.", + "blankedPassword": "당신이 다른 사이트에서 동일한 비밀번호를 사용했으며, 해당 사이트가 유출되었습니다. Lichess 계정의 안전을 위해 새 비밀번호를 설정해 주세요. 양해해 주셔서 감사합니다.", + "youAreLeavingLichess": "Lichess에서 나갑니다", + "neverTypeYourPassword": "다른 사이트에서는 절대로 Lichess 비밀번호를 입력하지 마세요!", + "proceedToX": "{param}로 진행", "passwordSuggestion": "다른 사람이 제안한 비밀번호를 설정하지 마세요. 타인이 계정을 도용하는 데 사용할 수 있습니다.", "emailSuggestion": "다른 사람이 추천한 이메일 주소를 설정하지 마세요. 타인이 계정을 도용하는 데 사용할 수 있습니다.", "emailConfirmHelp": "이메일 확인 도움말", @@ -570,77 +658,78 @@ "whatSignupUsername": "가입할 때 어떤 사용자 이름을 사용하셨나요?", "usernameNotFound": "사용자 이름을 찾을 수 없습니다: {param}.", "usernameCanBeUsedForNewAccount": "이 사용자 이름을 사용하여 새 계정을 만들 수 있습니다", - "emailSent": "{param}로 이메일을 전송했습니다.", - "emailCanTakeSomeTime": "도착하는데 시간이 걸릴 수 있습니다.", - "refreshInboxAfterFiveMinutes": "5분 가량 기다린 후 이메일 수신함을 새로고침하세요.", - "checkSpamFolder": "또한 스펨메일함을 확인해주시고 스펨을 해제해주세요.", - "emailForSignupHelp": "모두 실패했다면 이곳으로 메일을 보내주세요:", - "copyTextToEmail": "위의 텍스트를 복사해서 {param}로 보내주세요.", + "emailSent": "{param}(으)로 이메일을 전송했습니다.", + "emailCanTakeSomeTime": "이메일이 도착하는데 시간이 좀 걸릴 수 있습니다.", + "refreshInboxAfterFiveMinutes": "5분 가량 기다린 후 이메일 수신함을 새로고침 해주세요.", + "checkSpamFolder": "또한 스팸 메일함에 들어가 있을 수 있습니다. 만약 그런 경우, 스팸이 아님으로 표시해 두세요.", + "emailForSignupHelp": "모두 실패했다면, 이곳으로 메일을 보내주세요:", + "copyTextToEmail": "위의 텍스트를 복사해서 {param}(으)로 보내주세요", "waitForSignupHelp": "가입을 완료할 수 있도록 빠르게 연락드리겠습니다.", - "accountConfirmed": "{param} 사용자가 성공적으로 확인되었습니다.", - "accountCanLogin": "이제 {param}로 로그인할 수 있습니다.", + "accountConfirmed": "유저 {param}(이)가 성공적으로 확인되었습니다.", + "accountCanLogin": "이제 {param}(으)로 로그인할 수 있습니다.", "accountConfirmationEmailNotNeeded": "이메일 확인은 필요하지 않습니다.", - "accountClosed": "{param} 계정은 폐쇄되었습니다.", + "accountClosed": "계정 {param}(은)는 폐쇄되었습니다.", "accountRegisteredWithoutEmail": "{param} 계정은 이메일 주소가 없이 등록되었습니다.", "rank": "순위", "rankX": "순위: {param}등", "gamesPlayed": "게임", + "ok": "확인", "cancel": "취소", - "whiteTimeOut": "백색 시간 초과", - "blackTimeOut": "흑색 시간 초과", - "drawOfferSent": "무승부를 요청했습니다", - "drawOfferAccepted": "무승부 요청이 승낙됐습니다", - "drawOfferCanceled": "무승부 요청을 취소했습니다", - "whiteOffersDraw": "백이 무승부를 제안했습니다", - "blackOffersDraw": "흑이 무승부를 제안했습니다", - "whiteDeclinesDraw": "백이 무승부 제안을 거절했습니다", - "blackDeclinesDraw": "흑이 무승부 제안을 거절했습니다", - "yourOpponentOffersADraw": "상대가 무승부를 요청했습니다", - "accept": "승낙", + "whiteTimeOut": "백 시간 초과", + "blackTimeOut": "흑 시간 초과", + "drawOfferSent": "무승부 요청함", + "drawOfferAccepted": "무승부 요청 수락됨", + "drawOfferCanceled": "무승부 요청 취소함", + "whiteOffersDraw": "백이 무승부를 제안합니다", + "blackOffersDraw": "흑이 무승부를 제안합니다", + "whiteDeclinesDraw": "백이 무승부 제안을 거절하였습니다", + "blackDeclinesDraw": "흑이 무승부 제안을 거절하였습니다", + "yourOpponentOffersADraw": "상대가 무승부를 요청합니다", + "accept": "수락", "decline": "거절", - "playingRightNow": "대국 중", + "playingRightNow": "지금 대국 중", "eventInProgress": "지금 대국 중", - "finished": "종료", + "finished": "종료됨", "abortGame": "게임 중단", "gameAborted": "게임 중단됨", - "standard": "표준", - "customPosition": "사용자 지정 포지션", + "standard": "스탠다드", + "customPosition": "커스텀 포지션", "unlimited": "무제한", "mode": "모드", "casual": "캐주얼", "rated": "레이팅", - "casualTournament": "일반", + "casualTournament": "캐주얼", "ratedTournament": "레이팅", "thisGameIsRated": "이 게임은 레이팅 게임입니다", "rematch": "재대결", - "rematchOfferSent": "재대결 요청을 보냈습니다", - "rematchOfferAccepted": "재대결 요청이 승낙됐습니다", - "rematchOfferCanceled": "재대결 요청이 취소됐습니다", + "rematchOfferSent": "재대결 요청 전송됨", + "rematchOfferAccepted": "재대결 요청 승낙됨", + "rematchOfferCanceled": "재대결 요청 취소됨", "rematchOfferDeclined": "재대결 요청이 거절됐습니다", "cancelRematchOffer": "재대결 요청 취소", - "viewRematch": "재대결 보러 가기", + "viewRematch": "재대결 보기", "confirmMove": "수 확인", "play": "플레이", - "inbox": "받은편지함", + "inbox": "편지함", "chatRoom": "채팅", - "loginToChat": "채팅에 로그인하기", - "youHaveBeenTimedOut": "채팅에서 로그아웃 되었습니다.", + "loginToChat": "채팅하려면 로그인하세요", + "youHaveBeenTimedOut": "채팅에서 타임아웃 되었습니다.", "spectatorRoom": "관전자 채팅", "composeMessage": "메시지 작성", "subject": "제목", "send": "전송", - "incrementInSeconds": "턴 당 추가 시간(초)", + "incrementInSeconds": "수 당 추가 시간(초)", "freeOnlineChess": "무료 온라인 체스", "exportGames": "게임 내보내기", - "ratingRange": "ELO 범위", - "thisAccountViolatedTos": "이 계정은 Lichess 이용 약관을 위반했습니다.", + "ratingRange": "레이팅 범위", + "thisAccountViolatedTos": "이 계정은 Lichess 이용 약관을 위반하였습니다", "openingExplorerAndTablebase": "오프닝 탐색 & 테이블베이스", "takeback": "무르기", - "proposeATakeback": "무르기를 요청합니다", - "takebackPropositionSent": "무르기 요청을 보냈습니다", - "takebackPropositionDeclined": "무르기 요청이 거절됐습니다", - "takebackPropositionAccepted": "무르기 요청이 승낙됐습니다", - "takebackPropositionCanceled": "무르기 요청이 취소됐습니다", + "proposeATakeback": "무르기 요청", + "takebackPropositionSent": "무르기 요청 전송됨", + "takebackPropositionDeclined": "무르기 요청 거절됨", + "takebackPropositionAccepted": "무르기 요청 승낙됨", + "takebackPropositionCanceled": "무르기 요청 취소됨", "yourOpponentProposesATakeback": "상대가 무르기를 요청합니다", "bookmarkThisGame": "이 게임을 즐겨찾기에 추가하기", "tournament": "토너먼트", @@ -665,10 +754,10 @@ "startedStreaming": "스트리밍 시작", "xStartedStreaming": "{param} 님이 스트리밍을 시작했습니다", "averageElo": "평균 레이팅", - "location": "주소", - "filterGames": "필터", + "location": "위치", + "filterGames": "대국 필터", "reset": "초기화", - "apply": "적용", + "apply": "저장", "save": "저장하기", "leaderboard": "리더보드", "screenshotCurrentPosition": "스크린샷 찍기", @@ -681,7 +770,7 @@ "toStudy": "연구", "importGame": "게임 불러오기", "importGameExplanation": "게임의 PGN 을 붙여넣으면, 브라우저에서의 리플레이, 컴퓨터 해석, 게임챗, 공유가능 URL을 얻습니다.", - "importGameCaveat": "변형은 지워집니다. 변형을 유지하려면 스터디를 통해 PGN을 가져오세요.", + "importGameCaveat": "변형은 지워집니다. 변형을 유지하려면 연구를 통해 PGN을 가져오세요.", "importGameDataPrivacyWarning": "이 PGN은 모두가 볼 수 있게 됩니다. 비공개로 게임을 불러오려면, 연구 기능을 이용하세요.", "thisIsAChessCaptcha": "자동 기입을 방지하기 위한 체스 퀴즈입니다.", "clickOnTheBoardToMakeYourMove": "보드를 클릭해서 체스 퍼즐을 풀고 당신이 사람임을 알려주세요.", @@ -690,7 +779,7 @@ "whiteCheckmatesInOneMove": "백이 한 수 만에 체크메이트하기", "blackCheckmatesInOneMove": "흑이 한 수 만에 체크메이트하기", "retry": "재시도", - "reconnecting": "연결 재시도 중", + "reconnecting": "연결 중", "noNetwork": "오프라인", "favoriteOpponents": "관심있는 상대", "follow": "팔로우", @@ -701,7 +790,6 @@ "block": "차단", "blocked": "차단됨", "unblock": "차단 해제", - "followsYou": "팔로워", "xStartedFollowingY": "{param1}(이)가 {param2}(을)를 팔로우했습니다", "more": "더보기", "memberSince": "가입 시기:", @@ -735,7 +823,7 @@ "resume": "재개", "youArePlaying": "참가중!", "winRate": "승률", - "berserkRate": "버서크율", + "berserkRate": "버서크 비율", "performance": "퍼포먼스 레이팅", "tournamentComplete": "대회 종료", "movesPlayed": "말 이동 횟수", @@ -757,17 +845,20 @@ "reportXToModerators": "{param} 신고", "profileCompletion": "프로필 완성도: {param}", "xRating": "레이팅: {param}", - "ifNoneLeaveEmpty": "없으면 무시하세요", + "ifNoneLeaveEmpty": "없다면 비워두세요", "profile": "프로필", "editProfile": "프로필 수정", - "realName": "본명", + "realName": "실명", + "setFlair": "아이콘을 선택하세요", + "flair": "아이콘", + "youCanHideFlair": "전체 사이트에서 모든 유저의 아이콘을 숨기는 설정이 있습니다.", "biography": "소개", - "countryRegion": "국가/지역", + "countryRegion": "국가 또는 지역", "thankYou": "감사합니다!", "socialMediaLinks": "소셜 미디어 링크", - "oneUrlPerLine": "한 줄에 1개 URL", - "inlineNotation": "기보를 가로쓰기", - "makeAStudy": "안전하게 보관하고 공유하려면 스터디를 만들어 보세요.", + "oneUrlPerLine": "한 줄에 당 URL 1개", + "inlineNotation": "기보법 가로쓰기", + "makeAStudy": "안전하게 보관하고 공유하려면 연구를 만들어 보세요.", "clearSavedMoves": "저장된 움직임 삭제", "previouslyOnLichessTV": "이전 방송", "onlinePlayers": "접속한 플레이어", @@ -777,15 +868,18 @@ "automaticallyProceedToNextGameAfterMoving": "수를 둔 다음에 자동으로 다음 게임에 이동", "autoSwitch": "자동 전환", "puzzles": "퍼즐", + "onlineBots": "온라인 봇", "name": "이름", "description": "설명", "descPrivate": "비공개 설명", "descPrivateHelp": "팀 멤버만 볼 수 있는 텍스트입니다. 설정된다면, 팀 멤버에게는 공개 설명 대신 보이게 됩니다.", "no": "아니오", "yes": "예", + "website": "웹사이트", + "mobile": "모바일", "help": "힌트:", - "createANewTopic": "새 토픽", - "topics": "토픽", + "createANewTopic": "새 주제 만들기", + "topics": "주제", "posts": "글", "lastPost": "최근 글", "views": "조회", @@ -793,7 +887,7 @@ "replyToThisTopic": "답글 달기", "reply": "전송", "message": "내용", - "createTheTopic": "새 토픽 생성", + "createTheTopic": "새 주제 생성", "reportAUser": "사용자 신고", "user": "신고할 사용자 이름", "reason": "이유", @@ -801,11 +895,13 @@ "cheat": "부정행위", "troll": "분란 조장", "other": "기타", - "reportDescriptionHelp": "게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요.", + "reportCheatBoostHelp": "게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요. 그냥 \"그들이 부정행위를 했어요\" 라고만 말하지 말고, 어떻게 당신이 이 결론에 도달하게 됐는지 알려주세요.", + "reportUsernameHelp": "왜 이 사용자의 이름이 불쾌한지 설명해주세요. 그저 \"불쾌해요/부적절해요\"라고만 말하지 마세요, 대신 왜 이런 결론에 도달했는지 말씀해 주세요. 단어가 난해하거나, 영어가 아니거나, 은어이거나, 문화적/역사적 배경이 있는 경우 특히 중요합니다.", + "reportProcessedFasterInEnglish": "귀하의 신고가 영어로 적혀있을 경우 빠르게 처리될 것입니다.", "error_provideOneCheatedGameLink": "부정행위가 존재하는 게임의 링크를 적어도 하나는 적어주세요.", "by": "작성: {param}", "importedByX": "{param}가 불러옴", - "thisTopicIsNowClosed": "이 토픽은 닫혔습니다.", + "thisTopicIsNowClosed": "이 주제는 닫혔습니다.", "blog": "블로그", "notes": "노트", "typePrivateNotesHere": "여기에 비공개 메모 작성하기", @@ -821,12 +917,12 @@ "newPasswordsDontMatch": "새로운 비밀번호가 일치하지 않습니다", "newPasswordStrength": "비밀번호 강도", "clockInitialTime": "기본 시간", - "clockIncrement": "한 수당 증가하는 시간", + "clockIncrement": "수 당 추가 시간", "privacy": "보안", "privacyPolicy": "개인정보취급방침", "letOtherPlayersFollowYou": "다른 사람이 팔로우할 수 있게 함", "letOtherPlayersChallengeYou": "다른 사람이 나에게 도전할 수 있게 함", - "letOtherPlayersInviteYouToStudy": "다른 플레이어들이 나를 학습에 초대할 수 있음", + "letOtherPlayersInviteYouToStudy": "다른 플레이어들이 나를 연구에 초대할 수 있음", "sound": "소리", "none": "없음", "fast": "빠르게", @@ -834,6 +930,7 @@ "slow": "느리게", "insideTheBoard": "보드 안쪽에", "outsideTheBoard": "보드 바깥쪽에", + "allSquaresOfTheBoard": "보드의 모든 칸", "onSlowGames": "느린 게임에서만", "always": "항상", "never": "안 함", @@ -849,8 +946,8 @@ "biographyDescription": "자신을 알려주세요. 왜 체스를 좋아하는지, 좋아하는 오프닝, 게임, 선수 등등...", "listBlockedPlayers": "이 플레이어를 차단", "human": "인간", - "computer": "인공지능", - "side": "진영", + "computer": "컴퓨터", + "side": "색", "clock": "시계", "opponent": "상대", "learnMenu": "배우기", @@ -858,11 +955,11 @@ "practice": "연습", "community": "커뮤니티", "tools": "도구", - "increment": "시간 증가", + "increment": "추가 시간", "error_unknown": "잘못된 값", "error_required": "필수 기입 사항입니다.", "error_email": "이메일 주소가 유효하지 않습니다", - "error_email_acceptable": "이 이메일 주소는 받을 수 없습니다. 다시 확인후 시도해주세요.", + "error_email_acceptable": "이 이메일 주소는 수용 불가합니다. 확인후 다시 시도해주세요.", "error_email_unique": "이메일 주소가 유효하지 않거나 이미 등록되었습니다", "error_email_different": "이미 당신의 이메일 주소입니다.", "error_minLength": "최소 {param}자여야 합니다.", @@ -892,21 +989,21 @@ "contribute": "기여하기", "termsOfService": "이용 약관", "sourceCode": "소스 코드", - "simultaneousExhibitions": "동시대국", - "host": "호스트", - "hostColorX": "호스트의 색: {param}", - "yourPendingSimuls": "대기 중인 동시대국", - "createdSimuls": "새롭게 생성된 동시대국", - "hostANewSimul": "새 동시대국을 생성하기", - "signUpToHostOrJoinASimul": "동시대국을 생성/참가하려면 로그인하세요", - "noSimulFound": "동시대국을 찾을 수 없습니다", - "noSimulExplanation": "존재하지 않는 동시대국입니다.", - "returnToSimulHomepage": "동시대국 홈으로 돌아가기", - "aboutSimul": "동시대국에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.", - "aboutSimulImage": "50명의 상대 중, 피셔는 47국을 승리하였고, 2국은 무승부였으며 1국만을 패배하였습니다.", - "aboutSimulRealLife": "이 동시대국의 개념은 실제 동시대국과 동일합니다. 실제로 1인 플레이어는 테이블을 넘기며 한 수씩 둡니다.", - "aboutSimulRules": "동시대국이 시작되면 모든 플레이어가 호스트와 게임을 합니다. 동시대국은 모든 플레이어와 게임이 끝나면 종료됩니다.", - "aboutSimulSettings": "동시대국은 캐주얼 전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.", + "simultaneousExhibitions": "다면기", + "host": "주최자", + "hostColorX": "주최자 색: {param}", + "yourPendingSimuls": "대기 중인 다면기", + "createdSimuls": "새롭게 생성된 다면기", + "hostANewSimul": "새 다면기 주최하기", + "signUpToHostOrJoinASimul": "다면기를 주최/참가하려면 로그인하세요", + "noSimulFound": "다면기를 찾을 수 없습니다", + "noSimulExplanation": "존재하지 않는 다면기입니다.", + "returnToSimulHomepage": "다면기 홈으로 돌아가기", + "aboutSimul": "다면기에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.", + "aboutSimulImage": "피셔는 50명의 상대 중, 47국을 승리하였고, 2국은 무승부였으며 1국만 패하였습니다.", + "aboutSimulRealLife": "이 컨셉은 실제 이벤트들을 본딴 것입니다. 실제 경기에서는 다면기 주최자가 테이블을 돌아다니며 한 수씩 둡니다.", + "aboutSimulRules": "다면기가 시작되면, 모든 플레이어가 주최자와 대국을 합니다. 다면기는 모든 플레이어와 게임이 끝나면 종료됩니다.", + "aboutSimulSettings": "다면기는 항상 캐주얼전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.", "create": "생성", "whenCreateSimul": "동시대국을 생성하면 한 번에 여러 명의 플레이어와 게임하게 됩니다.", "simulVariantsHint": "복수의 게임방식을 선택할 경우, 상대방 측에서 게임 방식을 선택하게 됩니다.", @@ -923,6 +1020,7 @@ "keyboardShortcuts": "키보드 단축키", "keyMoveBackwardOrForward": "뒤로/앞으로 가기", "keyGoToStartOrEnd": "처음/끝으로 가기", + "keyCycleSelectedVariation": "선택된 바리에이션 순환하기", "keyShowOrHideComments": "댓글 표시/숨기기", "keyEnterOrExitVariation": "바리에이션 들어가기/나오기", "keyRequestComputerAnalysis": "컴퓨터 분석 요청, 실수에서 배우기", @@ -930,10 +1028,16 @@ "keyNextBlunder": "다음 블런더", "keyNextMistake": "다음 실수", "keyNextInaccuracy": "다음 부정확한 수", + "keyPreviousBranch": "이전 부", + "keyNextBranch": "다음 부", + "toggleVariationArrows": "바리에이션 화살표 표시하기", + "cyclePreviousOrNextVariation": "이전/다음 바리에이션 순환하기", + "toggleGlyphAnnotations": "이동 주석 토글하기", + "togglePositionAnnotations": "위치 주석 토글하기", "variationArrowsInfo": "변형 화살표를 사용하면 이동 목록을 사용하지 않고 탐색이 가능합니다.", "playSelectedMove": "선택한 수 두기", "newTournament": "새로운 토너먼트", - "tournamentHomeTitle": "다양한 제한시간과 게임방식을 지원하는 체스 토너먼트", + "tournamentHomeTitle": "다양한 시간 제한과 변형을 지원하는 체스 토너먼트", "tournamentHomeDescription": "빠른 체스 토너먼트를 즐겨 보세요! 공식 일정이 잡힌 토너먼트에 참가할 수도, 당신만의 토너먼트를 만들 수도 있습니다. 불릿, 블리츠, 클래식, 체스960, 언덕의 왕, 3체크를 비롯하여 다양한 게임방식을 즐길 수 있습니다.", "tournamentNotFound": "토너먼트를 찾을 수 없습니다", "tournamentDoesNotExist": "존재하지 않는 토너먼트입니다.", @@ -980,16 +1084,16 @@ "sessions": "세션", "revokeAllSessions": "모든 세션 비활성화", "playChessEverywhere": "어디에서나 체스를 즐기세요", - "asFreeAsLichess": "lichess처럼 무료입니다", + "asFreeAsLichess": "Lichess처럼 무료예요", "builtForTheLoveOfChessNotMoney": "오직 체스에 대한 열정으로 만들어졌습니다", "everybodyGetsAllFeaturesForFree": "모두가 모든 기능을 무료로 이용할 수 있습니다", "zeroAdvertisement": "광고가 없습니다", "fullFeatured": "모든 기능을 지원합니다", "phoneAndTablet": "스마트폰과 태블릿 지원", "bulletBlitzClassical": "불릿, 블리츠, 클래식 방식 지원", - "correspondenceChess": "우편 체스 지원", + "correspondenceChess": "통신 체스", "onlineAndOfflinePlay": "온라인/오프라인 게임 모두 지원", - "viewTheSolution": "해답 보기", + "viewTheSolution": "정답 보기", "followAndChallengeFriends": "친구를 팔로우하고 도전하기", "gameAnalysis": "게임 분석기", "xHostsY": "{param2}가 {param1}를 시작했습니다.", @@ -1020,7 +1124,7 @@ "usernameCharsInvalid": "유저 이름에는 알파벳, 숫자, 언더스코어 ( _ ), 하이픈 ( - ) 만을 사용할 수 있습니다.", "usernameUnacceptable": "사용자 이름을 사용할 수 없습니다.", "playChessInStyle": "스타일리시하게 체스하기", - "chessBasics": "체스 기본", + "chessBasics": "체스의 기본", "coaches": "코치", "invalidPgn": "잘못된 PGN입니다.", "invalidFen": "잘못된 FEN입니다.", @@ -1051,18 +1155,18 @@ "findBetterMoveForBlack": "검은색에게 좀 더 나은 수를 찾아보세요", "resumeLearning": "학습 계속하기", "youCanDoBetter": "더 잘할 수 있어요", - "tryAnotherMoveForWhite": "흰색에게 또 다른 수를 찾아보세요", - "tryAnotherMoveForBlack": "검은색에게 또 다른 수를 찾아보세요", + "tryAnotherMoveForWhite": "백의 또 다른 수를 찾아보세요", + "tryAnotherMoveForBlack": "흑의 또 다른 수를 찾아보세요", "solution": "해답", "waitingForAnalysis": "분석을 기다리는 중", - "noMistakesFoundForWhite": "백에게 악수는 없었습니다", - "noMistakesFoundForBlack": "흑에게 악수는 없었습니다", - "doneReviewingWhiteMistakes": "백의 악수 체크가 종료됨", - "doneReviewingBlackMistakes": "흑의 악수 체크가 종료됨", + "noMistakesFoundForWhite": "백에게 실수는 없었습니다", + "noMistakesFoundForBlack": "흑에게 실수는 없었습니다", + "doneReviewingWhiteMistakes": "백의 실수 탐색이 종료됨", + "doneReviewingBlackMistakes": "흑의 실수 탐색이 종료됨", "doItAgain": "다시 하기", - "reviewWhiteMistakes": "백의 악수를 체크", - "reviewBlackMistakes": "흑의 악수를 체크", - "advantage": "이득", + "reviewWhiteMistakes": "백의 실수 탐색하기", + "reviewBlackMistakes": "흑의 실수 탐색하기", + "advantage": "이점", "opening": "오프닝", "middlegame": "미들게임", "endgame": "엔드게임", @@ -1071,7 +1175,7 @@ "playVariationToCreateConditionalPremoves": "기물을 움직여 조건적인 수를 만들기", "noConditionalPremoves": "조건적인 수가 없습니다", "playX": "{param} 를 둠", - "showUnreadLichessMessage": "리체스로부터 비공개 메시지를 받았습니다.", + "showUnreadLichessMessage": "Lichess로부터 비공개 메시지를 받았습니다.", "clickHereToReadIt": "클릭하여 읽기", "sorry": "죄송합니다 :(", "weHadToTimeYouOutForAWhile": "짧은 시간동안 정지를 받으셨습니다.", @@ -1103,14 +1207,14 @@ "blitzDesc": "빠른 게임: 3에서 8분", "rapidDesc": "래피드 게임: 8 ~ 25분", "classicalDesc": "클래시컬 게임: 25분 이상", - "correspondenceDesc": "통신전: 한 수당 하루 또는 수 일", + "correspondenceDesc": "통신 대국: 한 수당 하루 또는 며칠", "puzzleDesc": "체스 전술 트레이너", "important": "중요!", "yourQuestionMayHaveBeenAnswered": "{param1}에 원하시는 답변이 있을 수 있습니다.", "inTheFAQ": "F.A.Q", "toReportSomeoneForCheatingOrBadBehavior": "{param1}에서 엔진 사용이나 부적절한 행동을 신고하십시오.", "useTheReportForm": "사용자 신고", - "toRequestSupport": "{param1}에서 리체스에 문의하실 수 있습니다.", + "toRequestSupport": "{param1}에서 문의하실 수 있습니다.", "tryTheContactPage": "연락처", "makeSureToRead": "{param1}를 꼭 읽으세요", "theForumEtiquette": "포럼 에티켓", @@ -1132,16 +1236,16 @@ "resVsX": "{param1} vs {param2}", "lostAgainstTOSViolator": "당신은 Lichess의 서비스 약관을 어긴 플레이어에게 패배했습니다.", "refundXpointsTimeControlY": "환불: {param2} 레이팅 포인트 {param1}점", - "timeAlmostUp": "시간이 거의 다 되었습니다!", + "timeAlmostUp": "시간이 거의 다 되었어요!", "clickToRevealEmailAddress": "[이메일 주소를 보려면 클릭]", "download": "다운로드", "coachManager": "코치 설정", "streamerManager": "스트리머 설정", - "cancelTournament": "토너먼트 취소", + "cancelTournament": "토너먼트 취소하기", "tournDescription": "토너먼트 설명", "tournDescriptionHelp": "참가자에게 하고 싶은 말이 있나요? 짧게 작성해주세요. 마크다운 링크가 가능합니다: [name](https://url)", "ratedFormHelp": "레이팅 게임을 합니다\n플레이어 레이팅에 영향을 줍니다", - "onlyMembersOfTeam": "팀 멤버만", + "onlyMembersOfTeam": "팀 멤버들만", "noRestriction": "제한 없음", "minimumRatedGames": "최소 레이팅 게임 참여 횟수", "minimumRating": "최소 레이팅", @@ -1155,13 +1259,13 @@ "simulDescription": "동시대국 설명", "simulDescriptionHelp": "참가자들에게 하고 싶은 말이 있나요?", "markdownAvailable": "추가로 {param} 문법을 사용하실 수 있습니다.", - "embedsAvailable": "포함할 게임 URL 또는 스터디 챕터 URL을 붙여넣으세요.", + "embedsAvailable": "포함할 게임 URL 또는 연구 챕터 URL을 붙여넣으세요.", "inYourLocalTimezone": "본인의 현지 시간대 기준", "tournChat": "토너먼트 채팅", "noChat": "채팅 없음", "onlyTeamLeaders": "팀 리더만", "onlyTeamMembers": "팀 멤버만", - "navigateMoveTree": "수 탐색", + "navigateMoveTree": "수의 나무 탐색", "mouseTricks": "마우스 기능", "toggleLocalAnalysis": "로컬 컴퓨터 분석 켜기/끄기", "toggleAllAnalysis": "모든 컴퓨터 분석 켜기/끄기", @@ -1169,21 +1273,21 @@ "analysisOptions": "분석 옵션", "focusChat": "채팅에 포커스 주기", "showHelpDialog": "이 도움말 보기", - "reopenYourAccount": "계정 다시 활성화", + "reopenYourAccount": "계정 재활성화", "closedAccountChangedMind": "계정을 폐쇄한 후 마음이 바뀌었다면, 계정을 다시 활성화할 수 있는 기회가 한 번 있습니다.", "onlyWorksOnce": "단 한번만 가능합니다.", "cantDoThisTwice": "계정을 두 번째로 폐쇄했다면 복구할 방법이 없습니다.", "emailAssociatedToaccount": "계정에 등록된 이메일 주소", "sentEmailWithLink": "링크가 포함된 이메일을 보냈습니다.", "tournamentEntryCode": "토너먼트 입장 코드", - "hangOn": "잠깐!", - "gameInProgress": "{param}와 진행중인 게임이 있습니다.", + "hangOn": "잠깐만요!", + "gameInProgress": "{param}와(과) 진행중인 대국이 있습니다.", "abortTheGame": "게임 중단", "resignTheGame": "게임 기권", "youCantStartNewGame": "이 게임이 끝나기 전까지 새 게임을 시작할 수 없습니다.", "since": "부터", "until": "까지", - "lichessDbExplanation": "모든 리체스 플레이어의 레이팅 게임 샘플", + "lichessDbExplanation": "모든 Lichess 플레이어의 레이팅 게임 샘플", "switchSides": "색 바꾸기", "closingAccountWithdrawAppeal": "계정을 폐쇄하면 이의 제기는 자동으로 취소됩니다", "ourEventTips": "이벤트 준비를 위한 팁", @@ -1191,30 +1295,31 @@ "showMeEverything": "모두 보기", "lichessPatronInfo": "Lichess는 비영리 기구이며 완전한 무료/자유 오픈소스 소프트웨어입니다.\n모든 운영 비용, 개발, 컨텐츠 조달은 전적으로 사용자들의 기부로 이루어집니다.", "nothingToSeeHere": "지금은 여기에 볼 것이 없습니다.", - "opponentLeftCounter": "{count, plural, other{당신의 상대가 게임을 나갔습니다. {count} 초 후에 승리를 주장할 수 있습니다.}}", - "mateInXHalfMoves": "{count, plural, other{{count}반수만에 체크메이트}}", + "stats": "통계", + "opponentLeftCounter": "{count, plural, other{상대방이 게임을 나갔습니다. {count}초 후에 승리를 취할 수 있습니다.}}", + "mateInXHalfMoves": "{count, plural, other{{count}개의 반수 후 체크메이트}}", "nbBlunders": "{count, plural, other{{count} 블런더}}", "nbMistakes": "{count, plural, other{{count} 실수}}", - "nbInaccuracies": "{count, plural, other{{count} 사소한 실수}}", - "nbPlayers": "{count, plural, other{{count}명의 플레이어}}", - "nbGames": "{count, plural, other{{count}개의 게임}}", + "nbInaccuracies": "{count, plural, other{{count} 부정확한 수}}", + "nbPlayers": "{count, plural, other{플레이어 {count}명}}", + "nbGames": "{count, plural, other{게임 {count}개}}", "ratingXOverYGames": "{count, plural, other{{param2}게임간의 {count} 레이팅}}", "nbBookmarks": "{count, plural, other{{count}개의 즐겨찾기}}", "nbDays": "{count, plural, other{{count}일}}", "nbHours": "{count, plural, other{{count}시간}}", "nbMinutes": "{count, plural, other{{count}분}}", - "rankIsUpdatedEveryNbMinutes": "{count, plural, other{순위는 매 {count}분마다 갱신됩니다.}}", + "rankIsUpdatedEveryNbMinutes": "{count, plural, other{순위는 매 {count}분마다 갱신됩니다}}", "nbPuzzles": "{count, plural, other{퍼즐 {count}개}}", "nbGamesWithYou": "{count, plural, other{나와 {count}번 대국 함}}", - "nbRated": "{count, plural, other{{count}번의 레이팅 대국}}", + "nbRated": "{count, plural, other{{count}번의 레이팅 게임}}", "nbWins": "{count, plural, other{{count}번 승리}}", "nbLosses": "{count, plural, other{{count}번 패배}}", "nbDraws": "{count, plural, other{{count}번 비김}}", "nbPlaying": "{count, plural, other{플레이 중인 게임 {count}개}}", "giveNbSeconds": "{count, plural, other{{count}초 더 주기}}", - "nbTournamentPoints": "{count, plural, other{{count} 토너먼트 포인트}}", + "nbTournamentPoints": "{count, plural, other{{count} 토너먼트 점수}}", "nbStudies": "{count, plural, other{{count} 연구}}", - "nbSimuls": "{count, plural, other{{count} 동시대국}}", + "nbSimuls": "{count, plural, other{{count} 다면기}}", "moreThanNbRatedGames": "{count, plural, other{레이팅전 {count} 국 이상}}", "moreThanNbPerfRatedGames": "{count, plural, other{{param2} 레이팅전 {count} 국 이상}}", "needNbMorePerfGames": "{count, plural, other{{param2} 랭크 게임을 {count}회 더 플레이해야합니다.}}", @@ -1229,7 +1334,7 @@ "blocks": "{count, plural, other{차단한 사람 {count}명}}", "nbForumPosts": "{count, plural, other{{count} 포럼 글}}", "nbPerfTypePlayersThisWeek": "{count, plural, other{이번 주의 {param2} 플레이어는 {count}명입니다.}}", - "availableInNbLanguages": "{count, plural, other{{count}개의 언어 지원!}}", + "availableInNbLanguages": "{count, plural, other{{count}개의 언어를 지원합니다!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, other{{count} 초 안에 첫 수를 두십시오.}}", "nbSeconds": "{count, plural, other{{count}초}}", "andSaveNbPremoveLines": "{count, plural, other{{count} 종류의 조건 수를 설정}}", @@ -1287,6 +1392,178 @@ "stormXRuns": "{count, plural, other{{count}번 도전}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{{param2} 중{count}개 플레이함}}", "streamerLichessStreamers": "Lichess 스트리머", + "studyPrivate": "비공개", + "studyMyStudies": "내 연구", + "studyStudiesIContributeTo": "내가 기여한 연구", + "studyMyPublicStudies": "내 공개 연구", + "studyMyPrivateStudies": "내 비공개 연구", + "studyMyFavoriteStudies": "내가 즐겨찾는 연구", + "studyWhatAreStudies": "연구란 무엇인가요?", + "studyAllStudies": "모든 연구", + "studyStudiesCreatedByX": "{param}이(가) 만든 연구", + "studyNoneYet": "아직 없음", + "studyHot": "인기있는", + "studyDateAddedNewest": "추가된 날짜(새로운 순)", + "studyDateAddedOldest": "추가된 날짜(오래된 순)", + "studyRecentlyUpdated": "최근에 업데이트된 순", + "studyMostPopular": "인기 많은 순", + "studyAlphabetical": "알파벳 순", + "studyAddNewChapter": "새 챕터 추가하기", + "studyAddMembers": "멤버 추가", + "studyInviteToTheStudy": "연구에 초대", + "studyPleaseOnlyInvitePeopleYouKnow": "당신이 아는 사람들이나 연구에 적극적으로 참여하고 싶은 사람들만 초대하세요.", + "studySearchByUsername": "사용자 이름으로 검색", + "studySpectator": "관전자", + "studyContributor": "기여자", + "studyKick": "강제 퇴장", + "studyLeaveTheStudy": "연구 나가기", + "studyYouAreNowAContributor": "당신은 이제 기여자입니다", + "studyYouAreNowASpectator": "당신은 이제 관전자입니다", + "studyPgnTags": "PGN 태그", + "studyLike": "좋아요", + "studyUnlike": "좋아요 취소", + "studyNewTag": "새 태그", + "studyCommentThisPosition": "이 포지션에 댓글 달기", + "studyCommentThisMove": "이 수에 댓글 달기", + "studyAnnotateWithGlyphs": "기호로 주석 달기", + "studyTheChapterIsTooShortToBeAnalysed": "분석되기 너무 짧은 챕터입니다.", + "studyOnlyContributorsCanRequestAnalysis": "연구 기여자만이 컴퓨터 분석을 요청할 수 있습니다.", + "studyGetAFullComputerAnalysis": "메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.", + "studyMakeSureTheChapterIsComplete": "챕터가 완료되었는지 확인하세요. 분석은 한번만 요청할 수 있습니다.", + "studyAllSyncMembersRemainOnTheSamePosition": "동기화된 모든 멤버들은 같은 포지션을 공유합니다", + "studyShareChanges": "관전자와 변경 사항을 공유하고 서버에 저장", + "studyPlaying": "대국 중", + "studyShowEvalBar": "평가 막대", + "studyFirst": "처음", + "studyPrevious": "이전", + "studyNext": "다음", + "studyLast": "마지막", "studyShareAndExport": "공유 및 내보내기", - "studyStart": "시작" + "studyCloneStudy": "복제", + "studyStudyPgn": "연구 PGN", + "studyDownloadAllGames": "모든 게임 다운로드", + "studyChapterPgn": "챕터 PGN", + "studyCopyChapterPgn": "PGN 복사", + "studyDownloadGame": "게임 다운로드", + "studyStudyUrl": "연구 URL", + "studyCurrentChapterUrl": "현재 챕터 URL", + "studyYouCanPasteThisInTheForumToEmbed": "포럼에 공유하려면 이 주소를 붙여넣으세요", + "studyStartAtInitialPosition": "처음 포지션에서 시작", + "studyStartAtX": "{param}에서 시작", + "studyEmbedInYourWebsite": "웹사이트 또는 블로그에 공유하기", + "studyReadMoreAboutEmbedding": "공유에 대한 상세 정보", + "studyOnlyPublicStudiesCanBeEmbedded": "공개 연구만 공유할 수 있습니다!", + "studyOpen": "열기", + "studyXBroughtToYouByY": "{param1}. {param2}에서 가져옴", + "studyStudyNotFound": "연구를 찾을 수 없음", + "studyEditChapter": "챕터 편집하기", + "studyNewChapter": "새 챕터", + "studyImportFromChapterX": "{param}에서 가져오기", + "studyOrientation": "방향", + "studyAnalysisMode": "분석 모드", + "studyPinnedChapterComment": "챕터 댓글 고정하기", + "studySaveChapter": "챕터 저장", + "studyClearAnnotations": "주석 지우기", + "studyClearVariations": "바리에이션 초기화", + "studyDeleteChapter": "챕터 지우기", + "studyDeleteThisChapter": "이 챕터를 지울까요? 되돌릴 수 없습니다!", + "studyClearAllCommentsInThisChapter": "이 챕터의 모든 코멘트와 기호를 지울까요?", + "studyRightUnderTheBoard": "보드 우하단에", + "studyNoPinnedComment": "없음", + "studyNormalAnalysis": "일반 분석", + "studyHideNextMoves": "다음 수 숨기기", + "studyInteractiveLesson": "상호 대화형 레슨", + "studyChapterX": "챕터 {param}", + "studyEmpty": "비어있음", + "studyStartFromInitialPosition": "초기 포지션에서 시작", + "studyEditor": "편집기", + "studyStartFromCustomPosition": "커스텀 포지션에서 시작", + "studyLoadAGameByUrl": "URL로 게임 가져오기", + "studyLoadAPositionFromFen": "FEN으로 포지션 가져오기", + "studyLoadAGameFromPgn": "PGN으로 게임 가져오기", + "studyAutomatic": "자동", + "studyUrlOfTheGame": "한 줄에 하나씩, 게임의 URL", + "studyLoadAGameFromXOrY": "{param1} 또는 {param2}에서 게임 로드", + "studyCreateChapter": "챕터 만들기", + "studyCreateStudy": "연구 만들기", + "studyEditStudy": "연구 편집하기", + "studyVisibility": "공개 설정", + "studyPublic": "공개", + "studyUnlisted": "비공개", + "studyInviteOnly": "초대만", + "studyAllowCloning": "복제 허용", + "studyNobody": "아무도", + "studyOnlyMe": "나만", + "studyContributors": "기여자만", + "studyMembers": "멤버만", + "studyEveryone": "모두", + "studyEnableSync": "동기화 사용", + "studyYesKeepEveryoneOnTheSamePosition": "예: 모두가 같은 위치를 봅니다", + "studyNoLetPeopleBrowseFreely": "아니요: 사람들이 자유롭게 이동할 수 있습니다", + "studyPinnedStudyComment": "고정된 댓글", + "studyStart": "시작", + "studySave": "저장", + "studyClearChat": "채팅 기록 지우기", + "studyDeleteTheStudyChatHistory": "연구 채팅 기록을 삭제할까요? 되돌릴 수 없습니다!", + "studyDeleteStudy": "연구 삭제", + "studyConfirmDeleteStudy": "모든 연구를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 연구의 이름을 입력하세요: {param}", + "studyWhereDoYouWantToStudyThat": "어디에서 연구를 시작하시겠습니까?", + "studyGoodMove": "좋은 수", + "studyMistake": "실수", + "studyBrilliantMove": "매우 좋은 수", + "studyBlunder": "블런더", + "studyInterestingMove": "흥미로운 수", + "studyDubiousMove": "애매한 수", + "studyOnlyMove": "유일한 수", + "studyZugzwang": "추크추방", + "studyEqualPosition": "동등한 포지션", + "studyUnclearPosition": "불확실한 포지션", + "studyWhiteIsSlightlyBetter": "백이 미세하게 좋음", + "studyBlackIsSlightlyBetter": "흑이 미세하게 좋음", + "studyWhiteIsBetter": "백이 유리함", + "studyBlackIsBetter": "흑이 유리함", + "studyWhiteIsWinning": "백이 이기고 있음", + "studyBlackIsWinning": "흑이 이기고 있음", + "studyNovelty": "새로운 수", + "studyDevelopment": "발전", + "studyInitiative": "주도권", + "studyAttack": "공격", + "studyCounterplay": "반격", + "studyTimeTrouble": "시간이 부족함", + "studyWithCompensation": "보상이 있음", + "studyWithTheIdea": "아이디어", + "studyNextChapter": "다음 챕터", + "studyPrevChapter": "이전 챕터", + "studyStudyActions": "연구 작업", + "studyTopics": "주제", + "studyMyTopics": "내 주제", + "studyPopularTopics": "인기 주제", + "studyManageTopics": "주제 관리", + "studyBack": "뒤로", + "studyPlayAgain": "다시 플레이", + "studyWhatWouldYouPlay": "이 포지션에서 무엇을 하시겠습니까?", + "studyYouCompletedThisLesson": "축하합니다! 이 레슨을 완료했습니다.", + "studyPerPage": "페이지 당 {param}개", + "studyNbChapters": "{count, plural, other{{count} 챕터}}", + "studyNbGames": "{count, plural, other{{count} 게임}}", + "studyNbMembers": "{count, plural, other{멤버 {count}명}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{PGN을 여기에 붙여넣으세요. 최대 {count} 게임까지 가능합니다.}}", + "timeagoJustNow": "방금", + "timeagoRightNow": "지금", + "timeagoCompleted": "종료됨", + "timeagoInNbSeconds": "{count, plural, other{{count}초 후}}", + "timeagoInNbMinutes": "{count, plural, other{{count}분 후}}", + "timeagoInNbHours": "{count, plural, other{{count}시간 후}}", + "timeagoInNbDays": "{count, plural, other{{count}일 후}}", + "timeagoInNbWeeks": "{count, plural, other{{count}주 후}}", + "timeagoInNbMonths": "{count, plural, other{{count}개월 후}}", + "timeagoInNbYears": "{count, plural, other{{count}년 후}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count}분 전}}", + "timeagoNbHoursAgo": "{count, plural, other{{count}시간 전}}", + "timeagoNbDaysAgo": "{count, plural, other{{count}일 전}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count}주 전}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count}개월 전}}", + "timeagoNbYearsAgo": "{count, plural, other{{count}년 전}}", + "timeagoNbMinutesRemaining": "{count, plural, other{{count}분 남음}}", + "timeagoNbHoursRemaining": "{count, plural, other{{count}시간 남음}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lb.arb b/lib/l10n/lila_lb.arb index b2cda00e3d..68e84b1b7f 100644 --- a/lib/l10n/lila_lb.arb +++ b/lib/l10n/lila_lb.arb @@ -1,29 +1,30 @@ { - "mobilePuzzlesTab": "Aufgaben", - "mobileMustBeLoggedIn": "Du muss ageloggt si fir dës Säit ze gesinn.", - "mobileSystemColors": "Systemsfaarwen", + "mobileAllGames": "All Partien", + "mobileAreYouSure": "Bass de sécher?", + "mobileBlindfoldMode": "Blann", "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Moien, {param}", + "mobileGreetingWithoutName": "Moien", + "mobileHideVariation": "Variante verstoppen", + "mobileMustBeLoggedIn": "Du muss ageloggt si fir dës Säit ze gesinn.", + "mobileNoSearchResults": "Keng Resultater", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spiller mat „{param}“", + "mobilePrefMagnifyDraggedPiece": "Gezunne Figur vergréisseren", + "mobilePuzzleStormSubtitle": "Léis sou vill Aufgabe wéi méiglech an 3 Minutten.", + "mobilePuzzleThemesSubtitle": "Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.", + "mobilePuzzlesTab": "Aufgaben", "mobileSettingsHapticFeedback": "Haptesche Feedback", "mobileSettingsImmersiveMode": "Immersive Modus", - "mobileAllGames": "All Partien", - "mobilePlayersMatchingSearchTerm": "Spiller mat „{param}“", - "mobileNoSearchResults": "Keng Resultater", - "mobileAreYouSure": "Bass de sécher?", - "mobileSharePuzzle": "Dës Aufgab deelen", - "mobileShareGameURL": "URL vun der Partie deelen", "mobileShareGamePGN": "PGN deelen", + "mobileShareGameURL": "URL vun der Partie deelen", "mobileSharePositionAsFEN": "Stellung als FEN deelen", - "mobileShowVariations": "Variante weisen", - "mobileHideVariation": "Variante verstoppen", + "mobileSharePuzzle": "Dës Aufgab deelen", "mobileShowComments": "Kommentarer weisen", - "mobileBlindfoldMode": "Blann", - "mobileSomethingWentWrong": "Et ass eppes schifgaang.", "mobileShowResult": "Resultat weisen", - "mobilePuzzleThemesSubtitle": "Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.", - "mobilePuzzleStormSubtitle": "Léis sou vill Aufgabe wéi méiglech an 3 Minutten.", - "mobileGreeting": "Moien, {param}", - "mobileGreetingWithoutName": "Moien", + "mobileShowVariations": "Variante weisen", + "mobileSomethingWentWrong": "Et ass eppes schifgaang.", + "mobileSystemColors": "Systemsfaarwen", "activityActivity": "Verlaf", "activityHostedALiveStream": "Huet live gestreamt", "activityRankedInSwissTournament": "Huet sech als #{param1} an {param2} placéiert", @@ -36,6 +37,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Huet {count} Zuch gespillt} other{Huet {count} Zich gespillt}}", "activityInNbCorrespondenceGames": "{count, plural, =1{an {count} Fernschachpartie} other{an {count} Fernschachpartien}}", "activityCompletedNbGames": "{count, plural, =1{Huet {count} Fernschachpartie gespillt} other{Huet {count} Fernschachpartien gespillt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Huet {count} {param2}-Fernschachpartie gespillt} other{Huet {count} {param2}-Fernschachpartië gespillt}}", "activityFollowedNbPlayers": "{count, plural, =1{Suivéiert {count} Spiller} other{Suivéiert {count} Spiller}}", "activityGainedNbFollowers": "{count, plural, =1{Huet {count} neien Unhänger} other{Huet {count} nei Unhänger}}", "activityHostedNbSimuls": "{count, plural, =1{Huet {count} Simultanvirstellung presentéiert} other{Huet {count} Simultanvirstellunge presentéiert}}", @@ -47,6 +49,56 @@ "activityJoinedNbTeams": "{count, plural, =1{Ass {count} Ekipp bäigetrueden} other{Ass {count} Ekippe bäigetrueden}}", "broadcastBroadcasts": "Iwwerdroungen", "broadcastLiveBroadcasts": "Live Turnéier Iwwerdroungen", + "broadcastNewBroadcast": "Nei Live Iwwerdroung", + "broadcastAddRound": "Ronn hinzufügen", + "broadcastOngoing": "Am Gaang", + "broadcastUpcoming": "Demnächst", + "broadcastCompleted": "Eriwwer", + "broadcastRoundName": "Ronnennumm", + "broadcastRoundNumber": "Ronnennummer", + "broadcastTournamentName": "Turnéiernumm", + "broadcastTournamentDescription": "Kuerz Turnéierbeschreiwung", + "broadcastFullDescription": "Komplett Turnéierbeschreiwung", + "broadcastFullDescriptionHelp": "Optional laang Beschreiwung vum Turnéier. {param1} ass disponibel. Längt muss manner wéi {param2} Buschtawen sinn.", + "broadcastSourceUrlHelp": "URL déi Lichess checkt fir PGN à jour ze halen. Muss ëffentlech iwwer Internet zougänglech sinn.", + "broadcastSourceGameIds": "Bis zu 64 Lichess-Partie-IDen, duerch Espacë getrennt.", + "broadcastStartDateTimeZone": "Startdatum vum Turnéier an der lokaler Zäitzon: {param}", + "broadcastStartDateHelp": "Optional, wann du wees wéini den Turnéier ufänkt", + "broadcastCurrentGameUrl": "URL vun der aktueller Partie", + "broadcastDownloadAllRounds": "All Ronnen eroflueden", + "broadcastResetRound": "Ronn zerécksetzen", + "broadcastDeleteRound": "Ronn läschen", + "broadcastDefinitivelyDeleteRound": "Dës Ronn an hir Partien endgülteg läschen.", + "broadcastDeleteAllGamesOfThisRound": "All Partien vun dëser Ronn läschen. D'Quell muss aktiv sinn fir se ze rekreéieren.", + "broadcastEditRoundStudy": "Ronnen-Etüd modifiéieren", + "broadcastDeleteTournament": "Dësen Turnéier läschen", + "broadcastDefinitivelyDeleteTournament": "De ganzen Turnéier definitiv läschen, all seng Ronnen an all seng Partien.", + "broadcastReplacePlayerTags": "Optional: Spillernimm, Wäertungen an Titelen ersetzen", + "broadcastFideFederations": "FIDE-Federatiounen", + "broadcastFidePlayers": "FIDE-Spiller", + "broadcastFidePlayerNotFound": "FIDE-Spiller net tfonnt", + "broadcastFideProfile": "FIDE-Profil", + "broadcastFederation": "Federatioun", + "broadcastAgeThisYear": "Alter dëst Joer", + "broadcastUnrated": "Ongewäert", + "broadcastRecentTournaments": "Rezent Turnéieren", + "broadcastTeams": "Ekippen", + "broadcastOverview": "Iwwersiicht", + "broadcastUploadImage": "Turnéierbild eroplueden", + "broadcastStartsAfter": "Fänkt no {param} un", + "broadcastOfficialWebsite": "Offiziell Websäit", + "broadcastOfficialStandings": "Offizielle Stand", + "broadcastIframeHelp": "Méi Optiounen op der {param}", + "broadcastWebmastersPage": "Webmaster-Säit", + "broadcastGamesThisTournament": "Partien an dësem Turnéier", + "broadcastAllTeams": "All Ekippen", + "broadcastTournamentFormat": "Turnéierformat", + "broadcastTournamentLocation": "Turnéierplaz", + "broadcastTopPlayers": "Topspiller", + "broadcastTimezone": "Zäitzon", + "broadcastFideRatingCategory": "FIDE-Wäertungskategorie", + "broadcastOptionalDetails": "Fakultativ Detailler", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Iwwerdroung} other{{count} Iwwerdroungen}}", "challengeChallengesX": "Erausfuerderungen: {param1}", "challengeChallengeToPlay": "Erausfuerderung zu enger Partie", "challengeChallengeDeclined": "Erausfuerderung ofgeleent", @@ -169,6 +221,7 @@ "preferencesNotifyWeb": "Web-Browser", "preferencesNotifyDevice": "Gerät", "preferencesBellNotificationSound": "Glacken-Notifikatiounstoun", + "preferencesBlindfold": "Blann", "puzzlePuzzles": "Aufgaben", "puzzlePuzzleThemes": "Aufgabentheemen", "puzzleRecommended": "Recommandéiert", @@ -364,8 +417,8 @@ "puzzleThemeXRayAttackDescription": "Eng Figur attackéiert oder verdeedegte Feld duerch eng géigneresch Figur.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "De Géigner huet eng begrenzten Unzuel un Zich an all Zuch verschlechtert seng Positioun.", - "puzzleThemeHealthyMix": "Gesonde Mix", - "puzzleThemeHealthyMixDescription": "E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.", + "puzzleThemeMix": "Gesonde Mix", + "puzzleThemeMixDescription": "E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.", "puzzleThemePlayerGames": "Partie vu Spiller", "puzzleThemePlayerGamesDescription": "Sich no Aufgaben, déi aus denge Partien, oder aus de Partie vun anere Spiller generéiert goufen.", "puzzleThemePuzzleDownloadInformation": "Dës Aufgaben sinn ëffentlech zougänglech an kënnen ënner {param} erofgelueden ginn.", @@ -486,7 +539,6 @@ "replayMode": "Replay-Modus", "realtimeReplay": "Echtzäit", "byCPL": "No CPL", - "openStudy": "Studie opmaachen", "enable": "Aktivéieren", "bestMoveArrow": "Beschten Zuch Feil", "showVariationArrows": "Variantefeiler weisen", @@ -496,7 +548,6 @@ "memory": "Aarbechtsspäicher", "infiniteAnalysis": "Endlos Analys", "removesTheDepthLimit": "Entfernt d'Déifenbegrenzung an hält däin Computer waarm", - "engineManager": "Engineverwaltung", "blunder": "Gaffe", "mistake": "Feeler", "inaccuracy": "Ongenauegkeet", @@ -693,7 +744,6 @@ "block": "Blockéieren", "blocked": "Geblockt", "unblock": "Spär ophiewen", - "followsYou": "Followt dir", "xStartedFollowingY": "{param1} followt elo {param2}", "more": "Méi", "memberSince": "Member säit", @@ -795,7 +845,6 @@ "cheat": "Bedruch", "troll": "Troll", "other": "Aner", - "reportDescriptionHelp": "Post den Link vun Partie(n) and erklär wat den Problem mat dësem Benotzer sengem Verhalen ass. So net just \"Hien fuddelt\", mee so eis wéi du zu dëser Konklusioun komm bass. Däin Rapport gëtt méi schnell veraarbecht wann en op Englesch ass.", "error_provideOneCheatedGameLink": "Wannechgelift gëff eis op mannst een Link zu enger Partie mat Bedruch.", "by": "vum {param}", "importedByX": "Importéiert vun {param}", @@ -1182,6 +1231,7 @@ "showMeEverything": "Alles weisen", "lichessPatronInfo": "Lichess ass eng Wohltätegkeetsorganisatioun an eng komplett kostenfrei/open source Software.\nAll Betriebskäschten, Entwécklung an Inhalter ginn ausschließlech vun Benotzerspenden finanzéiert.", "nothingToSeeHere": "Fir de Moment gëtt et hei näischt ze gesinn.", + "stats": "Statistiken", "opponentLeftCounter": "{count, plural, =1{Däin Géigner huet d'Partie verlooss. Du kanns an {count} Sekonn d'Victoire reklaméieren.} other{Däin Géigner huet d'Partie verlooss. Du kanns an {count} Sekonnen d'Victoire reklaméieren.}}", "mateInXHalfMoves": "{count, plural, =1{Matt an {count} Hallef-Zuch} other{Matt an {count} Hallef-Zich}}", "nbBlunders": "{count, plural, =1{{count} Gaffe} other{{count} Gaffen}}", @@ -1278,6 +1328,177 @@ "stormXRuns": "{count, plural, =1{1 Duerchlaf} other{{count} Duerchleef}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Huet een Duerchlaf vun {param2} gespillt} other{Huet {count} Duerchleef vun {param2} gespillt}}", "streamerLichessStreamers": "Lichess Streamer", + "studyPrivate": "Privat", + "studyMyStudies": "Meng Etüden", + "studyStudiesIContributeTo": "Etüden, un deenen ech matwierken", + "studyMyPublicStudies": "Meng öffentlech Etüden", + "studyMyPrivateStudies": "Meng privat Etüden", + "studyMyFavoriteStudies": "Meng Lieblingsetüden", + "studyWhatAreStudies": "Wat sinn Etüden?", + "studyAllStudies": "All Etüden", + "studyStudiesCreatedByX": "Etüden kreéiert vun {param}", + "studyNoneYet": "Nach keng.", + "studyHot": "Ugesot", + "studyDateAddedNewest": "Veröffentlechungsdatum (am neisten)", + "studyDateAddedOldest": "Veröffentlechungsdatum (am aalsten)", + "studyRecentlyUpdated": "Rezent aktualiséiert", + "studyMostPopular": "Am Beléiftsten", + "studyAlphabetical": "Alphabetesch", + "studyAddNewChapter": "Neit Kapitel bäifügen", + "studyAddMembers": "Memberen hinzufügen", + "studyInviteToTheStudy": "An d'Etüd alueden", + "studyPleaseOnlyInvitePeopleYouKnow": "Wannechgelift invitéier just Leit déi du kenns an déi aktiv un der Etüd matwierken wëllen.", + "studySearchByUsername": "No Benotzernumm sichen", + "studySpectator": "Zuschauer", + "studyContributor": "Matwierkenden", + "studyKick": "Rausgehéien", + "studyLeaveTheStudy": "Etüd verloossen", + "studyYouAreNowAContributor": "Du bass elo e Contributeur", + "studyYouAreNowASpectator": "Du bass elo en Zuschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gefällt mir", + "studyUnlike": "Gefällt mer net méi", + "studyNewTag": "Néien Tag", + "studyCommentThisPosition": "Kommentéier des Positioun", + "studyCommentThisMove": "Kommentéier dësen Zuch", + "studyAnnotateWithGlyphs": "Mat Symboler kommentéieren", + "studyTheChapterIsTooShortToBeAnalysed": "D'Kapitel ass ze kuerz fir analyséiert ze ginn.", + "studyOnlyContributorsCanRequestAnalysis": "Just Etüden Matwierkender kënnen eng Computer Analyse ufroen.", + "studyGetAFullComputerAnalysis": "Vollstänneg serversäiteg Computeranalyse vun der Haaptvariant erhalen.", + "studyMakeSureTheChapterIsComplete": "Stell sécher dass d'Kapitel vollstänneg ass. Du kanns eng Analyse just eemol ufroen.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC Memberen gesinn déi selwecht Positioun", + "studyShareChanges": "Deel Ännerungen mat den Zuschauer an späicher se um Server", + "studyPlaying": "Lafend Partie", + "studyFirst": "Éischt Säit", + "studyPrevious": "Zeréck", + "studyNext": "Weider", + "studyLast": "Lescht Säit", "studyShareAndExport": "Deelen & exportéieren", - "studyStart": "Lass" + "studyCloneStudy": "Klonen", + "studyStudyPgn": "Etüden PGN", + "studyDownloadAllGames": "All Partien eroflueden", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopéieren", + "studyDownloadGame": "Partie eroflueden", + "studyStudyUrl": "Etüden URL", + "studyCurrentChapterUrl": "Aktuellt Kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Zum Anbetten an een Forum oder Blog afügen", + "studyStartAtInitialPosition": "Mat Startpositioun ufänken", + "studyStartAtX": "Bei {param} ufänken", + "studyEmbedInYourWebsite": "An Websäit anbetten", + "studyReadMoreAboutEmbedding": "Méi iwwer Anbetten liesen", + "studyOnlyPublicStudiesCanBeEmbedded": "Just ëffentlech Etüden kënnen angebett ginn!", + "studyOpen": "Opmaachen", + "studyXBroughtToYouByY": "{param1}, presentéiert vum {param2}", + "studyStudyNotFound": "Etüd net fonnt", + "studyEditChapter": "Kapitel editéieren", + "studyNewChapter": "Neit Kapitel", + "studyImportFromChapterX": "Importéieren aus {param}", + "studyOrientation": "Orientatioun", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Ugepinnten Kapitelkommentar", + "studySaveChapter": "Kapitel späicheren", + "studyClearAnnotations": "Annotatiounen läschen", + "studyClearVariations": "Variante läschen", + "studyDeleteChapter": "Kapitel läschen", + "studyDeleteThisChapter": "Kapitel läschen? Et gëtt keen zeréck!", + "studyClearAllCommentsInThisChapter": "All Kommentarer, Symboler an Zeechnungsformen an dësem Kapitel läschen?", + "studyRightUnderTheBoard": "Direkt ënnert dem Briet", + "studyNoPinnedComment": "Keng", + "studyNormalAnalysis": "Normal Analyse", + "studyHideNextMoves": "Nächst Zich verstoppen", + "studyInteractiveLesson": "Interaktiv Übung", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Eidel", + "studyStartFromInitialPosition": "Aus Startpositioun ufänken", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Aus benotzerdefinéierter Positioun ufänken", + "studyLoadAGameByUrl": "Partien mat URL lueden", + "studyLoadAPositionFromFen": "Positioun aus FEN lueden", + "studyLoadAGameFromPgn": "Partien aus PGN lueden", + "studyAutomatic": "Automatesch", + "studyUrlOfTheGame": "URL vun den Partien, eng pro Zeil", + "studyLoadAGameFromXOrY": "Partien vun {param1} oder {param2} lueden", + "studyCreateChapter": "Kapitel kréieren", + "studyCreateStudy": "Etüd kreéieren", + "studyEditStudy": "Etüd änneren", + "studyVisibility": "Visibilitéit", + "studyPublic": "Ëffentlech", + "studyUnlisted": "Ongelëscht", + "studyInviteOnly": "Just mat Invitatioun", + "studyAllowCloning": "Klonen erlaaben", + "studyNobody": "Keen", + "studyOnlyMe": "Just ech", + "studyContributors": "Matwierkendender", + "studyMembers": "Memberen", + "studyEveryone": "Jiddereen", + "studyEnableSync": "Synchronisatioun aktivéieren", + "studyYesKeepEveryoneOnTheSamePosition": "Jo: Jiddereen op der selwechter Positioun halen", + "studyNoLetPeopleBrowseFreely": "Nee: Leit individuell browsen loossen", + "studyPinnedStudyComment": "Ugepinnten Etüdenkommentar", + "studyStart": "Lass", + "studySave": "Späicheren", + "studyClearChat": "Chat läschen", + "studyDeleteTheStudyChatHistory": "Etüdenchat läschen? Et gëtt keen zeréck!", + "studyDeleteStudy": "Etüd läschen", + "studyConfirmDeleteStudy": "Komplett Etüd läschen? Et gëett keen zeréck! Tipp den Numm vun der Etüd an fir ze konfirméieren: {param}", + "studyWhereDoYouWantToStudyThat": "Wéieng Etüd wëlls du benotzen?", + "studyGoodMove": "Gudden Zuch", + "studyMistake": "Feeler", + "studyBrilliantMove": "Brillianten Zuch", + "studyBlunder": "Gaffe", + "studyInterestingMove": "Interessanten Zuch", + "studyDubiousMove": "Dubiosen Zuch", + "studyOnlyMove": "Eenzegen Zuch", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Ausgeglach Positioun", + "studyUnclearPosition": "Onkloer Positioun", + "studyWhiteIsSlightlyBetter": "Wäiss steet liicht besser", + "studyBlackIsSlightlyBetter": "Schwaarz steet liicht besser", + "studyWhiteIsBetter": "Wäiss ass besser", + "studyBlackIsBetter": "Schwaarz ass besser", + "studyWhiteIsWinning": "Wéiss steet op Gewënn", + "studyBlackIsWinning": "Schwaarz steet op Gewënn", + "studyNovelty": "Neiheet", + "studyDevelopment": "Entwécklung", + "studyInitiative": "Initiativ", + "studyAttack": "Ugrëff", + "studyCounterplay": "Géigespill", + "studyTimeTrouble": "Zäitdrock", + "studyWithCompensation": "Mat Kompensatioun", + "studyWithTheIdea": "Mat der Iddi", + "studyNextChapter": "Nächst Kapitel", + "studyPrevChapter": "Kapitel virdrun", + "studyStudyActions": "Etüden-Aktiounen", + "studyTopics": "Themen", + "studyMyTopics": "Meng Themen", + "studyPopularTopics": "Beléift Themen", + "studyManageTopics": "Themen managen", + "studyBack": "Zeréck", + "studyPlayAgain": "Nach eng Kéier spillen", + "studyWhatWouldYouPlay": "Wat géifs du an dëser Positioun spillen?", + "studyYouCompletedThisLesson": "Gudd gemaach! Du hues dës Übung ofgeschloss.", + "studyPerPage": "{param} pro Säit", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Memberen}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN Text hei asetzen, bis zu {count} Partie} other{PGN Text hei asetzen, bis zu {count} Partien}}", + "timeagoJustNow": "elo grad", + "timeagoRightNow": "elo", + "timeagoCompleted": "eriwwer", + "timeagoInNbSeconds": "{count, plural, =1{an {count} Sekonn} other{an {count} Sekonnen}}", + "timeagoInNbMinutes": "{count, plural, =1{an {count} Minutt} other{an {count} Minutten}}", + "timeagoInNbHours": "{count, plural, =1{an {count} Stonn} other{an {count} Stonnen}}", + "timeagoInNbDays": "{count, plural, =1{an {count} Dag} other{an {count} Deeg}}", + "timeagoInNbWeeks": "{count, plural, =1{an {count} Woch} other{an {count} Wochen}}", + "timeagoInNbMonths": "{count, plural, =1{an {count} Mount} other{an {count} Méint}}", + "timeagoInNbYears": "{count, plural, =1{an {count} Joer} other{an {count} Joer}}", + "timeagoNbMinutesAgo": "{count, plural, =1{virun {count} Minutt} other{virun {count} Minutten}}", + "timeagoNbHoursAgo": "{count, plural, =1{virun {count} Stonn} other{virun {count} Stonnen}}", + "timeagoNbDaysAgo": "{count, plural, =1{virun {count} Dag} other{virun {count} Deeg}}", + "timeagoNbWeeksAgo": "{count, plural, =1{virun {count} Woch} other{virun {count} Wochen}}", + "timeagoNbMonthsAgo": "{count, plural, =1{virun {count} Mount} other{virun {count} Méint}}", + "timeagoNbYearsAgo": "{count, plural, =1{virun {count} Joer} other{virun {count} Joer}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minutt iwwereg} other{{count} Minutten iwwereg}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Stonn iwwereg} other{{count} Stonnen iwwereg}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lt.arb b/lib/l10n/lila_lt.arb index 590546cb3e..34017383c1 100644 --- a/lib/l10n/lila_lt.arb +++ b/lib/l10n/lila_lt.arb @@ -21,7 +21,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Dalyvavo {count} šveicariškame turnyre} few{Dalyvavo {count} šveicariškuose turnyruose} many{Dalyvavo {count} šveicariško turnyro} other{Dalyvavo {count} šveicariškų turnyrų}}", "activityJoinedNbTeams": "{count, plural, =1{Prisijungė prie {count} komandos} few{Prisijungė prie {count} komandų} many{Prisijungė prie {count} komandų} other{Prisijungė prie {count} komandų}}", "broadcastBroadcasts": "Transliacijos", + "broadcastMyBroadcasts": "Mano transliacijos", "broadcastLiveBroadcasts": "Vykstančios turnyrų transliacijos", + "broadcastBroadcastCalendar": "Transliavimo kalendorius", + "broadcastNewBroadcast": "Nauja transliacija", + "broadcastSubscribedBroadcasts": "Prenumeruojamos transliacijos", + "broadcastAboutBroadcasts": "Apie transliacijas", + "broadcastHowToUseLichessBroadcasts": "Kaip naudotis Lichess transliacijomis.", + "broadcastTheNewRoundHelp": "Naujajame ture bus tie patys nariai ir bendradarbiai, kaip ir ankstesniame.", + "broadcastAddRound": "Pridėti raundą", + "broadcastOngoing": "Vykstančios", + "broadcastUpcoming": "Artėjančios", + "broadcastCompleted": "Pasibaigę", + "broadcastCompletedHelp": "Lichess aptiko turo užbaigimą, bet galimai klaidingai. Naudokite tai, norėdami nustatyti rankiniu būdu.", + "broadcastRoundName": "Raundo pavadinimas", + "broadcastRoundNumber": "Raundo numeris", + "broadcastTournamentName": "Turnyro pavadinimas", + "broadcastTournamentDescription": "Trumpas turnyro aprašymas", + "broadcastFullDescription": "Pilnas renginio aprašymas", + "broadcastFullDescriptionHelp": "Neprivalomas pilnas transliacijos aprašymas. Galima naudoti {param1}. Ilgis negali viršyti {param2} simbolių.", + "broadcastSourceSingleUrl": "PGN šaltinio URL", + "broadcastSourceUrlHelp": "URL, į kurį „Lichess“ kreipsis gauti PGN atnaujinimus. Privalo būti viešai pasiekiamas internete.", + "broadcastSourceGameIds": "Iki 64 Lichess žaidimo ID, atskirtų tarpais.", + "broadcastStartDateTimeZone": "Turnyro pradžia vietos laiku: {param}", + "broadcastStartDateHelp": "Neprivaloma; tik jeigu žinote, kada prasideda renginys", + "broadcastCurrentGameUrl": "Dabartinio žaidimo adresas", + "broadcastDownloadAllRounds": "Atsisiųsti visus raundus", + "broadcastResetRound": "Atstatyti raundą", + "broadcastDeleteRound": "Ištrinti raundą", + "broadcastDefinitivelyDeleteRound": "Užtikrintai ištrinti raundą ir jo partijas.", + "broadcastDeleteAllGamesOfThisRound": "Ištrinti visas partijas šiame raunde. Norint jas perkurti reikės aktyvaus šaltinio.", + "broadcastEditRoundStudy": "Keisti raundo studiją", + "broadcastDeleteTournament": "Ištrinti šį turnyrą", + "broadcastDefinitivelyDeleteTournament": "Užtikrintai ištrinti visą turnyrą, visus raundus ir visas jų partijas.", + "broadcastShowScores": "Rodyti žaidėjų balus pagal partijų rezultatus", + "broadcastReplacePlayerTags": "Pasirenkama: pakeiskite žaidėjų vardus, reitingus ir titulus", + "broadcastFideFederations": "FIDE federacijos", + "broadcastTop10Rating": "10 aukščiausių reitingų", + "broadcastFidePlayers": "FIDE žaidėjai", + "broadcastFidePlayerNotFound": "FIDE žaidėjas nerastas", + "broadcastFideProfile": "FIDE profilis", + "broadcastFederation": "Federacija", + "broadcastAgeThisYear": "Amžius šiemet", + "broadcastUnrated": "Nereitinguota(s)", + "broadcastRecentTournaments": "Neseniai sukurti turnyrai", + "broadcastOpenLichess": "Atverti Lichess-e", + "broadcastTeams": "Komandos", + "broadcastBoards": "Lentos", + "broadcastOverview": "Apžvalga", + "broadcastSubscribeTitle": "Užsakykite pranešimą apie kiekvieno turo pradžią. Paskyros nustatymuose galite perjungti transliacijų skambėjimo signalą arba tiesioginius pranešimus.", + "broadcastUploadImage": "Įkelkite turnyro paveikslėlį", + "broadcastNoBoardsYet": "Dar nėra lentų. Jos bus rodomos, kai bus įkeltos partijos.", + "broadcastBoardsCanBeLoaded": "Lentas galima įkelti iš šaltinio arba per {param}", + "broadcastStartsAfter": "Pradedama po {param}", + "broadcastStartVerySoon": "Transliacija prasidės visai netrukus.", + "broadcastNotYetStarted": "Transliacija dar neprasidėjo.", + "broadcastOfficialWebsite": "Oficialus tinklapis", + "broadcastStandings": "Rezultatai", + "broadcastOfficialStandings": "Oficialūs rezultatai", + "broadcastIframeHelp": "Daugiau parinkčių {param}", + "broadcastWebmastersPage": "žiniatinklio valdytojų puslapis", + "broadcastPgnSourceHelp": "Viešas realaus laiko PGN šaltinis šiam turui. Taip pat siūlome {param} greitesniam ir efektyvesniam sinchronizavimui.", + "broadcastEmbedThisBroadcast": "Įterpkite šią transliaciją į savo svetainę", + "broadcastEmbedThisRound": "Įterpkite {param} į savo svetainę", + "broadcastRatingDiff": "Reitingo skirtumas", + "broadcastGamesThisTournament": "Partijos šiame turnyre", + "broadcastScore": "Taškų skaičius", + "broadcastAllTeams": "Visos komandos", + "broadcastTournamentFormat": "Turnyro formatas", + "broadcastTournamentLocation": "Turnyro vieta", + "broadcastTopPlayers": "Geriausi žaidėjai", + "broadcastTimezone": "Laiko juosta", + "broadcastFideRatingCategory": "FIDE reitingo kategorija", + "broadcastOptionalDetails": "Papildoma informacija", + "broadcastPastBroadcasts": "Ankstesnės transliacijos", + "broadcastAllBroadcastsByMonth": "Rodyti visas transliacijas pagal mėnesį", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transliacija} few{{count} transliacijos} many{{count} transliacijos} other{{count} transliacijų}}", "challengeChallengesX": "Iššūkiai: {param1}", "challengeChallengeToPlay": "Iškelti iššūkį", "challengeChallengeDeclined": "Iššūkis atmestas", @@ -340,8 +415,8 @@ "puzzleThemeXRayAttackDescription": "Figūra puola ar gina laukelį kiaurai priešininko figūros.", "puzzleThemeZugzwang": "Priverstinis ėjimas", "puzzleThemeZugzwangDescription": "Priešininkas apribotas ėjimais, kuriuos gali padaryti, ir visi jo ėjimai tik pabloginą jo poziciją.", - "puzzleThemeHealthyMix": "Visko po truputį", - "puzzleThemeHealthyMixDescription": "Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.", + "puzzleThemeMix": "Visko po truputį", + "puzzleThemeMixDescription": "Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.", "puzzleThemePlayerGames": "Žaidėjų žaidimai", "puzzleThemePlayerGamesDescription": "Galvosūkiai sugeneruoti iš jūsų partijų ar iš kitų žaidėjų partijų.", "puzzleThemePuzzleDownloadInformation": "Šie galvosūkiai yra laisvai prieinami ir gali būti parsisiųsti iš {param}.", @@ -359,12 +434,12 @@ "toInviteSomeoneToPlayGiveThisUrl": "Norėdami pakviesti varžovą, pasidalinkite šiuo adresu", "gameOver": "Partija baigta", "waitingForOpponent": "Laukiama varžovo", - "orLetYourOpponentScanQrCode": "Arba leiskite priešininkui nuskanuoti šį QR kodą", + "orLetYourOpponentScanQrCode": "Arba leiskite priešininkui nuskenuoti šį QR kodą", "waiting": "Laukiama", "yourTurn": "Jūsų ėjimas", "aiNameLevelAiLevel": "{param1} lygis Nr. {param2}", "level": "Lygis", - "strength": "Stiprumas", + "strength": "Pasipriešinimo stiprumas", "toggleTheChat": "Įjungti / išjungti pokalbį", "chat": "Pokalbis", "resign": "Pasiduoti", @@ -372,14 +447,14 @@ "stalemate": "Patas", "white": "Baltieji", "black": "Juodieji", - "asWhite": "kaip baltieji", - "asBlack": "kaip juodieji", + "asWhite": "už baltuosius", + "asBlack": "už juoduosius", "randomColor": "Atsitiktinė spalva", "createAGame": "Kurti žaidimą", "whiteIsVictorious": "Baltieji laimėjo", "blackIsVictorious": "Juodieji laimėjo", - "youPlayTheWhitePieces": "Žaidžiate baltomis figūromis", - "youPlayTheBlackPieces": "Žaidžiate juodomis figūromis", + "youPlayTheWhitePieces": "Jūs žaidžiate baltosiomis figūromis", + "youPlayTheBlackPieces": "Jūs žaidžiate juodosiomis figūromis", "itsYourTurn": "Jūsų ėjimas!", "cheatDetected": "Aptiktas sukčiavimas", "kingInTheCenter": "Karalius centre", @@ -462,7 +537,6 @@ "replayMode": "Peržiūros režimas", "realtimeReplay": "Realiu laiku", "byCPL": "Pagal įvertį", - "openStudy": "Atverti studiją", "enable": "Įjungti", "bestMoveArrow": "Geriausio ėjimo rodyklė", "showVariationArrows": "Rodyti variacijų rodykles", @@ -472,7 +546,6 @@ "memory": "Atmintinė", "infiniteAnalysis": "Neribota analizė", "removesTheDepthLimit": "Panaikina gylio limitą ir neleidžia kompiuteriui atvėsti", - "engineManager": "Variklių valdymas", "blunder": "Šiurkšti klaida", "mistake": "Klaida", "inaccuracy": "Netikslumas", @@ -498,6 +571,7 @@ "latestForumPosts": "Naujausi diskusijų pranešimai", "players": "Žaidėjai", "friends": "Draugai", + "otherPlayers": "kiti žaidėjai", "discussions": "Diskusijos", "today": "Šiandien", "yesterday": "Vakar", @@ -553,6 +627,7 @@ "rank": "Rangas", "rankX": "Reitingas: {param}", "gamesPlayed": "sužaistos partijos", + "ok": "OK", "cancel": "Atšaukti", "whiteTimeOut": "Baigėsi laikas baltiesiems", "blackTimeOut": "Baigėsi laikas juodiesiems", @@ -669,7 +744,6 @@ "block": "Blokuoti", "blocked": "Užblokuotas", "unblock": "Atblokuoti", - "followsYou": "Seka jus", "xStartedFollowingY": "{param1} pradėjo sekti {param2}", "more": "Daugiau", "memberSince": "Narys nuo", @@ -755,6 +829,8 @@ "descPrivateHelp": "Tekstas, kurį gali matyti tik komandos nariai. Jei nustatytas, komandos nariams pakeičia viešą aprašymą.", "no": "Ne", "yes": "Taip", + "website": "Tinklapis", + "mobile": "Mobilus", "help": "Pagalba:", "createANewTopic": "Sukurti naują temą", "topics": "Temos", @@ -773,7 +849,9 @@ "cheat": "Sukčiaviavo", "troll": "„Troll'ino“", "other": "Kita", - "reportDescriptionHelp": "Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.", + "reportCheatBoostHelp": "Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.", + "reportUsernameHelp": "Paaiškinkite, kuo šis vartotojo vardas yra įžeidžiantis. Nesakykite tiesiog „tai įžeidžia/netinkama“, bet papasakokite, kaip priėjote prie šios išvados, ypač jei įžeidimas yra užmaskuotas, ne anglų kalba, yra slengas arba yra istorinė / kultūrinė nuoroda.", + "reportProcessedFasterInEnglish": "Jūsų pranešimas bus apdorotas greičiau, jei jis bus parašytas anglų kalba.", "error_provideOneCheatedGameLink": "Pateikite bent vieną nuorodą į partiją, kurioje buvo sukčiauta.", "by": "nuo {param}", "importedByX": "Importavo {param}", @@ -1171,7 +1249,8 @@ "showMeEverything": "Rodyti viską", "lichessPatronInfo": "Lichess yra labdara ir pilnai atviro kodo/libre projektas.\nVisos veikimo išlaidos, programavimas ir turinys yra padengti išskirtinai tik vartotojų parama.", "nothingToSeeHere": "Nieko naujo.", - "opponentLeftCounter": "{count, plural, =1{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundės.} few{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} many{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} other{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.}}", + "stats": "Statistika", + "opponentLeftCounter": "{count, plural, =1{Jūsų varžovas paliko partiją. Galite reikalauti pergalės už {count} sekundės.} few{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} many{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} other{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.}}", "mateInXHalfMoves": "{count, plural, =1{Matas už {count} pus-ėjimo} few{Matas už {count} pus-ėjimų} many{Matas už {count} pus-ėjimų} other{Matas už {count} pus-ėjimų}}", "nbBlunders": "{count, plural, =1{{count} šiurkšti klaida} few{{count} šiurkščios klaidos} many{{count} šiurkščios klaidos} other{{count} šiurkščių klaidų}}", "nbMistakes": "{count, plural, =1{{count} klaida} few{{count} klaidos} many{{count} klaidos} other{{count} klaidų}}", @@ -1267,6 +1346,178 @@ "stormXRuns": "{count, plural, =1{1 eilė} few{{count} eilės} many{{count} eilės} other{{count} eilių}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Žaista viena {param2} eilė} few{Žaistos {count} {param2} eilės} many{Žaista {count} {param2} eilės} other{Žaista {count} {param2} eilių}}", "streamerLichessStreamers": "Lichess transliuotojai", + "studyPrivate": "Privati", + "studyMyStudies": "Mano studijos", + "studyStudiesIContributeTo": "Studijos, kuriose prisidedu", + "studyMyPublicStudies": "Mano viešos studijos", + "studyMyPrivateStudies": "Mano privačios studijos", + "studyMyFavoriteStudies": "Mano mėgstamiausios studijos", + "studyWhatAreStudies": "Kas yra studijos?", + "studyAllStudies": "Visos studijos", + "studyStudiesCreatedByX": "Studijos, sukurtos {param}", + "studyNoneYet": "Dar nėra.", + "studyHot": "Populiaru dabar", + "studyDateAddedNewest": "Sukūrimo data (naujausios)", + "studyDateAddedOldest": "Sukūrimo data (seniausios)", + "studyRecentlyUpdated": "Neseniai atnaujintos", + "studyMostPopular": "Populiariausios", + "studyAlphabetical": "Abėcėlės tvarka", + "studyAddNewChapter": "Pridėti naują skyrių", + "studyAddMembers": "Pridėti narių", + "studyInviteToTheStudy": "Pakviesti į studiją", + "studyPleaseOnlyInvitePeopleYouKnow": "Kvieskite tik pažįstamus žmones, ir tik norinčius dalyvauti šioje studijoje.", + "studySearchByUsername": "Ieškoti pagal naudotojo vardą", + "studySpectator": "Žiūrovas", + "studyContributor": "Talkininkas", + "studyKick": "Išmesti", + "studyLeaveTheStudy": "Palikti studiją", + "studyYouAreNowAContributor": "Dabar esate talkininkas", + "studyYouAreNowASpectator": "Dabar esate žiūrovas", + "studyPgnTags": "PGN žymos", + "studyLike": "Mėgti", + "studyUnlike": "Nebemėgti", + "studyNewTag": "Nauja žyma", + "studyCommentThisPosition": "Komentuoti šią poziciją", + "studyCommentThisMove": "Komentuoti šį ėjimą", + "studyAnnotateWithGlyphs": "Komentuoti su glifais", + "studyTheChapterIsTooShortToBeAnalysed": "Skyrius yra per trumpas analizei.", + "studyOnlyContributorsCanRequestAnalysis": "Tik studijos talkininkai gali prašyti kompiuterio analizės.", + "studyGetAFullComputerAnalysis": "Gaukite pilną pagrindinės linijos kompiuterio analizę.", + "studyMakeSureTheChapterIsComplete": "Įsitikinkite, kad skyrius užbaigtas. Analizės galite prašyti tik kartą.", + "studyAllSyncMembersRemainOnTheSamePosition": "Visi SYNC nariai lieka toje pačioje pozicijoje", + "studyShareChanges": "Dalinkitės pakeitimais su žiūrovais ir saugokite juos serveryje", + "studyPlaying": "Žaidžiama", + "studyShowEvalBar": "Vertinimo skalė", + "studyFirst": "Pirmas", + "studyPrevious": "Ankstesnis", + "studyNext": "Kitas", + "studyLast": "Paskutinis", "studyShareAndExport": "Dalintis ir eksportuoti", - "studyStart": "Pradėti" + "studyCloneStudy": "Klonuoti", + "studyStudyPgn": "Studijos PGN", + "studyDownloadAllGames": "Atsisiųsti visus žaidimus", + "studyChapterPgn": "Skyriaus PGN", + "studyCopyChapterPgn": "Kopijuoti PGN", + "studyDownloadGame": "Atsisiųsti žaidimą", + "studyStudyUrl": "Studijos URL", + "studyCurrentChapterUrl": "Dabartinio skyriaus URL", + "studyYouCanPasteThisInTheForumToEmbed": "Galite įklijuoti šį forume norėdami įterpti", + "studyStartAtInitialPosition": "Pradėti pradinėje pozicijoje", + "studyStartAtX": "Pradėti nuo {param}", + "studyEmbedInYourWebsite": "Įterpti savo svetainėje ar tinklaraštyje", + "studyReadMoreAboutEmbedding": "Skaitykite daugiau apie įterpimą", + "studyOnlyPublicStudiesCanBeEmbedded": "Gali būti įterptos tik viešos studijos!", + "studyOpen": "Atverti", + "studyXBroughtToYouByY": "{param1} iš {param2}", + "studyStudyNotFound": "Studija nerasta", + "studyEditChapter": "Redaguoti skyrių", + "studyNewChapter": "Naujas skyrius", + "studyImportFromChapterX": "Importuoti iš {param}", + "studyOrientation": "Kryptis", + "studyAnalysisMode": "Analizės režimas", + "studyPinnedChapterComment": "Prisegtas skyriaus komentaras", + "studySaveChapter": "Išsaugoti skyrių", + "studyClearAnnotations": "Pašalinti anotacijas", + "studyClearVariations": "Išvalyti variacijas", + "studyDeleteChapter": "Ištrinti skyrių", + "studyDeleteThisChapter": "Ištrinti šį skyrių? Nėra kelio atgal!", + "studyClearAllCommentsInThisChapter": "Išvalyti visus komentarus, ženklus ir figūras šiame skyriuje?", + "studyRightUnderTheBoard": "Iš karto po lenta", + "studyNoPinnedComment": "Jokio", + "studyNormalAnalysis": "Įprasta analizė", + "studyHideNextMoves": "Slėpti kitus ėjimus", + "studyInteractiveLesson": "Interaktyvi pamoka", + "studyChapterX": "Skyrius {param}", + "studyEmpty": "Tuščia", + "studyStartFromInitialPosition": "Pradėti nuo pirminės pozicijos", + "studyEditor": "Redaktorius", + "studyStartFromCustomPosition": "Pradėti nuo tinkintos pozicijos", + "studyLoadAGameByUrl": "Pakrauti partijas iš adresų", + "studyLoadAPositionFromFen": "Pakrauti poziciją iš FEN", + "studyLoadAGameFromPgn": "Pakrauti partijas iš PGN", + "studyAutomatic": "Automatinis", + "studyUrlOfTheGame": "Partijų adresai, vienas per eilutę", + "studyLoadAGameFromXOrY": "Pakrauti partijas iš {param1} arba {param2}", + "studyCreateChapter": "Sukurti skyrių", + "studyCreateStudy": "Sukurti studiją", + "studyEditStudy": "Redaguoti studiją", + "studyVisibility": "Matomumas", + "studyPublic": "Viešas", + "studyUnlisted": "Nėra sąraše", + "studyInviteOnly": "Tik su pakvietimu", + "studyAllowCloning": "Leisti kopijuoti", + "studyNobody": "Niekam", + "studyOnlyMe": "Tik man", + "studyContributors": "Dalyviams", + "studyMembers": "Nariams", + "studyEveryone": "Visiems", + "studyEnableSync": "Įgalinti sinchronizaciją", + "studyYesKeepEveryoneOnTheSamePosition": "Taip: visiems rodyti tą pačią poziciją", + "studyNoLetPeopleBrowseFreely": "Ne: leisti žmonėms naršyti laisvai", + "studyPinnedStudyComment": "Prisegtas studijos komentaras", + "studyStart": "Pradėti", + "studySave": "Išsaugoti", + "studyClearChat": "Išvalyti pokalbį", + "studyDeleteTheStudyChatHistory": "Ištrinti studijos pokalbių istoriją? Nėra kelio atgal!", + "studyDeleteStudy": "Ištrinti studiją", + "studyConfirmDeleteStudy": "Ištrinti visą studiją? Ištrynimas negrįžtamas. Norėdami tęsti įrašykite studijos pavadinimą: {param}", + "studyWhereDoYouWantToStudyThat": "Kur norite tai studijuoti?", + "studyGoodMove": "Geras ėjimas", + "studyMistake": "Klaida", + "studyBrilliantMove": "Puikus ėjimas", + "studyBlunder": "Šiurkšti klaida", + "studyInterestingMove": "Įdomus ėjimas", + "studyDubiousMove": "Abejotinas ėjimas", + "studyOnlyMove": "Vienintelis ėjimas", + "studyZugzwang": "Cugcvangas", + "studyEqualPosition": "Lygi pozicija", + "studyUnclearPosition": "Neaiški pozicija", + "studyWhiteIsSlightlyBetter": "Šiek tiek geriau baltiesiems", + "studyBlackIsSlightlyBetter": "Šiek tiek geriau juodiesiems", + "studyWhiteIsBetter": "Geriau baltiesiems", + "studyBlackIsBetter": "Geriau juodiesiems", + "studyWhiteIsWinning": "Laimi baltieji", + "studyBlackIsWinning": "Laimi juodieji", + "studyNovelty": "Naujovė", + "studyDevelopment": "Plėtojimas", + "studyInitiative": "Iniciatyva", + "studyAttack": "Ataka", + "studyCounterplay": "Kontraėjimas", + "studyTimeTrouble": "Laiko problemos", + "studyWithCompensation": "Su kompensacija", + "studyWithTheIdea": "Su mintimi", + "studyNextChapter": "Kitas skyrius", + "studyPrevChapter": "Ankstenis skyrius", + "studyStudyActions": "Studijos veiksmai", + "studyTopics": "Temos", + "studyMyTopics": "Mano temos", + "studyPopularTopics": "Populiarios temos", + "studyManageTopics": "Valdyti temas", + "studyBack": "Atgal", + "studyPlayAgain": "Žaisti dar kartą", + "studyWhatWouldYouPlay": "Ar norėtumėte žaisti nuo šios pozicijos?", + "studyYouCompletedThisLesson": "Sveikiname! Jūs pabaigėte šią pamoką.", + "studyPerPage": "{param} puslapyje", + "studyNbChapters": "{count, plural, =1{{count} skyrius} few{{count} skyriai} many{{count} skyrių} other{{count} skyrių}}", + "studyNbGames": "{count, plural, =1{{count} partija} few{{count} partijos} many{{count} partijų} other{{count} partijų}}", + "studyNbMembers": "{count, plural, =1{{count} narys} few{{count} nariai} many{{count} narių} other{{count} narių}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} few{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų} many{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} other{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų}}", + "timeagoJustNow": "ką tik", + "timeagoRightNow": "dabar", + "timeagoCompleted": "užbaigta", + "timeagoInNbSeconds": "{count, plural, =1{po {count} sekundės} few{po {count} sekundžių} many{po {count} sekundės} other{po {count} sekundžių}}", + "timeagoInNbMinutes": "{count, plural, =1{po {count} minutės} few{po {count} minučių} many{po {count} minutės} other{po {count} minučių}}", + "timeagoInNbHours": "{count, plural, =1{po {count} valandos} few{po {count} valandų} many{po {count} valandos} other{po {count} valandų}}", + "timeagoInNbDays": "{count, plural, =1{po {count} dienos} few{po {count} dienų} many{po {count} dienos} other{po {count} dienų}}", + "timeagoInNbWeeks": "{count, plural, =1{po {count} savaitės} few{po {count} savaičių} many{po {count} savaitės} other{po {count} savaičių}}", + "timeagoInNbMonths": "{count, plural, =1{po {count} mėnesio} few{po {count} mėnesių} many{po {count} mėnesio} other{po {count} mėnesių}}", + "timeagoInNbYears": "{count, plural, =1{po {count} metų} few{po {count} metų} many{po {count} metų} other{po {count} metų}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Prieš {count} minutę} few{Prieš {count} minutes} many{Prieš {count} minutės} other{Prieš {count} minučių}}", + "timeagoNbHoursAgo": "{count, plural, =1{Prieš {count} valandą} few{Prieš {count} valandas} many{Prieš {count} valandos} other{Prieš {count} valandų}}", + "timeagoNbDaysAgo": "{count, plural, =1{Prieš {count} dieną} few{Prieš {count} dienas} many{Prieš {count} dienos} other{Prieš {count} dienų}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Prieš {count} savaitę} few{Prieš {count} savaites} many{Prieš {count} savaitės} other{Prieš {count} savaičių}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Prieš {count} mėnesį} few{Prieš {count} mėnesius} many{Prieš {count} mėnesio} other{Prieš {count} mėnesių}}", + "timeagoNbYearsAgo": "{count, plural, =1{Prieš {count} metus} few{Prieš {count} metus} many{Prieš {count} metų} other{Prieš {count} metų}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Liko {count} minutė} few{Liko {count} minutės} many{Liko {count} minučių} other{Liko {count} minučių}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Liko {count} valanda} few{Liko {count} valandos} many{Liko {count} valandų} other{Liko {count} valandų}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lv.arb b/lib/l10n/lila_lv.arb index 9b83c2b18b..1a2b200cb8 100644 --- a/lib/l10n/lila_lv.arb +++ b/lib/l10n/lila_lv.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =0{Pievienojās {count} komandām} =1{Pievienojās {count} komandai} other{Pievienojās {count} komandām}}", "broadcastBroadcasts": "Raidījumi", "broadcastLiveBroadcasts": "Reāllaika turnīru raidījumi", + "broadcastNewBroadcast": "Jauns reāllaika raidījums", + "broadcastAddRound": "Pievienot raundu", + "broadcastOngoing": "Notiekošie", + "broadcastUpcoming": "Gaidāmie", + "broadcastCompleted": "Notikušie", + "broadcastRoundName": "Raunda nosaukums", + "broadcastRoundNumber": "Raunda skaitlis", + "broadcastTournamentName": "Turnīra nosaukums", + "broadcastTournamentDescription": "Īss turnīra apraksts", + "broadcastFullDescription": "Pilns pasākuma apraksts", + "broadcastFullDescriptionHelp": "Neobligāts garš raidījuma apraksts. Pieejams {param1}. Garumam jābūt mazāk kā {param2} rakstzīmēm.", + "broadcastSourceUrlHelp": "URL, ko Lichess aptaujās, lai iegūtu PGN atjauninājumus. Tam jābūt publiski piekļūstamam no interneta.", + "broadcastStartDateHelp": "Neobligāts, ja zināt, kad pasākums sākas", + "broadcastCurrentGameUrl": "Pašreizējās spēles URL", + "broadcastDownloadAllRounds": "Lejupielādēt visus raundus", + "broadcastResetRound": "Atiestatīt šo raundu", + "broadcastDeleteRound": "Dzēst šo raundu", + "broadcastDefinitivelyDeleteRound": "Neatgriezeniski dzēst raundu un tā spēles.", + "broadcastDeleteAllGamesOfThisRound": "Izdzēst visas šī raunda spēles. To atjaunošanai būs nepieciešams aktīvs avots.", "challengeChallengesX": "Izaicinājumi: {param1}", "challengeChallengeToPlay": "Izaicināt uz spēli", "challengeChallengeDeclined": "Izaicinājums noraidīts", @@ -337,8 +356,8 @@ "puzzleThemeXRayAttackDescription": "Figūra uzbrūk vai apsargā lauciņu caur pretinieka figūru.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Pretiniekam ir ierobežoti iespējamie gājieni, un visi no tiem pasliktina pretinieka pozīciju.", - "puzzleThemeHealthyMix": "Veselīgs sajaukums", - "puzzleThemeHealthyMixDescription": "Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.", + "puzzleThemeMix": "Veselīgs sajaukums", + "puzzleThemeMixDescription": "Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.", "puzzleThemePlayerGames": "Spēlētāja spēles", "puzzleThemePlayerGamesDescription": "Meklējiet uzdevumus, kas radīti no jūsu vai cita spēlētāja spēlēm.", "puzzleThemePuzzleDownloadInformation": "Šie uzdevumi ir neaizsargājami darbi, un tos var lejupielādēt lapā {param}.", @@ -455,7 +474,6 @@ "replayMode": "Atkārtojuma režīms", "realtimeReplay": "Reāllaikā", "byCPL": "Pēc CPL", - "openStudy": "Atvērt izpēti", "enable": "Iespējot", "bestMoveArrow": "Labākā gājiena bulta", "evaluationGauge": "Novērtējuma rādītājs", @@ -464,7 +482,6 @@ "memory": "Atmiņa", "infiniteAnalysis": "Bezgalīga analīze", "removesTheDepthLimit": "Noņem dziļuma ierobežojumu un uztur tavu datoru siltu", - "engineManager": "Dzinēja pārvaldnieks", "blunder": "Rupja kļūda", "mistake": "Kļūda", "inaccuracy": "Neprecizitāte", @@ -658,7 +675,6 @@ "block": "Bloķēt", "blocked": "Bloķētie", "unblock": "Atbloķēt", - "followsYou": "Sekotāji", "xStartedFollowingY": "{param1} sāka sekot {param2}", "more": "Vairāk", "memberSince": "Dalībnieks kopš", @@ -755,7 +771,6 @@ "cheat": "Krāpšanās", "troll": "Troļļošana", "other": "Cits", - "reportDescriptionHelp": "Ielīmējiet spēles saiti un paskaidrojiet, kas nav kārtībā ar lietotāja uzvedību. Nepietiks, ja tikai norādīsiet, ka \"lietotājs krāpjas\" — lūdzu, pastāstiet, kā nonācāt pie šī secinājuma. Ja jūsu ziņojums būs rakstīts angliski, par to varēsim parūpēties ātrāk.", "error_provideOneCheatedGameLink": "Lūdzu, norādiet vismaz vienu saiti uz spēli, kurā pretinieks ir krāpies.", "by": "no {param}", "importedByX": "Importēja {param}", @@ -1223,6 +1238,173 @@ "stormXRuns": "{count, plural, =0{{count} izspēlēšanas} =1{{count} izspēlēšana} other{{count} izspēlēšanas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =0{Izspēlēja {param2} {count} reizes} =1{Izspēlēja {param2} {count} reizi} other{Izspēlēja {param2} {count} reizes}}", "streamerLichessStreamers": "Lichess straumētāji", + "studyPrivate": "Privāta", + "studyMyStudies": "Manas izpētes", + "studyStudiesIContributeTo": "Izpētes, kurās piedalos", + "studyMyPublicStudies": "Manas publiskās izpētes", + "studyMyPrivateStudies": "Manas privātās izpētes", + "studyMyFavoriteStudies": "Mana izpēšu izlase", + "studyWhatAreStudies": "Kas ir izpētes?", + "studyAllStudies": "Visas izpētes", + "studyStudiesCreatedByX": "Izpētes, ko izveidoja {param}", + "studyNoneYet": "Pagaidām nevienas.", + "studyHot": "Nesen populārās", + "studyDateAddedNewest": "Pievienošanas datums (jaunākās)", + "studyDateAddedOldest": "Pievienošanas datums (vecākās)", + "studyRecentlyUpdated": "Nesen atjaunotās", + "studyMostPopular": "Populārākās", + "studyAlphabetical": "Alfabētiskā secībā", + "studyAddNewChapter": "Pievienot nodaļu", + "studyAddMembers": "Pievienot dalībniekus", + "studyInviteToTheStudy": "Ielūgt uz izpēti", + "studyPleaseOnlyInvitePeopleYouKnow": "Lūdzu, ielūdziet tikai cilvēkus, kurus pazīstat un kuri vēlas pievienoties izpētei.", + "studySearchByUsername": "Meklēt pēc lietotājvārda", + "studySpectator": "Skatītājs", + "studyContributor": "Ieguldītājs", + "studyKick": "Izmest", + "studyLeaveTheStudy": "Pamest izpēti", + "studyYouAreNowAContributor": "Tagad esat ieguldītājs", + "studyYouAreNowASpectator": "Tagad esat skatītājs", + "studyPgnTags": "PGN birkas", + "studyLike": "Patīk", + "studyUnlike": "Noņemt atzīmi \"patīk\"", + "studyNewTag": "Jauna birka", + "studyCommentThisPosition": "Komentēt šo pozīciju", + "studyCommentThisMove": "Komentēt šo gājienu", + "studyAnnotateWithGlyphs": "Anotēt ar glifiem", + "studyTheChapterIsTooShortToBeAnalysed": "Šī nodaļa ir par īsu lai to analizētu.", + "studyOnlyContributorsCanRequestAnalysis": "Tikai izpētes ieguldītāji var pieprasīt datoranalīzi.", + "studyGetAFullComputerAnalysis": "Iegūstiet pilnu servera puses pamatvarianta datoranalīzi.", + "studyMakeSureTheChapterIsComplete": "Pārliecinieties, ka nodaļa ir pabeigta. Datoranalīzi var pieprasīt tikai vienreiz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Visi SYNC dalībnieki paliek vienā pozīcijā", + "studyShareChanges": "Koplietot izmaiņas ar skatītājiem un saglabāt tās serverī", + "studyPlaying": "Notiek", + "studyFirst": "Pirmais", + "studyPrevious": "Iepriekšējais", + "studyNext": "Nākamais", + "studyLast": "Pēdējais", "studyShareAndExport": "Koplietot & eksportēt", - "studyStart": "Sākt" + "studyCloneStudy": "Klonēt", + "studyStudyPgn": "Izpētes PGN", + "studyDownloadAllGames": "Lejupielādēt visas spēles", + "studyChapterPgn": "Nodaļas PGN", + "studyCopyChapterPgn": "Kopēt PGN", + "studyDownloadGame": "Lejupielādēt spēli", + "studyStudyUrl": "Izpētes URL", + "studyCurrentChapterUrl": "Pašreizējās nodaļas URL", + "studyYouCanPasteThisInTheForumToEmbed": "Šo varat ielīmēt forumā, lai iegultu", + "studyStartAtInitialPosition": "Sākt no sākotnējās pozīcijas", + "studyStartAtX": "Sākt ar {param}", + "studyEmbedInYourWebsite": "Iegult savā mājaslapā vai blogā", + "studyReadMoreAboutEmbedding": "Lasīt vairāk par iegulšanu", + "studyOnlyPublicStudiesCanBeEmbedded": "Iegult var tikai publiskas izpētes!", + "studyOpen": "Atvērt", + "studyXBroughtToYouByY": "{param2} piedāvā \"{param1}\"", + "studyStudyNotFound": "Izpēte nav atrasta", + "studyEditChapter": "Rediģēt nodaļu", + "studyNewChapter": "Jauna nodaļa", + "studyImportFromChapterX": "Importēt no {param}", + "studyOrientation": "Orientācija", + "studyAnalysisMode": "Analīzes režīms", + "studyPinnedChapterComment": "Piesprausts nodaļas komentārs", + "studySaveChapter": "Saglabāt nodaļu", + "studyClearAnnotations": "Notīrīt piezīmes", + "studyClearVariations": "Notīrīt variantus", + "studyDeleteChapter": "Dzēst nodaļu", + "studyDeleteThisChapter": "Vai dzēst šo nodaļu? Atpakaļceļa nav!", + "studyClearAllCommentsInThisChapter": "Notīrīt visus komentārus un figūras šajā nodaļā?", + "studyRightUnderTheBoard": "Tieši zem galdiņa", + "studyNoPinnedComment": "Neviens", + "studyNormalAnalysis": "Parasta analīze", + "studyHideNextMoves": "Slēpt turpmākos gājienus", + "studyInteractiveLesson": "Interaktīva nodarbība", + "studyChapterX": "{param}. nodaļa", + "studyEmpty": "Tukšs", + "studyStartFromInitialPosition": "Sākt no sākotnējās pozīcijas", + "studyEditor": "Redaktors", + "studyStartFromCustomPosition": "Sākt no pielāgotas pozīcijas", + "studyLoadAGameByUrl": "Ielādēt spēli, norādot URL", + "studyLoadAPositionFromFen": "Ielādēt pozīciju no FEN", + "studyLoadAGameFromPgn": "Ielādēt spēli no PGN", + "studyAutomatic": "Automātisks", + "studyUrlOfTheGame": "Spēles URL", + "studyLoadAGameFromXOrY": "Ielādēt spēli no {param1} vai {param2}", + "studyCreateChapter": "Izveidot nodaļu", + "studyCreateStudy": "Izveidot izpēti", + "studyEditStudy": "Rediģēt izpēti", + "studyVisibility": "Redzamība", + "studyPublic": "Publiska", + "studyUnlisted": "Nerindota", + "studyInviteOnly": "Tikai ar ielūgumu", + "studyAllowCloning": "Atļaut dublēšanu", + "studyNobody": "Neviens", + "studyOnlyMe": "Tikai es", + "studyContributors": "Ieguldītāji", + "studyMembers": "Dalībnieki", + "studyEveryone": "Visi", + "studyEnableSync": "Iespējot sinhronizāciju", + "studyYesKeepEveryoneOnTheSamePosition": "Jā: paturēt visus vienā pozīcijā", + "studyNoLetPeopleBrowseFreely": "Nē: ļaut katram brīvi pārlūkot", + "studyPinnedStudyComment": "Piesprausts izpētes komentārs", + "studyStart": "Sākt", + "studySave": "Saglabāt", + "studyClearChat": "Notīrīt saraksti", + "studyDeleteTheStudyChatHistory": "Vai dzēst izpētes sarakstes vēsturi? Atpakaļceļa nav!", + "studyDeleteStudy": "Dzēst izpēti", + "studyConfirmDeleteStudy": "Dzēst visu izpēti? Atpakaļceļa nav! Ievadiet izpētes nosaukumu, lai apstiprinātu: {param}", + "studyWhereDoYouWantToStudyThat": "Kur vēlaties to izpētīt?", + "studyGoodMove": "Labs gājiens", + "studyMistake": "Kļūda", + "studyBrilliantMove": "Izcils gājiens", + "studyBlunder": "Rupja kļūda", + "studyInterestingMove": "Interesants gājiens", + "studyDubiousMove": "Apšaubāms gājiens", + "studyOnlyMove": "Vienīgais gājiens", + "studyZugzwang": "Gājiena spaids", + "studyEqualPosition": "Vienlīdzīga pozīcija", + "studyUnclearPosition": "Neskaidra pozīcija", + "studyWhiteIsSlightlyBetter": "Baltajiem nedaudz labāka pozīcija", + "studyBlackIsSlightlyBetter": "Melnajiem nedaudz labāka pozīcija", + "studyWhiteIsBetter": "Baltajiem labāka pozīcija", + "studyBlackIsBetter": "Melnajiem labāka pozīcija", + "studyWhiteIsWinning": "Baltie tuvojas uzvarai", + "studyBlackIsWinning": "Melnie tuvojas uzvarai", + "studyNovelty": "Oriģināls gājiens", + "studyDevelopment": "Attīstība", + "studyInitiative": "Iniciatīva", + "studyAttack": "Uzbrukums", + "studyCounterplay": "Pretspēle", + "studyTimeTrouble": "Laika trūkuma grūtības", + "studyWithCompensation": "Ar atlīdzinājumu", + "studyWithTheIdea": "Ar domu", + "studyNextChapter": "Nākamā nodaļa", + "studyPrevChapter": "Iepriekšējā nodaļa", + "studyStudyActions": "Izpētes darbības", + "studyTopics": "Temati", + "studyMyTopics": "Mani temati", + "studyPopularTopics": "Populāri temati", + "studyManageTopics": "Pārvaldīt tematus", + "studyBack": "Atpakaļ", + "studyPlayAgain": "Spēlēt vēlreiz", + "studyWhatWouldYouPlay": "Kā jūs spēlētu šādā pozīcijā?", + "studyYouCompletedThisLesson": "Apsveicam! Pabeidzāt šo nodarbību.", + "studyNbChapters": "{count, plural, =0{{count} Nodaļas} =1{{count} Nodaļa} other{{count} Nodaļas}}", + "studyNbGames": "{count, plural, =0{{count} Spēles} =1{{count} Spēle} other{{count} Spēles}}", + "studyNbMembers": "{count, plural, =0{{count} Dalībnieki} =1{{count} Dalībnieks} other{{count} Dalībnieki}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles} =1{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēli} other{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles}}", + "timeagoJustNow": "tikko", + "timeagoRightNow": "tieši tagad", + "timeagoInNbSeconds": "{count, plural, =0{pēc {count} sekundēm} =1{pēc {count} sekundes} other{pēc {count} sekundēm}}", + "timeagoInNbMinutes": "{count, plural, =0{pēc {count} minūtēm} =1{pēc {count} minūtes} other{pēc {count} minūtēm}}", + "timeagoInNbHours": "{count, plural, =0{pēc {count} stundām} =1{pēc {count} stundas} other{pēc {count} stundām}}", + "timeagoInNbDays": "{count, plural, =0{pēc {count} dienām} =1{pēc {count} dienas} other{pēc {count} dienām}}", + "timeagoInNbWeeks": "{count, plural, =0{pēc {count} nedēļām} =1{pēc {count} nedēļas} other{pēc {count} nedēļām}}", + "timeagoInNbMonths": "{count, plural, =0{pēc {count} mēnešiem} =1{pēc {count} mēneša} other{pēc {count} mēnešiem}}", + "timeagoInNbYears": "{count, plural, =0{pēc {count} gadiem} =1{pēc {count} gada} other{pēc {count} gadiem}}", + "timeagoNbMinutesAgo": "{count, plural, =0{pirms {count} minūtēm} =1{pirms {count} minūtes} other{pirms {count} minūtēm}}", + "timeagoNbHoursAgo": "{count, plural, =0{pirms {count} stundām} =1{pirms {count} stundas} other{pirms {count} stundām}}", + "timeagoNbDaysAgo": "{count, plural, =0{pirms {count} dienām} =1{pirms {count} dienas} other{pirms {count} dienām}}", + "timeagoNbWeeksAgo": "{count, plural, =0{pirms {count} nedēļām} =1{pirms {count} nedēļas} other{pirms {count} nedēļām}}", + "timeagoNbMonthsAgo": "{count, plural, =0{pirms {count} mēnešiem} =1{pirms {count} mēneša} other{pirms {count} mēnešiem}}", + "timeagoNbYearsAgo": "{count, plural, =0{pirms {count} gadiem} =1{pirms {count} gada} other{pirms {count} gadiem}}" } \ No newline at end of file diff --git a/lib/l10n/lila_mk.arb b/lib/l10n/lila_mk.arb index 2cfffc3f16..49572434fb 100644 --- a/lib/l10n/lila_mk.arb +++ b/lib/l10n/lila_mk.arb @@ -1,4 +1,7 @@ { + "mobileFeedbackButton": "Повратна информација", + "mobileSettingsHapticFeedback": "Тактилен фидбек", + "mobileSystemColors": "Системски бои", "activityActivity": "Активност", "activityHostedALiveStream": "Емитуваше во живо", "activityRankedInSwissTournament": "Ранг #{param1} во {param2}", @@ -22,6 +25,15 @@ "activityJoinedNbTeams": "{count, plural, =1{Се придружи на {count} тим} other{Се придружи на {count} тимови}}", "broadcastBroadcasts": "Емитувања", "broadcastLiveBroadcasts": "Пренос на турнири во живо", + "broadcastNewBroadcast": "Ново емитување во живо", + "broadcastOngoing": "Во тек", + "broadcastUpcoming": "Претстојни", + "broadcastCompleted": "Завршени", + "broadcastRoundNumber": "Заокружен број", + "broadcastFullDescription": "Цел опис на настанот", + "broadcastFullDescriptionHelp": "Незадолжителен, долг опис на емитуваниот настан. {param1} е достапен. Должината мора да е пократка од {param2} знаци.", + "broadcastSourceUrlHelp": "URL кое Lichess ќе го користи за ажурирање на PGN датотеката. Мора да биде јавно достапно на интернет.", + "broadcastStartDateHelp": "Незадолжително, ако знаете кога почнува настанот", "challengeChallengeToPlay": "Предизвикај на Игра", "challengeChallengeDeclined": "Предизвикот е одбиен", "challengeChallengeAccepted": "Предизвикот е прифатен!", @@ -299,7 +311,6 @@ "replayMode": "Режим на реприза", "realtimeReplay": "Во реално време", "byCPL": "По CPL", - "openStudy": "Отвори студија", "enable": "Овозможи", "bestMoveArrow": "Стрелка за најдобар потег", "evaluationGauge": "Мерач за проценка", @@ -308,7 +319,6 @@ "memory": "Меморија", "infiniteAnalysis": "Бесконечна анализа", "removesTheDepthLimit": "Неограничена длабочина на анализа, го загрева вашиот компјутер", - "engineManager": "Менаџер на компјутерот", "blunder": "Глупа грешка", "mistake": "Грешка", "inaccuracy": "Непрецизност", @@ -502,7 +512,6 @@ "block": "Блокирај", "blocked": "Блокиран", "unblock": "Одблокирај", - "followsYou": "Те следи", "xStartedFollowingY": "{param1} почна да го следи {param2}", "more": "Повеќе", "memberSince": "Член од", @@ -599,7 +608,6 @@ "cheat": "Мамење", "troll": "Трол", "other": "Друго", - "reportDescriptionHelp": "Внесете линк од играта/игрите и објаснете каде е проблемот во однесувањето на овој корисник. Немојте само да обвините за мамење, туку објаснете како дојдовте до тој заклучок. Вашата пријава ќе биди разгледана побрзо ако е напишана на англиски јазик.", "error_provideOneCheatedGameLink": "Ве молиме доставете барем една врска до партија со мамење.", "by": "од {param}", "importedByX": "Внесено од {param}", @@ -1010,5 +1018,26 @@ "availableInNbLanguages": "{count, plural, =1{Достапно на {count} јазик!} other{Достапно на {count} јазици!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{{count} секунда за првиот потег} other{{count} секунди за првиот потег}}", "nbSeconds": "{count, plural, =1{{count} секунда} other{{count} секунди}}", - "andSaveNbPremoveLines": "{count, plural, =1{и заштеди {count} пред-потег} other{и заштеди {count} пред-потези}}" + "andSaveNbPremoveLines": "{count, plural, =1{и заштеди {count} пред-потег} other{и заштеди {count} пред-потези}}", + "studyNext": "Следно", + "studyEmbedInYourWebsite": "Вгради во твојот сајт", + "studySave": "Зачувај", + "studyGoodMove": "Добар потег", + "studyMistake": "Грешка", + "studyBlunder": "Глупа грешка", + "timeagoJustNow": "тукушто", + "timeagoRightNow": "Тукушто", + "timeagoInNbSeconds": "{count, plural, =1{За {count} секунди} other{За {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{За {count} минута} other{За {count} минути}}", + "timeagoInNbHours": "{count, plural, =1{За {count} час} other{За {count} часови}}", + "timeagoInNbDays": "{count, plural, =1{За {count} ден} other{За {count} денови}}", + "timeagoInNbWeeks": "{count, plural, =1{За {count} седмица} other{За {count} седмици}}", + "timeagoInNbMonths": "{count, plural, =1{За {count} месец} other{За {count} месеци}}", + "timeagoInNbYears": "{count, plural, =1{За {count} година} other{За {count} години}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Пред {count} минута} other{Пред {count} минути}}", + "timeagoNbHoursAgo": "{count, plural, =1{пред {count} час} other{Пред {count} часа}}", + "timeagoNbDaysAgo": "{count, plural, =1{пред {count} ден} other{Пред {count} дена}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Пред {count} седмица} other{Пред {count} седмици}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Пред {count} месец} other{Пред {count} месеци}}", + "timeagoNbYearsAgo": "{count, plural, =1{Пред {count} година} other{Пред {count} години}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nb.arb b/lib/l10n/lila_nb.arb index 0b37d7d989..275cd990f9 100644 --- a/lib/l10n/lila_nb.arb +++ b/lib/l10n/lila_nb.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle partier", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Blindsjakk", + "mobileCancelTakebackOffer": "Avbryt tilbud om å angre", + "mobileClearButton": "Tøm", + "mobileCorrespondenceClearSavedMove": "Fjern lagret trekk", + "mobileCustomGameJoinAGame": "Bli med på et parti", + "mobileFeedbackButton": "Tilbakemeldinger", + "mobileGreeting": "Hei, {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Skjul variant", "mobileHomeTab": "Hjem", - "mobilePuzzlesTab": "Nøtter", - "mobileToolsTab": "Verktøy", - "mobileWatchTab": "Se", - "mobileSettingsTab": "Valg", + "mobileLiveStreamers": "Direktestrømmere", "mobileMustBeLoggedIn": "Du må være logget inn for å vise denne siden.", - "mobileSystemColors": "Systemfarger", - "mobileFeedbackButton": "Tilbakemeldinger", + "mobileNoSearchResults": "Ingen treff", + "mobileNotFollowingAnyUser": "Du følger ingen brukere.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spillere med «{param}»", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikker når de dras", + "mobilePuzzleStormConfirmEndRun": "Vil du avslutte denne runden?", + "mobilePuzzleStormFilterNothingToShow": "Ingenting her, endre filteret", + "mobilePuzzleStormNothingToShow": "Ingenting her. Spill noen runder med Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter du klarer i løpet av 3 minutter.", + "mobilePuzzleStreakAbortWarning": "Du mister rekken og poengsummen din blir lagret.", + "mobilePuzzleThemesSubtitle": "Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.", + "mobilePuzzlesTab": "Nøtter", + "mobileRecentSearches": "Nylige søk", "mobileSettingsHapticFeedback": "Haptiske tilbakemeldinger", "mobileSettingsImmersiveMode": "Fordypelsesmodus", "mobileSettingsImmersiveModeSubtitle": "Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.", - "mobileNotFollowingAnyUser": "Du følger ingen brukere.", - "mobileAllGames": "Alle partier", - "mobileRecentSearches": "Nylige søk", - "mobileClearButton": "Tøm", - "mobilePlayersMatchingSearchTerm": "Spillere med «{param}»", - "mobileNoSearchResults": "Ingen treff", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du mister rekken og poengsummen din blir lagret.", - "mobilePuzzleStormNothingToShow": "Ingenting her. Spill noen runder med Puzzle Storm.", - "mobileSharePuzzle": "Del denne sjakknøtten", - "mobileShareGameURL": "Del URL til partiet", + "mobileSettingsTab": "Valg", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del URL til partiet", "mobileSharePositionAsFEN": "Del stillingen som FEN", - "mobileShowVariations": "Vis varianter", - "mobileHideVariation": "Skjul variant", + "mobileSharePuzzle": "Del denne sjakknøtten", "mobileShowComments": "Vis kommentarer", - "mobilePuzzleStormConfirmEndRun": "Vil du avslutte denne runden?", - "mobilePuzzleStormFilterNothingToShow": "Ingenting her, endre filteret", - "mobileCancelTakebackOffer": "Avbryt tilbud om å angre", - "mobileCancelDrawOffer": "Avbryt remistilbud", - "mobileWaitingForOpponentToJoin": "Venter på motstanderen ...", - "mobileBlindfoldMode": "Blindsjakk", - "mobileLiveStreamers": "Direktestrømmere", - "mobileCustomGameJoinAGame": "Bli med på et parti", - "mobileCorrespondenceClearSavedMove": "Fjern lagret trekk", - "mobileSomethingWentWrong": "Noe gikk galt.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.", - "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter som mulig i løpet av 3 minutter.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", + "mobileShowVariations": "Vis varianter", + "mobileSomethingWentWrong": "Noe gikk galt.", + "mobileSystemColors": "Systemfarger", + "mobileTheme": "Tema", + "mobileToolsTab": "Verktøy", + "mobileWaitingForOpponentToJoin": "Venter på motstanderen ...", + "mobileWatchTab": "Se", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Startet en direktestrøm", "activityRankedInSwissTournament": "Ble nummer {param1} i {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Har spilt {count} trekk} other{Har spilt {count} trekk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} fjernsjakkparti} other{i {count} fjernsjakkpartier}}", "activityCompletedNbGames": "{count, plural, =1{Har spilt ferdig {count} fjernsjakkparti} other{Har spilt ferdig {count} fjernsjakkpartier}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Har spilt ferdig {count} fjernsjakkparti i {param2}} other{Har spilt ferdig {count} fjernsjakkpartier i {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{Følger {count} spiller} other{Følger {count} spillere}}", "activityGainedNbFollowers": "{count, plural, =1{Har {count} ny følger} other{Har {count} nye følgere}}", "activityHostedNbSimuls": "{count, plural, =1{Har vært vertskap for {count} simultanoppvisning} other{Har vært vertskap for {count} simultanoppvisninger}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Har deltatt i {count} sveitserturnering} other{Har deltatt i {count} sveitserturneringer}}", "activityJoinedNbTeams": "{count, plural, =1{Er medlem av {count} lag} other{Er medlem av {count} lag}}", "broadcastBroadcasts": "Overføringer", + "broadcastMyBroadcasts": "Mine overføringer", "broadcastLiveBroadcasts": "Direkteoverføringer av turneringer", + "broadcastBroadcastCalendar": "Kalender for overføringer", + "broadcastNewBroadcast": "Ny direkteoverføring", + "broadcastSubscribedBroadcasts": "Overføringer som du abonnerer på", + "broadcastAboutBroadcasts": "Om overføringer", + "broadcastHowToUseLichessBroadcasts": "Hvordan bruke overføringer hos Lichess.", + "broadcastTheNewRoundHelp": "Den nye runden vil ha de samme medlemmene og bidragsyterne som den forrige.", + "broadcastAddRound": "Legg til runde", + "broadcastOngoing": "Pågående", + "broadcastUpcoming": "Kommende", + "broadcastCompleted": "Fullført", + "broadcastCompletedHelp": "Lichess oppdager fullførte runder basert på kildepartiene. Bruk denne knappen hvis det ikke finnes noen kilde.", + "broadcastRoundName": "Rundenavn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnavn", + "broadcastTournamentDescription": "Kort beskrivelse av turneringen", + "broadcastFullDescription": "Full beskrivelse av turneringen", + "broadcastFullDescriptionHelp": "Valgfri lang beskrivelse av turneringen. {param1} er tilgjengelig. Beskrivelsen må være kortere enn {param2} tegn.", + "broadcastSourceSingleUrl": "URL til PGN-kilden", + "broadcastSourceUrlHelp": "Lenke som Lichess vil hente PGN-oppdateringer fra. Den må være offentlig tilgjengelig på internett.", + "broadcastSourceGameIds": "Opptil 64 ID-er for partier hos Lichess. De må være adskilt med mellomrom.", + "broadcastStartDateTimeZone": "Startdato i turneringens lokale tidssone: {param}", + "broadcastStartDateHelp": "Valgfritt, hvis du vet når arrangementet starter", + "broadcastCurrentGameUrl": "URL for dette partiet", + "broadcastDownloadAllRounds": "Last ned alle rundene", + "broadcastResetRound": "Nullstill denne runden", + "broadcastDeleteRound": "Slett denne runden", + "broadcastDefinitivelyDeleteRound": "Slett runden og tilhørende partier ugjenkallelig.", + "broadcastDeleteAllGamesOfThisRound": "Slett alle partiene i denne runden. Kilden må være aktiv for å gjenopprette dem.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slett denne turneringen", + "broadcastDefinitivelyDeleteTournament": "Slett hele turneringen for godt, sammen med alle rundene og alle partiene.", + "broadcastShowScores": "Vis poeng for spillerne basert på resultater av partiene", + "broadcastReplacePlayerTags": "Valgfritt: erstatt spillernavn, ratinger og titler", + "broadcastFideFederations": "FIDE-forbund", + "broadcastTop10Rating": "Topp 10 rating", + "broadcastFidePlayers": "FIDE-spillere", + "broadcastFidePlayerNotFound": "Fant ikke FIDE-spiller", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Forbund", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Uratet", + "broadcastRecentTournaments": "Nylige turneringer", + "broadcastOpenLichess": "Åpne i Lichess", + "broadcastTeams": "Lag", + "broadcastBoards": "Brett", + "broadcastOverview": "Oversikt", + "broadcastSubscribeTitle": "Abonner for å bli varslet når hver runde starter. Du kan velge varselform i kontoinnstillingene dine.", + "broadcastUploadImage": "Last opp bilde for turneringen", + "broadcastNoBoardsYet": "Ingen brett. De vises når partiene er lastet opp.", + "broadcastBoardsCanBeLoaded": "Brett kan lastes med en kilde eller via {param}", + "broadcastStartsAfter": "Starter etter {param}", + "broadcastStartVerySoon": "Overføringen starter straks.", + "broadcastNotYetStarted": "Overføringen har ikke startet.", + "broadcastOfficialWebsite": "Offisiell nettside", + "broadcastStandings": "Resultatliste", + "broadcastOfficialStandings": "Offisiell tabell", + "broadcastIframeHelp": "Flere alternativer på {param}", + "broadcastWebmastersPage": "administratorens side", + "broadcastPgnSourceHelp": "En offentlig PGN-kilde i sanntid for denne runden. Vi tilbyr også en {param} for raskere og mer effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Bygg inn denne overføringen på nettstedet ditt", + "broadcastEmbedThisRound": "Bygg inn {param} på nettstedet ditt", + "broadcastRatingDiff": "Ratingdifferanse", + "broadcastGamesThisTournament": "Partier i denne turneringen", + "broadcastScore": "Poengsum", + "broadcastAllTeams": "Alle lag", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringssted", + "broadcastTopPlayers": "Toppspillere", + "broadcastTimezone": "Tidssone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valgfrie detaljer", + "broadcastPastBroadcasts": "Tidligere overføringer", + "broadcastAllBroadcastsByMonth": "Vis alle overføringer etter måned", + "broadcastNbBroadcasts": "{count, plural, =1{{count} overføring} other{{count} overføringer}}", "challengeChallengesX": "Utfordringer: {param1}", "challengeChallengeToPlay": "Utfordre til et parti", "challengeChallengeDeclined": "Utfordring avslått", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Nettleser", "preferencesNotifyDevice": "Enhet", "preferencesBellNotificationSound": "Bjellevarsel med lyd", + "preferencesBlindfold": "Blindsjakk", "puzzlePuzzles": "Sjakknøtter", "puzzlePuzzleThemes": "Temaer for sjakknøtter", "puzzleRecommended": "Anbefalt", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "En brikke angriper eller dekker et felt indirekte, gjennom en av motstanderens brikker.", "puzzleThemeZugzwang": "Trekktvang", "puzzleThemeZugzwangDescription": "Motstanderen kan bare utføre trekk som forverrer egen stilling.", - "puzzleThemeHealthyMix": "Frisk blanding", - "puzzleThemeHealthyMixDescription": "Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.", + "puzzleThemeMix": "Frisk blanding", + "puzzleThemeMixDescription": "Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.", "puzzleThemePlayerGames": "Spillerpartier", "puzzleThemePlayerGamesDescription": "Finn sjakknøtter generert fra dine eller andres partier.", "puzzleThemePuzzleDownloadInformation": "Disse sjakknøttene er offentlig eiendom og kan lastes ned fra {param}.", @@ -505,7 +583,6 @@ "replayMode": "Gjennomspilling", "realtimeReplay": "Sanntid", "byCPL": "Etter CBT", - "openStudy": "Åpne studie", "enable": "Aktiver", "bestMoveArrow": "Pil for beste trekk", "showVariationArrows": "Vis variantpiler", @@ -515,7 +592,6 @@ "memory": "Minne", "infiniteAnalysis": "Uendelig analyse", "removesTheDepthLimit": "Fjerner dybdebegrensning, og holder maskinen din varm", - "engineManager": "Innstillinger for sjakkmotorer", "blunder": "Bukk", "mistake": "Feil", "inaccuracy": "Unøyaktighet", @@ -597,6 +673,7 @@ "rank": "Rangering", "rankX": "Plassering: {param}", "gamesPlayed": "partier spilt", + "ok": "OK", "cancel": "Avbryt", "whiteTimeOut": "Tiden er ute for hvit", "blackTimeOut": "Tiden er ute for svart", @@ -713,7 +790,6 @@ "block": "Blokker", "blocked": "Blokkert", "unblock": "Fjern blokkering", - "followsYou": "Følger deg", "xStartedFollowingY": "{param1} begynte å følge {param2}", "more": "Mer", "memberSince": "Medlem siden", @@ -819,7 +895,9 @@ "cheat": "Juks", "troll": "Troll", "other": "Annet", - "reportDescriptionHelp": "Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel.", + "reportCheatBoostHelp": "Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel. Skriv en utdypende begrunnelse, ikke bare «vedkommende jukser».", + "reportUsernameHelp": "Forklar hvorfor brukernavnet er støtende. Skriv en utdypende begrunnelse, ikke bare «det er støtende/upassende». Dette gjelder særlig hvis fornærmelsen er tilslørt, ikke er på engelsk, er et slanguttrykk eller er en historisk/kulturell referanse.", + "reportProcessedFasterInEnglish": "Rapporten din blir behandlet raskere hvis den er skrevet på engelsk.", "error_provideOneCheatedGameLink": "Oppgi minst én lenke til et jukseparti.", "by": "av {param}", "importedByX": "Importert av {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Vis meg alt", "lichessPatronInfo": "Lichess er en ideell forening, basert på fri programvare med åpen kildekode.\nAlle kostnader for drift, utvikling og innhold finansieres utelukkende av brukerbidrag.", "nothingToSeeHere": "Ingenting her for nå.", + "stats": "Statistikk", "opponentLeftCounter": "{count, plural, =1{Motspilleren din har forlatt partiet. Du kan kreve seier om {count} sekund.} other{Motspilleren din har forlatt partiet. Du kan kreve seier om {count} sekunder.}}", "mateInXHalfMoves": "{count, plural, =1{Matt om {count} halvtrekk} other{Matt om {count} halvtrekk}}", "nbBlunders": "{count, plural, =1{{count} bukk} other{{count} bukker}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 runde} other{{count} runder}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Har spilt én runde med {param2}} other{Har spilt {count} runder med {param2}}}", "streamerLichessStreamers": "Lichess-strømmere", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studier", + "studyStudiesIContributeTo": "Studier jeg bidrar til", + "studyMyPublicStudies": "Mine offentlige studier", + "studyMyPrivateStudies": "Mine private studier", + "studyMyFavoriteStudies": "Mine favorittstudier", + "studyWhatAreStudies": "Hva er studier?", + "studyAllStudies": "Alle studier", + "studyStudiesCreatedByX": "Studier opprettet av {param}", + "studyNoneYet": "Ingen så langt.", + "studyHot": "Hett", + "studyDateAddedNewest": "Dato tilføyd (nyeste)", + "studyDateAddedOldest": "Dato tilføyd (eldste)", + "studyRecentlyUpdated": "Nylig oppdatert", + "studyMostPopular": "Mest populære", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Legg til kapittel", + "studyAddMembers": "Legg til medlemmer", + "studyInviteToTheStudy": "Inviter til studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter bare folk du kjenner som ønsker å delta i studien.", + "studySearchByUsername": "Søk på brukernavn", + "studySpectator": "Tilskuer", + "studyContributor": "Bidragsyter", + "studyKick": "Kast ut", + "studyLeaveTheStudy": "Forlat studien", + "studyYouAreNowAContributor": "Du er nå bidragsyter", + "studyYouAreNowASpectator": "Du er nå tilskuer", + "studyPgnTags": "PGN-merkelapper", + "studyLike": "Lik", + "studyUnlike": "Slutt å like", + "studyNewTag": "Ny merkelapp", + "studyCommentThisPosition": "Kommenter denne stillingen", + "studyCommentThisMove": "Kommenter dette trekket", + "studyAnnotateWithGlyphs": "Kommenter med symboler", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittelet er for kort for analyse.", + "studyOnlyContributorsCanRequestAnalysis": "Bare bidragsyterne til studien kan be om maskinanalyse.", + "studyGetAFullComputerAnalysis": "Få full maskinanalyse av hovedvarianten fra serveren.", + "studyMakeSureTheChapterIsComplete": "Sørg for at kapittelet er fullført. Du kan bare be om analyse én gang.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle synkroniserte medlemmer ser den samme stillingen", + "studyShareChanges": "Del endringer med tilskuere og lagre dem på serveren", + "studyPlaying": "Pågår", + "studyShowEvalBar": "Evalueringssøyler", + "studyFirst": "Første", + "studyPrevious": "Forrige", + "studyNext": "Neste", + "studyLast": "Siste", "studyShareAndExport": "Del og eksporter", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie-PGN", + "studyDownloadAllGames": "Last ned alle partiene", + "studyChapterPgn": "Kapittel-PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Last ned partiet", + "studyStudyUrl": "Studie-URL", + "studyCurrentChapterUrl": "Kapittel-URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan lime inn dette i forumet for å bygge det inn der", + "studyStartAtInitialPosition": "Start ved innledende stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Bygg inn på nettstedet ditt eller bloggen din", + "studyReadMoreAboutEmbedding": "Les mer om å bygge inn", + "studyOnlyPublicStudiesCanBeEmbedded": "Bare offentlige studier kan bygges inn!", + "studyOpen": "Åpne", + "studyXBroughtToYouByY": "{param1} presentert av {param2}", + "studyStudyNotFound": "Fant ikke studien", + "studyEditChapter": "Rediger kapittel", + "studyNewChapter": "Nytt kapittel", + "studyImportFromChapterX": "Importer fra {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Fastspikrede kapittelkommenter", + "studySaveChapter": "Lagre kapittelet", + "studyClearAnnotations": "Fjern notater", + "studyClearVariations": "Fjern varianter", + "studyDeleteChapter": "Slett kapittel", + "studyDeleteThisChapter": "Slette dette kapittelet? Du kan ikke angre!", + "studyClearAllCommentsInThisChapter": "Fjern alle kommentarer og figurer i dette kapittelet?", + "studyRightUnderTheBoard": "Rett under brettet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul neste trekk", + "studyInteractiveLesson": "Interaktiv leksjon", + "studyChapterX": "Kapittel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved innledende stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start fra innledende stilling", + "studyLoadAGameByUrl": "Last inn partier fra URL-er", + "studyLoadAPositionFromFen": "Last inn en stilling fra FEN", + "studyLoadAGameFromPgn": "Last inn partier fra PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiene, én pr. linje", + "studyLoadAGameFromXOrY": "Last inn partier fra {param1} eller {param2}", + "studyCreateChapter": "Opprett kapittel", + "studyCreateStudy": "Opprett en studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlighet", + "studyPublic": "Offentlig", + "studyUnlisted": "Ikke listet", + "studyInviteOnly": "Bare etter invitasjon", + "studyAllowCloning": "Tillat kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Bare meg", + "studyContributors": "Bidragsytere", + "studyMembers": "Medlemmer", + "studyEveryone": "Alle", + "studyEnableSync": "Aktiver synkronisering", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behold alle i samme stilling", + "studyNoLetPeopleBrowseFreely": "Nei: la folk se fritt gjennom", + "studyPinnedStudyComment": "Fastspikrede studiekommentarer", + "studyStart": "Start", + "studySave": "Lagre", + "studyClearChat": "Fjern samtalen", + "studyDeleteTheStudyChatHistory": "Slette studiens samtalehistorikk? Du kan ikke angre!", + "studyDeleteStudy": "Slett studie", + "studyConfirmDeleteStudy": "Slette hele studien? Du kan ikke angre! Bekreft ved å skrive inn navnet på studien: {param}", + "studyWhereDoYouWantToStudyThat": "Hvilken studie vil du bruke?", + "studyGoodMove": "Godt trekk", + "studyMistake": "Feil", + "studyBrilliantMove": "Strålende trekk", + "studyBlunder": "Bukk", + "studyInterestingMove": "Interessant trekk", + "studyDubiousMove": "Tvilsomt trekk", + "studyOnlyMove": "Eneste trekk", + "studyZugzwang": "Trekktvang", + "studyEqualPosition": "Lik stilling", + "studyUnclearPosition": "Uavklart stilling", + "studyWhiteIsSlightlyBetter": "Hvit står litt bedre", + "studyBlackIsSlightlyBetter": "Svart står litt bedre", + "studyWhiteIsBetter": "Hvit står bedre", + "studyBlackIsBetter": "Svart står bedre", + "studyWhiteIsWinning": "Hvit står til vinst", + "studyBlackIsWinning": "Svart står til vinst", + "studyNovelty": "Nyvinning", + "studyDevelopment": "Utvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Angrep", + "studyCounterplay": "Motspill", + "studyTimeTrouble": "Tidsnød", + "studyWithCompensation": "Med kompensasjon", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Neste kapittel", + "studyPrevChapter": "Forrige kapittel", + "studyStudyActions": "Studiehandlinger", + "studyTopics": "Emner", + "studyMyTopics": "Mine emner", + "studyPopularTopics": "Populære emner", + "studyManageTopics": "Administrer emner", + "studyBack": "Tilbake", + "studyPlayAgain": "Spill igjen", + "studyWhatWouldYouPlay": "Hva vil du spille i denne stillingen?", + "studyYouCompletedThisLesson": "Gratulerer! Du har fullført denne leksjonen.", + "studyPerPage": "{param} per side", + "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapitler}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemmer}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} partier}}", + "timeagoJustNow": "om litt", + "timeagoRightNow": "for litt siden", + "timeagoCompleted": "fullført", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minutt} other{om {count} minutter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timer}}", + "timeagoInNbDays": "{count, plural, =1{om {count} døgn} other{om {count} døgn}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} uke} other{om {count} uker}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} måned} other{om {count} måneder}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{for {count} minutt siden} other{for {count} minutter siden}}", + "timeagoNbHoursAgo": "{count, plural, =1{for {count} time siden} other{for {count} timer siden}}", + "timeagoNbDaysAgo": "{count, plural, =1{for {count} døgn siden} other{for {count} døgn siden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{for {count} uke siden} other{for {count} uker siden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{for {count} måned siden} other{for {count} måneder siden}}", + "timeagoNbYearsAgo": "{count, plural, =1{for {count} år siden} other{for {count} år siden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutt igjen} other{{count} minutter igjen}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time igjen} other{{count} timer igjen}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nl.arb b/lib/l10n/lila_nl.arb index f2e10f73dc..3bd8a36149 100644 --- a/lib/l10n/lila_nl.arb +++ b/lib/l10n/lila_nl.arb @@ -1,55 +1,61 @@ { + "mobileAllGames": "Alle partijen", + "mobileAreYouSure": "Weet je het zeker?", + "mobileBlindfoldMode": "Geblinddoekt", + "mobileCancelTakebackOffer": "Terugnameaanbod annuleren", + "mobileClearButton": "Wissen", + "mobileCorrespondenceClearSavedMove": "Opgeslagen zet wissen", + "mobileCustomGameJoinAGame": "Een partij beginnen", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Verberg varianten", "mobileHomeTab": "Startscherm", - "mobilePuzzlesTab": "Puzzels", - "mobileToolsTab": "Gereedschap", - "mobileWatchTab": "Kijken", - "mobileSettingsTab": "Instellingen", + "mobileLiveStreamers": "Live streamers", "mobileMustBeLoggedIn": "Je moet ingelogd zijn om deze pagina te bekijken.", - "mobileSystemColors": "Systeemkleuren", - "mobileFeedbackButton": "Feedback", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Haptische feedback", - "mobileSettingsImmersiveMode": "Volledig scherm-modus", + "mobileNoSearchResults": "Geen resultaten", "mobileNotFollowingAnyUser": "U volgt geen gebruiker.", - "mobileAllGames": "Alle partijen", - "mobileRecentSearches": "Recente zoekopdrachten", - "mobileClearButton": "Wissen", + "mobileOkButton": "OK", "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", - "mobileNoSearchResults": "Geen resultaten", - "mobileAreYouSure": "Weet je het zeker?", - "mobilePuzzleStreakAbortWarning": "Je verliest je huidige reeks en de score wordt opgeslagen.", + "mobilePrefMagnifyDraggedPiece": "Versleept stuk vergroot weergeven", + "mobilePuzzleStormConfirmEndRun": "Wil je deze reeks beëindigen?", + "mobilePuzzleStormFilterNothingToShow": "Niets te tonen, wijzig de filters", "mobilePuzzleStormNothingToShow": "Niets om te tonen. Speel een aantal reeksen Puzzle Storm.", - "mobileSharePuzzle": "Deze puzzel delen", - "mobileShareGameURL": "Partij URL delen", + "mobilePuzzleStormSubtitle": "Los zoveel mogelijk puzzels op in 3 minuten.", + "mobilePuzzleStreakAbortWarning": "Je verliest je huidige reeks en de score wordt opgeslagen.", + "mobilePuzzleThemesSubtitle": "Speel puzzels uit je favorieten openingen, of kies een thema.", + "mobilePuzzlesTab": "Puzzels", + "mobileRecentSearches": "Recente zoekopdrachten", + "mobileSettingsHapticFeedback": "Haptische feedback", + "mobileSettingsImmersiveMode": "Volledig scherm-modus", + "mobileSettingsImmersiveModeSubtitle": "Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.", + "mobileSettingsTab": "Instellingen", "mobileShareGamePGN": "PGN delen", + "mobileShareGameURL": "Partij URL delen", "mobileSharePositionAsFEN": "Stelling delen als FEN", - "mobileShowVariations": "Toon varianten", - "mobileHideVariation": "Verberg varianten", + "mobileSharePuzzle": "Deze puzzel delen", "mobileShowComments": "Opmerkingen weergeven", - "mobilePuzzleStormConfirmEndRun": "Wil je dit uitvoeren beëindigen?", - "mobileCancelDrawOffer": "Remiseaanbod intrekken", - "mobileWaitingForOpponentToJoin": "Wachten op een tegenstander...", - "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Een partij beginnen", - "mobileCorrespondenceClearSavedMove": "Opgeslagen zet wissen", - "mobileSomethingWentWrong": "Er is iets fout gegaan.", "mobileShowResult": "Toon resultaat", - "mobilePuzzleThemesSubtitle": "Speel puzzels uit je favorieten openingen, of kies een thema.", - "mobilePuzzleStormSubtitle": "Los zoveel mogelijk puzzels op in 3 minuten.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", + "mobileShowVariations": "Toon varianten", + "mobileSomethingWentWrong": "Er is iets fout gegaan.", + "mobileSystemColors": "Systeemkleuren", + "mobileTheme": "Thema", + "mobileToolsTab": "Gereedschap", + "mobileWaitingForOpponentToJoin": "Wachten op een tegenstander...", + "mobileWatchTab": "Kijken", "activityActivity": "Activiteit", "activityHostedALiveStream": "Heeft een live stream gehost", "activityRankedInSwissTournament": "Eindigde #{param1} in {param2}", "activitySignedUp": "Geregistreerd op lichess.org", "activitySupportedNbMonths": "{count, plural, =1{Steunde lichess.org voor {count} maand als {param2}} other{Steunde lichess.org voor {count} maanden als {param2}}}", - "activityPracticedNbPositions": "{count, plural, =1{Beoefende {count} positie van {param2}} other{Beoefende {count} posities van {param2}}}", + "activityPracticedNbPositions": "{count, plural, =1{Oefende {count} positie van {param2}} other{Oefende {count} posities van {param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{{count} tactische puzzel opgelost} other{{count} tactische puzzels opgelost}}", "activityPlayedNbGames": "{count, plural, =1{Speelde {count} {param2} partij} other{Speelde {count} {param2} partijen}}", "activityPostedNbMessages": "{count, plural, =1{Plaatste {count} bericht in {param2}} other{Plaatste {count} berichten in {param2}}}", "activityPlayedNbMoves": "{count, plural, =1{Speelde {count} zet} other{Speelde {count} zet}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} correspondentiepartij} other{in {count} correspondentiepartijen}}", "activityCompletedNbGames": "{count, plural, =1{Voltooide {count} correspondentiepartijen} other{Voltooide {count} correspondentiepartijen}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Voltooide {count} {param2} correspondentiepartijen} other{Voltooide {count} {param2} correspondentiepartijen}}", "activityFollowedNbPlayers": "{count, plural, =1{Begon {count} speler te volgen} other{Begon {count} spelers te volgen}}", "activityGainedNbFollowers": "{count, plural, =1{{count} nieuwe volger verworven} other{{count} nieuwe volgers verworven}}", "activityHostedNbSimuls": "{count, plural, =1{Gaf {count} simultaan} other{Gaf {count} simultanen}}", @@ -60,7 +66,81 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Nam deel aan {count} Zwitsers toernooi} other{Nam deel aan {count} Zwitserse toernooien}}", "activityJoinedNbTeams": "{count, plural, =1{Sloot zich aan bij {count} team} other{Sloot zich aan bij {count} teams}}", "broadcastBroadcasts": "Uitzendingen", + "broadcastMyBroadcasts": "Mijn uitzendingen", "broadcastLiveBroadcasts": "Live toernooi uitzendingen", + "broadcastBroadcastCalendar": "Uitzendkalender", + "broadcastNewBroadcast": "Nieuwe live uitzending", + "broadcastAboutBroadcasts": "Over uitzending", + "broadcastHowToUseLichessBroadcasts": "Hoe Lichess Uitzendingen te gebruiken.", + "broadcastTheNewRoundHelp": "De nieuwe ronde zal dezelfde leden en bijdragers hebben als de vorige.", + "broadcastAddRound": "Ronde toevoegen", + "broadcastOngoing": "Lopend", + "broadcastUpcoming": "Aankomend", + "broadcastCompleted": "Voltooid", + "broadcastCompletedHelp": "Lichess detecteert voltooiing van de ronde op basis van de bronpartijen. Gebruik deze schakelaar als er geen bron is.", + "broadcastRoundName": "Naam ronde", + "broadcastRoundNumber": "Ronde", + "broadcastTournamentName": "Naam toernooi", + "broadcastTournamentDescription": "Korte toernooibeschrijving", + "broadcastFullDescription": "Volledige beschrijving evenement", + "broadcastFullDescriptionHelp": "Optionele lange beschrijving van de uitzending. {param1} is beschikbaar. Totale lengte moet minder zijn dan {param2} tekens.", + "broadcastSourceSingleUrl": "URL van PGN-bron", + "broadcastSourceUrlHelp": "Link die Lichess gebruikt om PGN updates te krijgen. Deze moet openbaar toegankelijk zijn via internet.", + "broadcastSourceGameIds": "Tot 64 Lichess partij-ID''s, gescheiden door spaties.", + "broadcastStartDateTimeZone": "Startdatum in de lokale tijdzone van het tornooi: {param}", + "broadcastStartDateHelp": "Optioneel, als je weet wanneer het evenement start", + "broadcastCurrentGameUrl": "Huidige partij-link", + "broadcastDownloadAllRounds": "Alle rondes downloaden", + "broadcastResetRound": "Deze ronde opnieuw instellen", + "broadcastDeleteRound": "Deze ronde verwijderen", + "broadcastDefinitivelyDeleteRound": "Deze ronde en bijbehorende partijen definitief verwijderen.", + "broadcastDeleteAllGamesOfThisRound": "Alle partijen van deze ronde verwijderen. De bron zal actief moeten zijn om ze opnieuw te maken.", + "broadcastEditRoundStudy": "Studieronde bewerken", + "broadcastDeleteTournament": "Verwijder dit toernooi", + "broadcastDefinitivelyDeleteTournament": "Verwijder definitief het hele toernooi, inclusief alle rondes en partijen.", + "broadcastShowScores": "Toon scores van spelers op basis van partij-uitslagen", + "broadcastReplacePlayerTags": "Optioneel: vervang spelersnamen, beoordelingen en titels", + "broadcastFideFederations": "FIDE-federaties", + "broadcastTop10Rating": "Top 10-rating", + "broadcastFidePlayers": "FIDE-spelers", + "broadcastFidePlayerNotFound": "FIDE-speler niet gevonden", + "broadcastFideProfile": "FIDE-profiel", + "broadcastFederation": "Federatie", + "broadcastAgeThisYear": "Leeftijd dit jaar", + "broadcastUnrated": "Zonder rating", + "broadcastRecentTournaments": "Recente toernooien", + "broadcastOpenLichess": "Openen in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Borden", + "broadcastOverview": "Overzicht", + "broadcastSubscribeTitle": "Krijg een melding wanneer elke ronde start. Je kunt bel- of pushmeldingen voor uitzendingen in je accountvoorkeuren in-/uitschakelen.", + "broadcastUploadImage": "Toernooifoto uploaden", + "broadcastNoBoardsYet": "Nog geen borden. Deze zullen verschijnen van zodra er partijen worden geüpload.", + "broadcastBoardsCanBeLoaded": "Borden kunnen geladen worden met een bron of via de {param}", + "broadcastStartsAfter": "Start na {param}", + "broadcastStartVerySoon": "De uitzending begint binnenkort.", + "broadcastNotYetStarted": "De uitzending is nog niet begonnen.", + "broadcastOfficialWebsite": "Officiële website", + "broadcastStandings": "Klassement", + "broadcastOfficialStandings": "Officiële standen", + "broadcastIframeHelp": "Meer opties voor de {param}", + "broadcastWebmastersPage": "pagina van de webmaster", + "broadcastPgnSourceHelp": "Een publieke real-time PGN-bron voor deze ronde. We bieden ook een {param} aan voor een snellere en efficiëntere synchronisatie.", + "broadcastEmbedThisBroadcast": "Deze uitzending insluiten in je website", + "broadcastEmbedThisRound": "{param} insluiten in je website", + "broadcastRatingDiff": "Ratingverschil", + "broadcastGamesThisTournament": "Partijen in dit toernooi", + "broadcastScore": "Score", + "broadcastAllTeams": "Alle teams", + "broadcastTournamentFormat": "Toernooivorm", + "broadcastTournamentLocation": "Toernooilocatie", + "broadcastTopPlayers": "Topspelers", + "broadcastTimezone": "Tijdzone", + "broadcastFideRatingCategory": "FIDE-rating categorie", + "broadcastOptionalDetails": "Optionele info", + "broadcastPastBroadcasts": "Afgelopen uitzendingen", + "broadcastAllBroadcastsByMonth": "Alle uitzendingen per maand weergeven", + "broadcastNbBroadcasts": "{count, plural, =1{{count} uitzending} other{{count} uitzendingen}}", "challengeChallengesX": "Uitdagingen: {param1}", "challengeChallengeToPlay": "Uitdagen voor een partij", "challengeChallengeDeclined": "Uitdaging geweigerd", @@ -184,6 +264,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Apparaat", "preferencesBellNotificationSound": "Meldingsgeluid", + "preferencesBlindfold": "Geblinddoekt", "puzzlePuzzles": "Puzzels", "puzzlePuzzleThemes": "Puzzelthema's", "puzzleRecommended": "Aanbevolen", @@ -379,8 +460,8 @@ "puzzleThemeXRayAttackDescription": "Een stuk valt een veld aan of verdedigt een veld, door een vijandelijk stuk heen.", "puzzleThemeZugzwang": "Zetdwang", "puzzleThemeZugzwangDescription": "De tegenstander is beperkt in de zetten die hij kan doen, en elke zet verslechtert zijn stelling.", - "puzzleThemeHealthyMix": "Gezonde mix", - "puzzleThemeHealthyMixDescription": "Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.", + "puzzleThemeMix": "Gezonde mix", + "puzzleThemeMixDescription": "Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.", "puzzleThemePlayerGames": "Eigen partijen", "puzzleThemePlayerGamesDescription": "Zoek puzzels gegenereerd uit jouw partijen, of uit partijen van een andere speler.", "puzzleThemePuzzleDownloadInformation": "Deze puzzels zijn beschikbaar in het publieke domein en kunnen worden gedownload op {param}.", @@ -501,7 +582,6 @@ "replayMode": "Terugspeelmodus", "realtimeReplay": "Realtime", "byCPL": "Door CPL", - "openStudy": "Open Study", "enable": "Aanzetten", "bestMoveArrow": "Beste zet-pijl", "showVariationArrows": "Toon variantpijlen", @@ -511,7 +591,6 @@ "memory": "Geheugen", "infiniteAnalysis": "Oneindige analyse", "removesTheDepthLimit": "Verwijdert de dieptelimiet, en houdt je computer warm", - "engineManager": "Engine-beheer", "blunder": "Blunder", "mistake": "Fout", "inaccuracy": "Onnauwkeurigheid", @@ -593,6 +672,7 @@ "rank": "Positie", "rankX": "Positie: {param}", "gamesPlayed": "Gespeelde partijen", + "ok": "Oké", "cancel": "Annuleren", "whiteTimeOut": "Tijd om voor wit", "blackTimeOut": "Tijd om voor zwart", @@ -612,6 +692,7 @@ "abortGame": "Partij afbreken", "gameAborted": "Partij afgebroken", "standard": "Standaard", + "customPosition": "Aangepaste positie", "unlimited": "Onbeperkt", "mode": "Instelling", "casual": "Vrijblijvend", @@ -708,7 +789,6 @@ "block": "Blokkeren", "blocked": "Geblokkeerd", "unblock": "Deblokkeren", - "followsYou": "Volgt u", "xStartedFollowingY": "{param1} volgt nu {param2}", "more": "Meer", "memberSince": "Lid sinds", @@ -814,7 +894,9 @@ "cheat": "Valsspelen", "troll": "Provoceren", "other": "Anders", - "reportDescriptionHelp": "Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen 'hij speelt vals', maar vertel ons hoe u bent gekomen op deze conclusie. Uw rapportage zal sneller worden verwerkt als het in het Engels is geschreven.", + "reportCheatBoostHelp": "Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen 'hij speelt vals', maar leg ook uit hoe je tot deze conclusie komt.", + "reportUsernameHelp": "Leg uit wat er aan deze gebruikersnaam beledigend is. Zeg niet gewoon \"het is aanstootgevend/ongepast\", maar vertel ons hoe je tot deze conclusie komt, vooral als de belediging verhuld wordt, niet in het Engels is, in dialect is, of een historische of culturele verwijzing is.", + "reportProcessedFasterInEnglish": "Je melding wordt sneller verwerkt als deze in het Engels is geschreven.", "error_provideOneCheatedGameLink": "Geef ten minste één link naar een partij waarin vals gespeeld is.", "by": "door {param}", "importedByX": "Geïmporteerd door {param}", @@ -1212,6 +1294,7 @@ "showMeEverything": "Alles tonen", "lichessPatronInfo": "Lichess is een organisatie zonder winstoogmerk en is volledig open en gratis (libre).\nAlle exploitatiekosten, ontwikkeling en inhoud worden enkel gefinancierd door donaties van gebruikers.", "nothingToSeeHere": "Hier is momenteel niets te zien.", + "stats": "Statistieken", "opponentLeftCounter": "{count, plural, =1{Je tegenstander heeft het spel verlaten. Je kan de overwinning opeisen over {count} seconde.} other{Je tegenstander speelt niet verder. Je kunt de overwinning opeisen over {count} seconden.}}", "mateInXHalfMoves": "{count, plural, =1{Schaakmat in {count} halfzet} other{Schaakmat in {count} halfzetten}}", "nbBlunders": "{count, plural, =1{{count} blunder} other{{count} blunders}}", @@ -1308,6 +1391,178 @@ "stormXRuns": "{count, plural, =1{1 sessie} other{{count} sessies}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Eén sessie {param2} gespeeld} other{{count} sessies {param2} gespeeld}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Privé", + "studyMyStudies": "Mijn Studies", + "studyStudiesIContributeTo": "Studies waaraan ik bijdraag", + "studyMyPublicStudies": "Mijn openbare studies", + "studyMyPrivateStudies": "Mijn privé studies", + "studyMyFavoriteStudies": "Mijn favoriete studies", + "studyWhatAreStudies": "Wat zijn studies?", + "studyAllStudies": "Alle studies", + "studyStudiesCreatedByX": "Studies gemaakt door {param}", + "studyNoneYet": "Nog geen...", + "studyHot": "Populair", + "studyDateAddedNewest": "Datum toegevoegd (nieuwste)", + "studyDateAddedOldest": "Datum toegevoegd (oudste)", + "studyRecentlyUpdated": "Recent bijgewerkt", + "studyMostPopular": "Meest populair", + "studyAlphabetical": "Alfabetisch", + "studyAddNewChapter": "Nieuw hoofdstuk toevoegen", + "studyAddMembers": "Deelnemers toevoegen", + "studyInviteToTheStudy": "Uitnodigen voor de studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Nodig alleen deelnemers uit die jou kennen en actief mee willen doen aan deze studie.", + "studySearchByUsername": "Zoeken op gebruikersnaam", + "studySpectator": "Kijker", + "studyContributor": "Bijdrager", + "studyKick": "Verwijder", + "studyLeaveTheStudy": "Verlaat de studie", + "studyYouAreNowAContributor": "Je bent nu een bijdrager", + "studyYouAreNowASpectator": "Je bent nu een toeschouwer", + "studyPgnTags": "PGN labels", + "studyLike": "Vind ik leuk", + "studyUnlike": "Vind ik niet meer leuk", + "studyNewTag": "Nieuw label", + "studyCommentThisPosition": "Reageer op deze positie", + "studyCommentThisMove": "Reageer op deze zet", + "studyAnnotateWithGlyphs": "Maak aantekeningen met symbolen", + "studyTheChapterIsTooShortToBeAnalysed": "Dit hoofdstuk is te kort om geanalyseerd te worden.", + "studyOnlyContributorsCanRequestAnalysis": "Alleen de bijdragers kunnen een computer analyse aanvragen.", + "studyGetAFullComputerAnalysis": "Krijg een volledige computer analyse van de hoofdlijn.", + "studyMakeSureTheChapterIsComplete": "Zorg ervoor dat het hoofdstuk voltooid is. Je kunt slechts één keer een analyse aanvragen.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC leden blijven op dezelfde positie", + "studyShareChanges": "Deel veranderingen met toeschouwers en sla deze op op de server", + "studyPlaying": "Spelend", + "studyShowEvalBar": "Evaluatiebalk", + "studyFirst": "Eerste", + "studyPrevious": "Vorige", + "studyNext": "Volgende", + "studyLast": "Laatste", "studyShareAndExport": "Deel & exporteer", - "studyStart": "Start" + "studyCloneStudy": "Kopiëren", + "studyStudyPgn": "PGN bestuderen", + "studyDownloadAllGames": "Download alle partijen", + "studyChapterPgn": "Hoofdstuk PGN", + "studyCopyChapterPgn": "PGN kopiëren", + "studyDownloadGame": "Partij downloaden", + "studyStudyUrl": "Studie URL", + "studyCurrentChapterUrl": "Huidige hoofdstuk URL", + "studyYouCanPasteThisInTheForumToEmbed": "Je kunt deze link plakken wanneer je een bericht schrijft op het forum om de partij interactief weer te geven", + "studyStartAtInitialPosition": "Begin bij de startpositie", + "studyStartAtX": "Beginnen bij {param}", + "studyEmbedInYourWebsite": "Insluiten in blog of website", + "studyReadMoreAboutEmbedding": "Lees meer over insluiten", + "studyOnlyPublicStudiesCanBeEmbedded": "Alleen openbare studies kunnen worden ingevoegd!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1} aangeboden door {param2}", + "studyStudyNotFound": "Studie niet gevonden", + "studyEditChapter": "Hoofdstuk bewerken", + "studyNewChapter": "Nieuw hoofdstuk", + "studyImportFromChapterX": "Importeren van {param}", + "studyOrientation": "Oriëntatie", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Vastgezet commentaar van het hoofdstuk", + "studySaveChapter": "Hoofdstuk opslaan", + "studyClearAnnotations": "Wis aantekeningen", + "studyClearVariations": "Verwijder variaties", + "studyDeleteChapter": "Verwijder hoofdstuk", + "studyDeleteThisChapter": "Wil je dit hoofdstuk verwijderen? Je kan dit niet ongedaan maken!", + "studyClearAllCommentsInThisChapter": "Verwijder alle aantekeningen, tekens en getekende figuren in dit hoofdstuk?", + "studyRightUnderTheBoard": "Recht onder het bord", + "studyNoPinnedComment": "Geen", + "studyNormalAnalysis": "Normale analyse", + "studyHideNextMoves": "Verberg volgende zetten", + "studyInteractiveLesson": "Interactieve les", + "studyChapterX": "Hoofdstuk {param}", + "studyEmpty": "Leeg", + "studyStartFromInitialPosition": "Start bij de initiële positie", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start bij een aangepaste positie", + "studyLoadAGameByUrl": "Laad partijen via een URL", + "studyLoadAPositionFromFen": "Laad een spel via een FEN", + "studyLoadAGameFromPgn": "Laad partijen via een PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL van de partijen, één per regel", + "studyLoadAGameFromXOrY": "Laad partijen van {param1} of {param2}", + "studyCreateChapter": "Creëer hoofdstuk", + "studyCreateStudy": "Maak studie", + "studyEditStudy": "Bewerk studie", + "studyVisibility": "Zichtbaarheid", + "studyPublic": "Openbaar", + "studyUnlisted": "Niet openbaar", + "studyInviteOnly": "Alleen op uitnodiging", + "studyAllowCloning": "Klonen toestaan", + "studyNobody": "Niemand", + "studyOnlyMe": "Alleen ik", + "studyContributors": "Bijdragers", + "studyMembers": "Deelnemers", + "studyEveryone": "Iedereen", + "studyEnableSync": "Synchronisatie inschakelen", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: houd iedereen op dezelfde stelling", + "studyNoLetPeopleBrowseFreely": "Nee: laat mensen vrij bladeren", + "studyPinnedStudyComment": "Vastgezette studie reactie", + "studyStart": "Start", + "studySave": "Opslaan", + "studyClearChat": "Maak de chat leeg", + "studyDeleteTheStudyChatHistory": "Verwijder de studiechat geschiedenis? Er is geen weg terug!", + "studyDeleteStudy": "Studie verwijderen", + "studyConfirmDeleteStudy": "De hele studie verwijderen? Er is geen weg terug! Type de naam van de studie om te bevestigen dat je de studie wilt verwijderen: {param}", + "studyWhereDoYouWantToStudyThat": "Waar wil je dat bestuderen?", + "studyGoodMove": "Goede zet", + "studyMistake": "Fout", + "studyBrilliantMove": "Briljante zet", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interessante zet", + "studyDubiousMove": "Dubieuze zet", + "studyOnlyMove": "Enig mogelijke zet", + "studyZugzwang": "Zetdwang", + "studyEqualPosition": "Stelling in evenwicht", + "studyUnclearPosition": "Onduidelijke stelling", + "studyWhiteIsSlightlyBetter": "Wit staat iets beter", + "studyBlackIsSlightlyBetter": "Zwart staat iets beter", + "studyWhiteIsBetter": "Wit staat beter", + "studyBlackIsBetter": "Zwart staat beter", + "studyWhiteIsWinning": "Wit staat gewonnen", + "studyBlackIsWinning": "Zwart staat gewonnen", + "studyNovelty": "Noviteit", + "studyDevelopment": "Ontwikkeling", + "studyInitiative": "Initiatief", + "studyAttack": "Aanval", + "studyCounterplay": "Tegenspel", + "studyTimeTrouble": "Tijdnood", + "studyWithCompensation": "Met compensatie", + "studyWithTheIdea": "Met het idee", + "studyNextChapter": "Volgende hoofdstuk", + "studyPrevChapter": "Vorige hoofdstuk", + "studyStudyActions": "Studie sneltoetsen", + "studyTopics": "Onderwerpen", + "studyMyTopics": "Mijn onderwerpen", + "studyPopularTopics": "Populaire onderwerpen", + "studyManageTopics": "Onderwerpen beheren", + "studyBack": "Terug", + "studyPlayAgain": "Opnieuw spelen", + "studyWhatWouldYouPlay": "Wat zou je in deze stelling spelen?", + "studyYouCompletedThisLesson": "Gefeliciteerd! Je hebt deze les voltooid.", + "studyPerPage": "{param} per pagina", + "studyNbChapters": "{count, plural, =1{{count} hoofdstuk} other{{count} hoofdstukken}}", + "studyNbGames": "{count, plural, =1{{count} Partij} other{{count} Partijen}}", + "studyNbMembers": "{count, plural, =1{{count} Deelnemer} other{{count} Deelnemers}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak je PGN tekst hier, tot {count} spel mogelijk} other{Plak je PGN tekst hier, tot {count} spellen mogelijk}}", + "timeagoJustNow": "zojuist", + "timeagoRightNow": "op dit moment", + "timeagoCompleted": "voltooid", + "timeagoInNbSeconds": "{count, plural, =1{over {count} seconde} other{over {count} seconden}}", + "timeagoInNbMinutes": "{count, plural, =1{over {count} minuut} other{over {count} minuten}}", + "timeagoInNbHours": "{count, plural, =1{over {count} uur} other{over {count} uur}}", + "timeagoInNbDays": "{count, plural, =1{over {count} dag} other{over {count} dagen}}", + "timeagoInNbWeeks": "{count, plural, =1{over {count} week} other{over {count} weken}}", + "timeagoInNbMonths": "{count, plural, =1{over {count} maand} other{over {count} maanden}}", + "timeagoInNbYears": "{count, plural, =1{over {count} jaar} other{over {count} jaren}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuut geleden} other{{count} minuten geleden}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} uur geleden} other{{count} uur geleden}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag geleden} other{{count} dagen geleden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week geleden} other{{count} weken geleden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} maand geleden} other{{count} maanden geleden}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} jaar geleden} other{{count} jaar geleden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuut resterend} other{{count} minuten resterend}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} uur resterend} other{{count} uur resterend}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nn.arb b/lib/l10n/lila_nn.arb index b6b613a009..acbde20304 100644 --- a/lib/l10n/lila_nn.arb +++ b/lib/l10n/lila_nn.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle spel", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Blindsjakk", + "mobileCancelTakebackOffer": "Avbryt tilbud om angrerett", + "mobileClearButton": "Tøm", + "mobileCorrespondenceClearSavedMove": "Fjern lagra trekk", + "mobileCustomGameJoinAGame": "Bli med på eit parti", + "mobileFeedbackButton": "Tilbakemelding", + "mobileGreeting": "Hei {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Skjul variant", "mobileHomeTab": "Startside", - "mobilePuzzlesTab": "Oppgåver", - "mobileToolsTab": "Verktøy", - "mobileWatchTab": "Sjå", - "mobileSettingsTab": "Innstillingar", + "mobileLiveStreamers": "Direkte strøymarar", "mobileMustBeLoggedIn": "Du må vera innlogga for å sjå denne sida.", - "mobileSystemColors": "Systemfargar", - "mobileFeedbackButton": "Tilbakemelding", + "mobileNoSearchResults": "Ingen resultat", + "mobileNotFollowingAnyUser": "Du følgjer ingen brukarar.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spelarar med \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikke som vert trekt", + "mobilePuzzleStormConfirmEndRun": "Vil du avslutte dette løpet?", + "mobilePuzzleStormFilterNothingToShow": "Ikkje noko å syna, ver venleg å endre filtera", + "mobilePuzzleStormNothingToShow": "Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løys så mange oppgåver som du maktar på tre minutt.", + "mobilePuzzleStreakAbortWarning": "Du vil mista din noverande vinstrekke og poengsummen din vert lagra.", + "mobilePuzzleThemesSubtitle": "Spel oppgåver frå favorittopningane dine, eller velg eit tema.", + "mobilePuzzlesTab": "Oppgåver", + "mobileRecentSearches": "Nylege søk", "mobileSettingsHapticFeedback": "Haptisk tilbakemelding", "mobileSettingsImmersiveMode": "Immersiv modus", "mobileSettingsImmersiveModeSubtitle": "Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.", - "mobileNotFollowingAnyUser": "Du følgjer ingen brukarar.", - "mobileAllGames": "Alle spel", - "mobileRecentSearches": "Nylege søk", - "mobileClearButton": "Tøm", - "mobilePlayersMatchingSearchTerm": "Spelarar med \"{param}\"", - "mobileNoSearchResults": "Ingen resultat", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du vil mista din noverande vinstrekke og poengsummen din vert lagra.", - "mobilePuzzleStormNothingToShow": "Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.", - "mobileSharePuzzle": "Del denne oppgåva", - "mobileShareGameURL": "Del URLen til partiet", + "mobileSettingsTab": "Innstillingar", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del URLen til partiet", "mobileSharePositionAsFEN": "Del stilling som FEN", - "mobileShowVariations": "Vis variantpilar", - "mobileHideVariation": "Skjul variant", + "mobileSharePuzzle": "Del denne oppgåva", "mobileShowComments": "Vis kommentarar", - "mobilePuzzleStormConfirmEndRun": "Vil du avslutte dette løpet?", - "mobilePuzzleStormFilterNothingToShow": "Ikkje noko å syna, ver venleg å endre filtera", - "mobileCancelTakebackOffer": "Avbryt tilbud om angrerett", - "mobileCancelDrawOffer": "Avbryt remistilbud", - "mobileWaitingForOpponentToJoin": "Ventar på motspelar...", - "mobileBlindfoldMode": "Blindsjakk", - "mobileLiveStreamers": "Direkte strøymarar", - "mobileCustomGameJoinAGame": "Bli med på eit parti", - "mobileCorrespondenceClearSavedMove": "Fjern lagra trekk", - "mobileSomethingWentWrong": "Det oppsto ein feil.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spel oppgåver frå favorittopningane dine, eller velg eit tema.", - "mobilePuzzleStormSubtitle": "Løys så mange oppgåver som du maktar på tre minutt.", - "mobileGreeting": "Hei {param}", - "mobileGreetingWithoutName": "Hei", + "mobileShowVariations": "Vis variantpilar", + "mobileSomethingWentWrong": "Det oppsto ein feil.", + "mobileSystemColors": "Systemfargar", + "mobileTheme": "Tema", + "mobileToolsTab": "Verktøy", + "mobileWaitingForOpponentToJoin": "Ventar på motspelar...", + "mobileWatchTab": "Sjå", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Starta en direktestraum", "activityRankedInSwissTournament": "Vart nr. {param1} i {param2}", @@ -54,8 +55,9 @@ "activityPlayedNbMoves": "{count, plural, =1{Spelt {count} trekk} other{Spelt {count} trekk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} fjernsjakkparti} other{i {count} fjernsjakkparti}}", "activityCompletedNbGames": "{count, plural, =1{Har spela {count} fjernsjakkparti} other{Har spela {count} fjernsjakkparti}}", - "activityFollowedNbPlayers": "{count, plural, =1{Fylgjer {count} spelar} other{Fylgjer {count} spelarar}}", - "activityGainedNbFollowers": "{count, plural, =1{Har {count} nye fylgjarar} other{Har {count} nye fylgjarar}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Har spelt {count} {param2}-fjernsjakkparti} other{Har spelt {count} {param2}-fjernsjakkparti}}", + "activityFollowedNbPlayers": "{count, plural, =1{Følgjer {count} spelar} other{Følgjer {count} spelarar}}", + "activityGainedNbFollowers": "{count, plural, =1{Har {count} ny følgjar} other{Har {count} nye følgjarar}}", "activityHostedNbSimuls": "{count, plural, =1{Har vore vert for {count} simultanframsyningar} other{Har vore vert for {count} simultan-matcher}}", "activityJoinedNbSimuls": "{count, plural, =1{Har vore deltakar i {count} simultanframsyningar} other{Har vore deltakar i {count} simultanmatcher}}", "activityCreatedNbStudies": "{count, plural, =1{Har laga {count} nye studiar} other{Har laga {count} nye studiar}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Har vore med i {count} sveitserturnering} other{Har vore med i {count} sveitserturneringar}}", "activityJoinedNbTeams": "{count, plural, =1{Er medlem av {count} lag} other{Er medlem av {count} lag}}", "broadcastBroadcasts": "Overføringar", + "broadcastMyBroadcasts": "Mine sendingar", "broadcastLiveBroadcasts": "Direktesende turneringar", + "broadcastBroadcastCalendar": "Kaldender for sendingar", + "broadcastNewBroadcast": "Ny direktesending", + "broadcastSubscribedBroadcasts": "Sendingar du abonnerar på", + "broadcastAboutBroadcasts": "Om sending", + "broadcastHowToUseLichessBroadcasts": "Korleis bruke Lichess-sendingar.", + "broadcastTheNewRoundHelp": "Den nye runden vil ha same medlemar og bidragsytarar som den førre.", + "broadcastAddRound": "Legg til ein runde", + "broadcastOngoing": "Pågåande", + "broadcastUpcoming": "Kommande", + "broadcastCompleted": "Fullførde", + "broadcastCompletedHelp": "Lichess detekterer ferdigspela rundar basert på kjeldeparita. Bruk denne innstillinga om det ikkje finst ei kjelde.", + "broadcastRoundName": "Rundenamn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnamn", + "broadcastTournamentDescription": "Kortfatta skildring av turneringa", + "broadcastFullDescription": "Full omtale av arrangementet", + "broadcastFullDescriptionHelp": "Valfri lang omtale av turneringa. {param1} er tilgjengeleg. Omtalen må vera kortare enn {param2} teikn.", + "broadcastSourceSingleUrl": "PGN kjelde-URL", + "broadcastSourceUrlHelp": "Lenke som Lichess vil hente PGN-oppdateringar frå. Den må vera offentleg tilgjengeleg på internett.", + "broadcastSourceGameIds": "Opp til 64 Lichess spel-ID'ar, skilde med mellomrom.", + "broadcastStartDateTimeZone": "Startdato i turneringas lokale tidssone: {param}", + "broadcastStartDateHelp": "Valfritt, om du veit når arrangementet startar", + "broadcastCurrentGameUrl": "URL til pågåande parti", + "broadcastDownloadAllRounds": "Last ned alle rundene", + "broadcastResetRound": "Tilbakestill denne runden", + "broadcastDeleteRound": "Slett denne runden", + "broadcastDefinitivelyDeleteRound": "Slett runden og tilhøyrande parti ugjenkalleleg.", + "broadcastDeleteAllGamesOfThisRound": "Fjern alle parti frå denne runden. Kjelda må vera aktiv om dei skal kunne rettast opp att.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slett denne turneringa", + "broadcastDefinitivelyDeleteTournament": "Slett heile turneringa med alle rundene og alle partia.", + "broadcastShowScores": "Vis poengsummane til spelarar basert på spelresultatet deira", + "broadcastReplacePlayerTags": "Valfritt: bytt ut spelarnamn, rangeringar og titlar", + "broadcastFideFederations": "FIDE-forbund", + "broadcastTop10Rating": "Topp 10 rating", + "broadcastFidePlayers": "FIDE-spelarar", + "broadcastFidePlayerNotFound": "Fann ikkje FIDE-spelar", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Forbund", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Urangert", + "broadcastRecentTournaments": "Nylegaste turneringar", + "broadcastOpenLichess": "Opne i Lichess", + "broadcastTeams": "Lag", + "broadcastBoards": "Brett", + "broadcastOverview": "Oversikt", + "broadcastSubscribeTitle": "Abonner for å få melding når kvarr runde startar. I konto-innstillingane dine kan du velje kva form varslane skal sendas som.", + "broadcastUploadImage": "Last opp turneringsbilete", + "broadcastNoBoardsYet": "Førebels er det ikkje brett å syne. Desse vert først vist når spel er lasta opp.", + "broadcastBoardsCanBeLoaded": "Brett kan lastas med ei kjelde eller via {param}", + "broadcastStartsAfter": "Startar etter {param}", + "broadcastStartVerySoon": "Sending vil starte om ikkje lenge.", + "broadcastNotYetStarted": "Sendinga har førebels ikkje starta.", + "broadcastOfficialWebsite": "Offisiell nettside", + "broadcastStandings": "Resultat", + "broadcastOfficialStandings": "Offisiell tabell", + "broadcastIframeHelp": "Fleire alternativ på {param}", + "broadcastWebmastersPage": "administratoren si side", + "broadcastPgnSourceHelp": "Ei offentleg PGN-kjelde i sanntid for denne runden. Vi tilbyr og ei {param} for raskare og meir effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Bygg inn denne sendinga på nettstaden din", + "broadcastEmbedThisRound": "Bygg inn {param} på nettstaden din", + "broadcastRatingDiff": "Rangeringsdiff", + "broadcastGamesThisTournament": "Spel i denne turneringa", + "broadcastScore": "Poengskår", + "broadcastAllTeams": "Alle lag", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringsstad", + "broadcastTopPlayers": "Toppspelarar", + "broadcastTimezone": "Tidssone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valfrie detaljar", + "broadcastPastBroadcasts": "Tidlegare overføringar", + "broadcastAllBroadcastsByMonth": "Vis alle overføringar etter månad", + "broadcastNbBroadcasts": "{count, plural, =1{{count} sending} other{{count} sendingar}}", "challengeChallengesX": "Utfordringar: {param1}", "challengeChallengeToPlay": "Utfordra til eit parti", "challengeChallengeDeclined": "Utfordring avvist", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Nettlesar", "preferencesNotifyDevice": "Eining", "preferencesBellNotificationSound": "Varsellyd", + "preferencesBlindfold": "Blindsjakk", "puzzlePuzzles": "Taktikkoppgåver", "puzzlePuzzleThemes": "Oppgåvetema", "puzzleRecommended": "Anbefalt", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Ein situasjon der ei brikke går til åtak gjennom ei eller fleire andre brikker, gjerne brikker som høyrer til motspelaren.", "puzzleThemeZugzwang": "Trekktvang", "puzzleThemeZugzwangDescription": "Ei stilling der alle moglege trekk skadar stillinga.", - "puzzleThemeHealthyMix": "Blanda drops", - "puzzleThemeHealthyMixDescription": "Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.", + "puzzleThemeMix": "Blanda drops", + "puzzleThemeMixDescription": "Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.", "puzzleThemePlayerGames": "Spelar parti", "puzzleThemePlayerGamesDescription": "Finn oppgåver generert frå dine eller andre sine parti.", "puzzleThemePuzzleDownloadInformation": "Desse oppgåvene er offentleg eigedom og kan lastast ned frå {param}.", @@ -505,7 +583,6 @@ "replayMode": "Modus for å spele oppatt", "realtimeReplay": "Sanntid", "byCPL": "CPL", - "openStudy": "Opne studie", "enable": "Aktiver", "bestMoveArrow": "Pil for beste trekk", "showVariationArrows": "Vis variantpiler", @@ -515,7 +592,6 @@ "memory": "Minne", "infiniteAnalysis": "Uendeleg analyse", "removesTheDepthLimit": "Tar bort avgrensing i søke-djupna, og varmar opp maskina", - "engineManager": "Innstillingar for sjakkprogram", "blunder": "Bukk", "mistake": "Mistak", "inaccuracy": "Småfeil", @@ -597,6 +673,7 @@ "rank": "Rangering", "rankX": "Plassering: {param}", "gamesPlayed": "Spelte parti", + "ok": "Ok", "cancel": "Avbryt", "whiteTimeOut": "Tida er ute for kvit", "blackTimeOut": "Tida er ute for svart", @@ -705,16 +782,15 @@ "reconnecting": "Koplar til på ny", "noNetwork": "Fråkopla", "favoriteOpponents": "Favorittmotstandarar", - "follow": "Fylgj", - "following": "Fylgjer", - "unfollow": "Slutt å fylgja", + "follow": "Følg", + "following": "Følgjer", + "unfollow": "Slutt å følgja", "followX": "Følg {param}", "unfollowX": "Slutt å følgja {param}", "block": "Blokkér", "blocked": "Blokkert", "unblock": "Fjern blokkering", - "followsYou": "Fylgjer deg", - "xStartedFollowingY": "{param1} byrja å fylgja {param2}", + "xStartedFollowingY": "{param1} byrja å følgja {param2}", "more": "Meir", "memberSince": "Medlem sidan", "lastSeenActive": "Siste innlogging {param}", @@ -819,7 +895,9 @@ "cheat": "Juks", "troll": "Troll", "other": "Anna", - "reportDescriptionHelp": "Lim inn link til partiet/partia og forklar kva som er gale med åtferda til denne brukaren.", + "reportCheatBoostHelp": "Legg ved lenke til partiet/partia og forklar kva som er gale med åtferda til denne brukaren. Å berre påstå at brukaren juksar er ikkje nok, men gje ei nærare forklaring på korleis du kom til denne konklusjonen.", + "reportUsernameHelp": "Forklår kva som gjer brukarnamnet er støytande. Det held ikkje med å påstå at \"namnet er støytande/upassande\", men fortell oss korleis du kom til denne konklusjonen, spesielt om tydinga er uklår, ikkje er på engelsk, er eit slanguttrykk, eller har ein historisk/kulturell referanse.", + "reportProcessedFasterInEnglish": "Rapporten din blir raskare behandla om du skriv på engelsk.", "error_provideOneCheatedGameLink": "Oppgje minst ei lenke til eit jukseparti.", "by": "av {param}", "importedByX": "Importert av {param}", @@ -842,7 +920,7 @@ "clockIncrement": "Inkrement", "privacy": "Privatsfære", "privacyPolicy": "Personvernpolitikk", - "letOtherPlayersFollowYou": "Lat andre spelarar fylgja deg", + "letOtherPlayersFollowYou": "Lat andre spelarar følgja deg", "letOtherPlayersChallengeYou": "Lat andre spelarar utfordra deg", "letOtherPlayersInviteYouToStudy": "Lat andre spelarar invitere til studium", "sound": "Lyd", @@ -1217,6 +1295,7 @@ "showMeEverything": "Vis alt", "lichessPatronInfo": "Lichess er ein velgjerdsorganisasjon basert på fritt tilgjengeleg open-kjeldekode-programvare.\nAlle kostnader for drift, utvikling og innhald vert finansiert eine og åleine av brukardonasjonar.", "nothingToSeeHere": "Ikkje noko å sjå nett no.", + "stats": "Statistikk", "opponentLeftCounter": "{count, plural, =1{Motspelaren din har forlate partiet. Du kan krevje vinst om {count} sekund.} other{Motspelaren din har forlate partiet. Du kan krevje siger om {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Matt om {count} halvtrekk} other{Matt om {count} halvtrekk}}", "nbBlunders": "{count, plural, =1{{count} bukk} other{{count} bukkar}}", @@ -1247,8 +1326,8 @@ "needNbMoreGames": "{count, plural, =1{Du må spela endå {count} rangert parti} other{Du må spela endå {count} rangerte parti}}", "nbImportedGames": "{count, plural, =1{{count} importert parti} other{{count} importerte parti}}", "nbFriendsOnline": "{count, plural, =1{{count} ven er innlogga} other{{count} vener er innlogga}}", - "nbFollowers": "{count, plural, =1{{count} fylgjar} other{{count} fylgjarar}}", - "nbFollowing": "{count, plural, =1{{count} fylgjer} other{{count} fylgjer}}", + "nbFollowers": "{count, plural, =1{{count} følgjar} other{{count} følgjarar}}", + "nbFollowing": "{count, plural, =1{{count} følgjer} other{{count} fylgjer}}", "lessThanNbMinutes": "{count, plural, =1{Mindre enn {count} minutt} other{Mindre enn {count} minutt}}", "nbGamesInPlay": "{count, plural, =1{{count} parti pågår} other{{count} parti pågår}}", "maximumNbCharacters": "{count, plural, =1{Maksimalt: {count} bokstav.} other{Maksimalt: {count} bokstavar.}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 runde} other{{count} rundar}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Har spela ein runde med {param2}} other{Har spela {count} rundar med {param2}}}", "streamerLichessStreamers": "Lichess-strøymarar", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studiar", + "studyStudiesIContributeTo": "Studiar eg bidreg til", + "studyMyPublicStudies": "Mine offentlege studiar", + "studyMyPrivateStudies": "Mine private studiar", + "studyMyFavoriteStudies": "Mine favorittstudiar", + "studyWhatAreStudies": "Kva er studiar?", + "studyAllStudies": "Alle studiar", + "studyStudiesCreatedByX": "Studiar oppretta av {param}", + "studyNoneYet": "Ingen så langt.", + "studyHot": "Omtykt", + "studyDateAddedNewest": "Dato tilføydd (siste)", + "studyDateAddedOldest": "Dato tilføydd (første)", + "studyRecentlyUpdated": "Nyleg oppdatert", + "studyMostPopular": "Mest omtykt", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Føy til eit nytt kapittel", + "studyAddMembers": "Legg til medlemar", + "studyInviteToTheStudy": "Inviter til studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter berre folk du kjenner og som aktivt ynskjer å delta i studien.", + "studySearchByUsername": "Søk på brukarnamn", + "studySpectator": "Tilskodar", + "studyContributor": "Bidragsytar", + "studyKick": "Kast ut", + "studyLeaveTheStudy": "Forlat studien", + "studyYouAreNowAContributor": "Du er no bidragsytar", + "studyYouAreNowASpectator": "Du er no tilskodar", + "studyPgnTags": "PGN-merkelappar", + "studyLike": "Lik", + "studyUnlike": "Slutt å lika", + "studyNewTag": "Ny merkelapp", + "studyCommentThisPosition": "Kommenter denne stillinga", + "studyCommentThisMove": "Kommenter dette trekket", + "studyAnnotateWithGlyphs": "Kommenter med symbol", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittelet er for kort for å analyserast.", + "studyOnlyContributorsCanRequestAnalysis": "Berre bidragsytarar til studien kan be om maskinanalyse.", + "studyGetAFullComputerAnalysis": "Få full maskinanalyse av hovedvarianten frå serveren.", + "studyMakeSureTheChapterIsComplete": "Sørg for at kapittelet er fullført. Du kan berre be om analyse ein gong.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC-medlemene ser den same stillingen", + "studyShareChanges": "Lagre endringar på serveren og del dei med tilskodarar", + "studyPlaying": "Spelar no", + "studyShowEvalBar": "Evalueringssøyler", + "studyFirst": "Første", + "studyPrevious": "Attende", + "studyNext": "Neste", + "studyLast": "Siste", "studyShareAndExport": "Del & eksporter", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie-PGN", + "studyDownloadAllGames": "Last ned alle spel", + "studyChapterPgn": "Kapittel-PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Last ned spel", + "studyStudyUrl": "Studie-URL", + "studyCurrentChapterUrl": "Kapittel-URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan lime inn dette i forumet for å syna det der", + "studyStartAtInitialPosition": "Start ved innleiande stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Inkorporer i websida eller bloggen din", + "studyReadMoreAboutEmbedding": "Les meir om innbygging", + "studyOnlyPublicStudiesCanBeEmbedded": "Berre offentlege studiar kan byggast inn!", + "studyOpen": "Opne", + "studyXBroughtToYouByY": "{param1} presentert av {param2}", + "studyStudyNotFound": "Fann ikkje studien", + "studyEditChapter": "Rediger kapittel", + "studyNewChapter": "Nytt kapittel", + "studyImportFromChapterX": "Importer frå {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Fastspikra kapittelkommentar", + "studySaveChapter": "Lagre kapittelet", + "studyClearAnnotations": "Fjern notat", + "studyClearVariations": "Fjern variantar", + "studyDeleteChapter": "Slett kapittel", + "studyDeleteThisChapter": "Slette dette kapittelet? Avgjerda er endeleg og kan ikkje angrast!", + "studyClearAllCommentsInThisChapter": "Fjern alle kommentarar og figurar i dette kapittelet?", + "studyRightUnderTheBoard": "Rett under brettet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul neste trekk", + "studyInteractiveLesson": "Interaktiv leksjon", + "studyChapterX": "Kapittel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved innleiande stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start frå innleiande stilling", + "studyLoadAGameByUrl": "Last opp eit parti frå URL", + "studyLoadAPositionFromFen": "Last opp ein stilling frå FEN", + "studyLoadAGameFromPgn": "Last opp eit parti frå PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiet", + "studyLoadAGameFromXOrY": "Last opp eit parti frå {param1} eller {param2}", + "studyCreateChapter": "Opprett kapittel", + "studyCreateStudy": "Opprett ein studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlegheit", + "studyPublic": "Offentleg", + "studyUnlisted": "Ikkje opplista", + "studyInviteOnly": "Berre etter invitasjon", + "studyAllowCloning": "Tillat kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Berre meg", + "studyContributors": "Bidragsytarar", + "studyMembers": "Medlemar", + "studyEveryone": "Alle", + "studyEnableSync": "Aktiver synk", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behald alle i den same stilllinga", + "studyNoLetPeopleBrowseFreely": "Nei: lat folk sjå fritt gjennom", + "studyPinnedStudyComment": "Fastspikra studiekommentar", + "studyStart": "Start", + "studySave": "Lagre", + "studyClearChat": "Fjern teksten frå kommentarfeltet", + "studyDeleteTheStudyChatHistory": "Slette studiens kommentar-historikk? Du kan ikkje angre!", + "studyDeleteStudy": "Slett studie", + "studyConfirmDeleteStudy": "Slette heile studien? Avgjerda er endeleg og kan ikke gjeras om! Skriv namnet på studien som skal stadfestast: {param}", + "studyWhereDoYouWantToStudyThat": "Kva for ein studie vil du bruke?", + "studyGoodMove": "Godt trekk", + "studyMistake": "Mistak", + "studyBrilliantMove": "Strålande trekk", + "studyBlunder": "Bukk", + "studyInterestingMove": "Interessant trekk", + "studyDubiousMove": "Tvilsamt trekk", + "studyOnlyMove": "Einaste moglege trekk", + "studyZugzwang": "Trekktvang", + "studyEqualPosition": "Lik stilling", + "studyUnclearPosition": "Uavklart stilling", + "studyWhiteIsSlightlyBetter": "Kvit står litt betre", + "studyBlackIsSlightlyBetter": "Svart står litt betre", + "studyWhiteIsBetter": "Kvit står betre", + "studyBlackIsBetter": "Svart står betre", + "studyWhiteIsWinning": "Kvit står til vinst", + "studyBlackIsWinning": "Svart står til vinst", + "studyNovelty": "Nyskapning", + "studyDevelopment": "Utvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Åtak", + "studyCounterplay": "Motspel", + "studyTimeTrouble": "Tidsnaud", + "studyWithCompensation": "Med kompensasjon", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Neste kapittel", + "studyPrevChapter": "Førre kapittel", + "studyStudyActions": "Studiehandlingar", + "studyTopics": "Tema", + "studyMyTopics": "Mine tema", + "studyPopularTopics": "Omtykte tema", + "studyManageTopics": "Administrer tema", + "studyBack": "Tilbake", + "studyPlayAgain": "Spel på ny", + "studyWhatWouldYouPlay": "Kva vil du spela i denne stillinga?", + "studyYouCompletedThisLesson": "Gratulerar! Du har fullført denne leksjonen.", + "studyPerPage": "{param} per side", + "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapittel}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} parti}}", + "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemar}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} parti}}", + "timeagoJustNow": "for kort tid sidan", + "timeagoRightNow": "nett no", + "timeagoCompleted": "fullført", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minutt} other{om {count} minutt}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timar}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dagar}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} veke} other{om {count} veker}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} månad} other{om {count} månader}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutt sidan} other{{count} minutt sidan}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} time sidan} other{{count} timar sidan}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag sidan} other{{count} dagar sidan}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} veke sidan} other{{count} veker sidan}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} månad sidan} other{{count} månader sidan}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år sidan} other{{count} år sidan}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutt igjen} other{{count} minutt igjen}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time igjen} other{{count} timar igjen}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pl.arb b/lib/l10n/lila_pl.arb index 0e35a8a68e..62513b3258 100644 --- a/lib/l10n/lila_pl.arb +++ b/lib/l10n/lila_pl.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Wszystkie partie", + "mobileAreYouSure": "Jesteś pewien?", + "mobileBlindfoldMode": "Gra na ślepo", + "mobileCancelTakebackOffer": "Anuluj prośbę cofnięcia ruchu", + "mobileClearButton": "Wyczyść", + "mobileCorrespondenceClearSavedMove": "Usuń zapisany ruch", + "mobileCustomGameJoinAGame": "Dołącz do partii", + "mobileFeedbackButton": "Opinie", + "mobileGreeting": "Witaj {param}", + "mobileGreetingWithoutName": "Witaj", + "mobileHideVariation": "Ukryj wariant", "mobileHomeTab": "Start", - "mobilePuzzlesTab": "Zadania", - "mobileToolsTab": "Narzędzia", - "mobileWatchTab": "Oglądaj", - "mobileSettingsTab": "Ustawienia", + "mobileLiveStreamers": "Aktywni streamerzy", "mobileMustBeLoggedIn": "Musisz być zalogowany, aby wyświetlić tę stronę.", - "mobileSystemColors": "Kolory systemowe", - "mobileFeedbackButton": "Opinie", + "mobileNoSearchResults": "Brak wyników", + "mobileNotFollowingAnyUser": "Nie obserwujesz żadnego gracza.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Gracze pasujący do \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Powiększ przeciąganą bierkę", + "mobilePuzzleStormConfirmEndRun": "Czy chcesz zakończyć tę serię?", + "mobilePuzzleStormFilterNothingToShow": "Brak wyników, zmień proszę filtry", + "mobilePuzzleStormNothingToShow": "Nic do wyświetlenia. Rozegraj kilka serii.", + "mobilePuzzleStormSubtitle": "Rozwiąż jak najwięcej zadań w ciągu 3 minut.", + "mobilePuzzleStreakAbortWarning": "Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.", + "mobilePuzzleThemesSubtitle": "Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.", + "mobilePuzzlesTab": "Zadania", + "mobileRecentSearches": "Ostatnio wyszukiwane", "mobileSettingsHapticFeedback": "Wibracja przy dotknięciu", "mobileSettingsImmersiveMode": "Tryb pełnoekranowy", "mobileSettingsImmersiveModeSubtitle": "Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.", - "mobileNotFollowingAnyUser": "Nie obserwujesz żadnego gracza.", - "mobileAllGames": "Wszystkie partie", - "mobileRecentSearches": "Ostatnio wyszukiwane", - "mobileClearButton": "Wyczyść", - "mobilePlayersMatchingSearchTerm": "Gracze pasujący do \"{param}\"", - "mobileNoSearchResults": "Brak wyników", - "mobileAreYouSure": "Jesteś pewien?", - "mobilePuzzleStreakAbortWarning": "Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.", - "mobilePuzzleStormNothingToShow": "Nic do wyświetlenia. Rozegraj kilka serii.", - "mobileSharePuzzle": "Udostępnij to zadanie", - "mobileShareGameURL": "Udostępnij adres URL partii", + "mobileSettingsTab": "Ustawienia", "mobileShareGamePGN": "Udostępnij PGN", + "mobileShareGameURL": "Udostępnij adres URL partii", "mobileSharePositionAsFEN": "Udostępnij pozycję jako FEN", - "mobileShowVariations": "Pokaż warianty", - "mobileHideVariation": "Ukryj wariant", + "mobileSharePuzzle": "Udostępnij to zadanie", "mobileShowComments": "Pokaż komentarze", - "mobilePuzzleStormConfirmEndRun": "Czy chcesz zakończyć tę serię?", - "mobilePuzzleStormFilterNothingToShow": "Brak wyników, zmień proszę filtry", - "mobileCancelTakebackOffer": "Anuluj prośbę cofnięcia ruchu", - "mobileCancelDrawOffer": "Anuluj propozycję remisu", - "mobileWaitingForOpponentToJoin": "Oczekiwanie na dołączenie przeciwnika...", - "mobileBlindfoldMode": "Gra na ślepo", - "mobileLiveStreamers": "Aktywni streamerzy", - "mobileCustomGameJoinAGame": "Dołącz do partii", - "mobileCorrespondenceClearSavedMove": "Usuń zapisany ruch", - "mobileSomethingWentWrong": "Coś poszło nie tak.", "mobileShowResult": "Pokaż wynik", - "mobilePuzzleThemesSubtitle": "Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.", - "mobilePuzzleStormSubtitle": "Rozwiąż jak najwięcej zadań w ciągu 3 minut.", - "mobileGreeting": "Witaj {param}", - "mobileGreetingWithoutName": "Witaj", + "mobileShowVariations": "Pokaż warianty", + "mobileSomethingWentWrong": "Coś poszło nie tak.", + "mobileSystemColors": "Kolory systemowe", + "mobileTheme": "Motyw", + "mobileToolsTab": "Narzędzia", + "mobileWaitingForOpponentToJoin": "Oczekiwanie na dołączenie przeciwnika...", + "mobileWatchTab": "Oglądaj", "activityActivity": "Aktywność", "activityHostedALiveStream": "Udostępnił stream na żywo", "activityRankedInSwissTournament": "{param1} miejsce w {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Wykonany {count} ruch} few{Wykonane {count} ruchy} many{Wykonane {count} ruchów} other{Wykonane {count} ruchów}}", "activityInNbCorrespondenceGames": "{count, plural, =1{w {count} partii korespondencyjnej} few{w {count} partiach korespondencyjnych} many{w {count} partiach korespondencyjnych} other{w {count} partiach korespondencyjnych}}", "activityCompletedNbGames": "{count, plural, =1{Zakończenie partii korespondencyjnej} few{Zakończenie {count} partii korespondencyjnych} many{Zakończenie {count} partii korespondencyjnych} other{Zakończenie {count} partii korespondencyjnych}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Zakończona {count} {param2} partia korespondencyjna} few{Zakończone {count} {param2} partie korespondencyjne} many{Zakończone {count} {param2} partii korespondencyjnych} other{Zakończone {count} {param2} partii korespondencyjnych}}", "activityFollowedNbPlayers": "{count, plural, =1{Rozpoczęcie obserwowania gracza} few{Rozpoczęcie obserwowania {count} graczy} many{Rozpoczęcie obserwowania {count} graczy} other{Rozpoczęcie obserwowania {count} graczy}}", "activityGainedNbFollowers": "{count, plural, =1{Zyskano {count} nowego obserwującego/-ą} few{Zyskano {count} nowych obserwujących} many{Zyskano {count} nowych obserwujących} other{Zyskano {count} nowych obserwujących}}", "activityHostedNbSimuls": "{count, plural, =1{Rozegranie symultany} few{Rozegranie {count} symultan} many{Rozegranie {count} symultan} other{Rozegranie {count} symultan}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Uczestniczył(a) w {count} turnieju szwajcarskim} few{Uczestniczył(a) w {count} turniejach szwajcarskich} many{Uczestniczył(a) w {count} turniejach szwajcarskich} other{Uczestniczył(a) w {count} turniejach szwajcarskich}}", "activityJoinedNbTeams": "{count, plural, =1{Dołączenie do klubu} few{Dołączenie do {count} klubów} many{Dołączenie do {count} klubów} other{Dołączono do {count} zespołów}}", "broadcastBroadcasts": "Transmisje", + "broadcastMyBroadcasts": "Moje transmisje", "broadcastLiveBroadcasts": "Transmisje turniejów na żywo", + "broadcastBroadcastCalendar": "Kalendarz transmisji", + "broadcastNewBroadcast": "Nowa transmisja na żywo", + "broadcastSubscribedBroadcasts": "Subskrybowane transmisje", + "broadcastAboutBroadcasts": "O transmisji", + "broadcastHowToUseLichessBroadcasts": "Jak korzystać z transmisji na Lichess.", + "broadcastTheNewRoundHelp": "Nowa runda będzie miała tych samych uczestników co poprzednia.", + "broadcastAddRound": "Dodaj rundę", + "broadcastOngoing": "Trwające", + "broadcastUpcoming": "Nadchodzące", + "broadcastCompleted": "Zakończone", + "broadcastCompletedHelp": "Lichess wykrywa ukończenie rundy w oparciu o śledzone partie. Użyj tego przełącznika, jeśli nie ma takich partii.", + "broadcastRoundName": "Nazwa rundy", + "broadcastRoundNumber": "Numer rundy", + "broadcastTournamentName": "Nazwa turnieju", + "broadcastTournamentDescription": "Krótki opis turnieju", + "broadcastFullDescription": "Pełny opis wydarzenia", + "broadcastFullDescriptionHelp": "Opcjonalny długi opis transmisji. {param1} jest dostępny. Długość musi być mniejsza niż {param2} znaków.", + "broadcastSourceSingleUrl": "Adres URL zapisu PGN", + "broadcastSourceUrlHelp": "Adres URL, który Lichess będzie udostępniał, aby można było uzyskać aktualizacje PGN. Musi być publicznie dostępny z internetu.", + "broadcastSourceGameIds": "Do 64 identyfikatorów partii, oddzielonych spacjami.", + "broadcastStartDateTimeZone": "Data rozpoczęcia w lokalnej strefie czasowej turnieju: {param}", + "broadcastStartDateHelp": "Opcjonalne, jeśli wiesz kiedy wydarzenie się rozpocznie", + "broadcastCurrentGameUrl": "Adres URL bieżącej partii", + "broadcastDownloadAllRounds": "Pobierz wszystkie rundy", + "broadcastResetRound": "Zresetuj tę rundę", + "broadcastDeleteRound": "Usuń tę rundę", + "broadcastDefinitivelyDeleteRound": "Ostatecznie usuń rundę i jej wszystkie partie.", + "broadcastDeleteAllGamesOfThisRound": "Usuń wszystkie partie w tej rundzie. Źródło będzie musiało być aktywne, aby je odtworzyć.", + "broadcastEditRoundStudy": "Edytuj opracowanie rundy", + "broadcastDeleteTournament": "Usuń ten turniej", + "broadcastDefinitivelyDeleteTournament": "Ostatecznie usuń cały turniej, jego wszystkie rundy i partie.", + "broadcastShowScores": "Pokaż wyniki graczy na podstawie wyników gry", + "broadcastReplacePlayerTags": "Opcjonalnie: zmień nazwy, rankingi oraz tytuły gracza", + "broadcastFideFederations": "Federacje FIDE", + "broadcastTop10Rating": "10 najlepszych rankingów", + "broadcastFidePlayers": "Zawodnicy FIDE", + "broadcastFidePlayerNotFound": "Nie znaleziono zawodnika FIDE", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federacja", + "broadcastAgeThisYear": "Wiek w tym roku", + "broadcastUnrated": "Bez rankingu", + "broadcastRecentTournaments": "Najnowsze turnieje", + "broadcastOpenLichess": "Otwórz w Lichess", + "broadcastTeams": "Drużyny", + "broadcastBoards": "Szachownice", + "broadcastOverview": "Podgląd", + "broadcastSubscribeTitle": "Subskrybuj, aby dostawać powiadomienia o każdej rozpoczętej rundzie. W preferencjach konta możesz przełączać czy chcesz powiadomienia dźwiękowe czy wyskakujące notyfikacje tekstowe.", + "broadcastUploadImage": "Prześlij logo turnieju", + "broadcastNoBoardsYet": "Szachownice pojawią się jak tylko załadują się partie.", + "broadcastBoardsCanBeLoaded": "Szachownice mogą być załadowane bezpośrednio ze źródła lub przez {param}", + "broadcastStartsAfter": "Rozpoczyna się po {param}", + "broadcastStartVerySoon": "Transmisja wkrótce się rozpocznie.", + "broadcastNotYetStarted": "Transmisja jeszcze się nie rozpoczęła.", + "broadcastOfficialWebsite": "Oficjalna strona", + "broadcastStandings": "Klasyfikacja", + "broadcastOfficialStandings": "Oficjalna klasyfikacja", + "broadcastIframeHelp": "Więcej opcji na {param}", + "broadcastWebmastersPage": "stronie webmasterów", + "broadcastPgnSourceHelp": "Publiczne źródło PGN w czasie rzeczywistym dla tej rundy. Oferujemy również {param} dla szybszej i skuteczniejszej synchronizacji.", + "broadcastEmbedThisBroadcast": "Umieść tę transmisję na swojej stronie internetowej", + "broadcastEmbedThisRound": "Osadź {param} na swojej stronie internetowej", + "broadcastRatingDiff": "Różnica rankingu", + "broadcastGamesThisTournament": "Partie w tym turnieju", + "broadcastScore": "Wynik", + "broadcastAllTeams": "Wszystkie kluby", + "broadcastTournamentFormat": "Format turnieju", + "broadcastTournamentLocation": "Lokalizacja turnieju", + "broadcastTopPlayers": "Najlepsi gracze", + "broadcastTimezone": "Strefa czasowa", + "broadcastFideRatingCategory": "Kategoria rankingu FIDE", + "broadcastOptionalDetails": "Opcjonalne szczegóły", + "broadcastPastBroadcasts": "Poprzednie transmisje", + "broadcastAllBroadcastsByMonth": "Zobacz wszystkie transmisje w danym miesiącu", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisja} few{{count} transmisje} many{{count} transmisji} other{{count} transmisji}}", "challengeChallengesX": "Wyzwania: {param1}", "challengeChallengeToPlay": "Zaproś do gry", "challengeChallengeDeclined": "Wyzwanie odrzucone", @@ -95,7 +172,7 @@ "perfStatPerfStats": "Statystyki dla {param}", "perfStatViewTheGames": "Zobacz partie", "perfStatProvisional": "prowizoryczny", - "perfStatNotEnoughRatedGames": "Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodną ranking.", + "perfStatNotEnoughRatedGames": "Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodny ranking.", "perfStatProgressOverLastXGames": "Postęp w ostatnich {param} partiach:", "perfStatRatingDeviation": "Odchylenie rankingu: {param}.", "perfStatRatingDeviationTooltip": "Niższa wartość oznacza, że ranking jest bardziej stabilny. Powyżej {param1}, ranking jest uważany za tymczasowy. Aby znaleźć się na listach rankingowych, wartość ta powinna być niższa niż {param2} (standardowe szachy) lub {param3} (warianty).", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Przeglądarka", "preferencesNotifyDevice": "Urządzenie", "preferencesBellNotificationSound": "Dźwięk powiadomień", + "preferencesBlindfold": "Gra na ślepo", "puzzlePuzzles": "Zadania szachowe", "puzzlePuzzleThemes": "Motywy zadań", "puzzleRecommended": "Polecane", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Figura atakuje albo broni pole przez wrogą figurę.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Ograniczone ruchy przeciwnika powodują, że każde posunięcie pogarsza jego pozycję.", - "puzzleThemeHealthyMix": "Miszmasz", - "puzzleThemeHealthyMixDescription": "Bądź gotów na wszystko! Jak podczas prawdziwej partii.", + "puzzleThemeMix": "Miszmasz", + "puzzleThemeMixDescription": "Po trochu wszystkiego. Nie wiesz czego się spodziewać, więc bądź gotów na wszystko! Tak jak w prawdziwej partii.", "puzzleThemePlayerGames": "Partie gracza", "puzzleThemePlayerGamesDescription": "Wyszukaj zadania wygenerowane z Twoich partii lub z partii innego gracza.", "puzzleThemePuzzleDownloadInformation": "Te zadania dostępne są w domenie publicznej i mogą być pobrane z {param}.", @@ -465,7 +543,7 @@ "deleteFromHere": "Usuń od tego miejsca", "collapseVariations": "Zwiń warianty", "expandVariations": "Rozwiń warianty", - "forceVariation": "Przedstaw jako wariant", + "forceVariation": "Zamień w wariant", "copyVariationPgn": "Skopiuj wariant PGN", "move": "Ruch", "variantLoss": "Wariant przegrywający", @@ -492,7 +570,7 @@ "openingExplorer": "Biblioteka otwarć", "openingEndgameExplorer": "Biblioteka otwarć i końcówek", "xOpeningExplorer": "Biblioteka otwarć {param}", - "playFirstOpeningEndgameExplorerMove": "Zagraj pierwsze posunięcie z przeglądarki otwarć/końcówek", + "playFirstOpeningEndgameExplorerMove": "Zagraj pierwsze posunięcie z biblioteki otwarć", "winPreventedBy50MoveRule": "Bez wygranej ze względu na regułę 50 ruchów", "lossSavedBy50MoveRule": "Bez przegranej ze względu na regułę 50 ruchów", "winOr50MovesByPriorMistake": "Zwycięstwo lub 50 posunięć bez rozstrzygnięcia", @@ -505,7 +583,6 @@ "replayMode": "Tryb odtwarzania", "realtimeReplay": "Jak w grze", "byCPL": "Wg SCP", - "openStudy": "Otwórz opracowanie", "enable": "Włącz", "bestMoveArrow": "Strzałka najlepszego ruchu", "showVariationArrows": "Pokaż strzałki wariantów", @@ -515,7 +592,6 @@ "memory": "Pamięć RAM", "infiniteAnalysis": "Nieskończona analiza", "removesTheDepthLimit": "Usuwa limit głębokości analizy i rozgrzewa Twój komputer do czerwoności ;)", - "engineManager": "Ustawienia silnika", "blunder": "Błąd", "mistake": "Pomyłka", "inaccuracy": "Niedokładność", @@ -597,6 +673,7 @@ "rank": "Miejsce", "rankX": "Miejsce: {param}", "gamesPlayed": "Rozegranych partii", + "ok": "OK", "cancel": "Anuluj", "whiteTimeOut": "Upłynął czas białych", "blackTimeOut": "Upłynął czas czarnych", @@ -713,7 +790,6 @@ "block": "Zablokuj", "blocked": "Zablokowany", "unblock": "Odblokuj", - "followsYou": "Obserwuje Cię", "xStartedFollowingY": "{param1} obserwuje {param2}", "more": "Więcej", "memberSince": "Zarejestrowano", @@ -800,6 +876,7 @@ "no": "Nie", "yes": "Tak", "website": "Strona internetowa", + "mobile": "Aplikacja mobilna", "help": "Porada:", "createANewTopic": "Załóż nowy temat", "topics": "Liczba tematów", @@ -818,7 +895,9 @@ "cheat": "Oszust", "troll": "Natręt (troll)", "other": "Inne", - "reportDescriptionHelp": "Wklej odnośnik do partii i wyjaśnij, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko, że „oszukuje”, ale wytłumacz nam, na jakiej podstawie doszedłeś/aś do takiego wniosku. Odniesiemy się do twojego zgłoszenia szybciej, jeżeli napiszesz je w języku angielskim.", + "reportCheatBoostHelp": "Wklej link do partii i wytłumacz, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko \"on oszukiwał\", lecz napisz jak doszedłeś/aś do tego wniosku.", + "reportUsernameHelp": "Wytłumacz, co w nazwie użytkownika jest obraźliwe. Nie pisz tylko \"nazwa jest obraźliwa\", lecz napisz jak doszedłeś/aś do tego wniosku, zwłaszcza jeśli jest mało znany, nie po angielsku, slangowy lub odniesieniem do kultury/historii.", + "reportProcessedFasterInEnglish": "Twoje zgłoszenie będzie sprawdzone szybciej, jeśli zostanie napisane po angielsku.", "error_provideOneCheatedGameLink": "Podaj przynajmniej jeden odnośnik do gry, w której oszukiwano.", "by": "autor {param}", "importedByX": "Zaimportowane przez {param}", @@ -1216,6 +1295,7 @@ "showMeEverything": "Pokaż mi wszystko", "lichessPatronInfo": "Lichess jest organizacją niedochodową i całkowicie darmowym otwartym oprogramowaniem.\nWszystkie koszty operacyjne, rozwój i treści są finansowane wyłącznie z darowizn użytkowników.", "nothingToSeeHere": "W tej chwili nie ma nic do zobaczenia.", + "stats": "Statystyki", "opponentLeftCounter": "{count, plural, =1{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekundę.} few{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekundy.} many{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekund.} other{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Mat w {count} posunięciu} few{Mat w {count} posunięciach} many{Mat w {count} posunięciach} other{Mat w {count} posunięciach}}", "nbBlunders": "{count, plural, =1{{count} błąd} few{{count} błędy} many{{count} błędów} other{{count} błędów}}", @@ -1312,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 próba} few{{count} próby} many{{count} prób} other{{count} prób}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Rozegrano jedną sesję {param2}} few{Rozegrano {count} sesje {param2}} many{Rozegrano {count} sesji {param2}} other{Rozegrano {count} sesji {param2}}}", "streamerLichessStreamers": "Streamerzy Lichess", + "studyPrivate": "Prywatne", + "studyMyStudies": "Moje opracowania", + "studyStudiesIContributeTo": "Opracowania, które współtworzę", + "studyMyPublicStudies": "Moje publiczne opracowania", + "studyMyPrivateStudies": "Moje prywatne opracowania", + "studyMyFavoriteStudies": "Moje ulubione opracowania", + "studyWhatAreStudies": "Czym są opracowania?", + "studyAllStudies": "Wszystkie opracowania", + "studyStudiesCreatedByX": "Opracowanie stworzone przez {param}", + "studyNoneYet": "Jeszcze brak.", + "studyHot": "Hity", + "studyDateAddedNewest": "Data dodania (od najnowszych)", + "studyDateAddedOldest": "Data dodania (od najstarszych)", + "studyRecentlyUpdated": "Ostatnio aktualizowane", + "studyMostPopular": "Najpopularniejsze", + "studyAlphabetical": "Alfabetycznie", + "studyAddNewChapter": "Dodaj nowy rozdział", + "studyAddMembers": "Dodaj uczestników", + "studyInviteToTheStudy": "Zaproś do opracowania", + "studyPleaseOnlyInvitePeopleYouKnow": "Zapraszaj do opracowania tylko znajomych, którzy chcą w nim aktywnie uczestniczyć.", + "studySearchByUsername": "Szukaj wg nazwy użytkownika", + "studySpectator": "Obserwator", + "studyContributor": "Współautor", + "studyKick": "Wyrzuć", + "studyLeaveTheStudy": "Opuść opracowanie", + "studyYouAreNowAContributor": "Jesteś teraz współautorem", + "studyYouAreNowASpectator": "Jesteś teraz obserwatorem", + "studyPgnTags": "Znaczniki PGN", + "studyLike": "Lubię to", + "studyUnlike": "Cofnij polubienie", + "studyNewTag": "Nowy znacznik", + "studyCommentThisPosition": "Skomentuj tę pozycję", + "studyCommentThisMove": "Skomentuj ten ruch", + "studyAnnotateWithGlyphs": "Dodaj adnotacje symbolami", + "studyTheChapterIsTooShortToBeAnalysed": "Rozdział jest zbyt krótki do analizy.", + "studyOnlyContributorsCanRequestAnalysis": "Tylko współautorzy opracowania mogą prosić o analizę komputerową.", + "studyGetAFullComputerAnalysis": "Uzyskaj pełną, zdalną analizę komputerową głównego wariantu.", + "studyMakeSureTheChapterIsComplete": "Upewnij się, że rozdział jest kompletny. O jego analizę możesz poprosić tylko raz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Wszyscy zsynchronizowani uczestnicy pozostają na tej samej pozycji", + "studyShareChanges": "Współdzielenie zmian z obserwatorami i ich zapis na serwerze", + "studyPlaying": "W toku", + "studyShowEvalBar": "Paski ewaluacji", + "studyFirst": "Pierwszy", + "studyPrevious": "Poprzedni", + "studyNext": "Następny", + "studyLast": "Ostatni", "studyShareAndExport": "Udostępnianie i eksport", - "studyStart": "Rozpocznij" + "studyCloneStudy": "Powiel", + "studyStudyPgn": "PGN opracowania", + "studyDownloadAllGames": "Pobierz wszystkie partie", + "studyChapterPgn": "PGN rozdziału", + "studyCopyChapterPgn": "Kopiuj PGN", + "studyDownloadGame": "Pobierz partię", + "studyStudyUrl": "Link do opracowania", + "studyCurrentChapterUrl": "URL bieżącego rozdziału", + "studyYouCanPasteThisInTheForumToEmbed": "Możesz wkleić to, aby osadzić na forum", + "studyStartAtInitialPosition": "Rozpocznij z pozycji początkowej", + "studyStartAtX": "Rozpocznij od {param}", + "studyEmbedInYourWebsite": "Udostępnij na swojej stronie lub na blogu", + "studyReadMoreAboutEmbedding": "Dowiedz się więcej o osadzaniu", + "studyOnlyPublicStudiesCanBeEmbedded": "Tylko publiczne opracowania mogą być osadzane!", + "studyOpen": "Otwórz", + "studyXBroughtToYouByY": "{param1} przygotowane przez {param2}", + "studyStudyNotFound": "Nie znaleziono opracowania", + "studyEditChapter": "Edytuj rozdział", + "studyNewChapter": "Nowy rozdział", + "studyImportFromChapterX": "Zaimportuj z {param}", + "studyOrientation": "Orientacja", + "studyAnalysisMode": "Rodzaj analizy", + "studyPinnedChapterComment": "Przypięty komentarz", + "studySaveChapter": "Zapisz rozdział", + "studyClearAnnotations": "Usuń adnotacje", + "studyClearVariations": "Wyczyść warianty", + "studyDeleteChapter": "Usuń rozdział", + "studyDeleteThisChapter": "Usunąć ten rozdział? Nie będzie można tego cofnąć!", + "studyClearAllCommentsInThisChapter": "Usunąć wszystkie komentarze i oznaczenia w tym rozdziale?", + "studyRightUnderTheBoard": "Pod szachownicą, po prawej stronie", + "studyNoPinnedComment": "Brak", + "studyNormalAnalysis": "Normalna", + "studyHideNextMoves": "Ukryj następne posunięcia", + "studyInteractiveLesson": "Lekcja interaktywna", + "studyChapterX": "Rozdział {param}", + "studyEmpty": "Pusty", + "studyStartFromInitialPosition": "Rozpocznij z pozycji początkowej", + "studyEditor": "Edytor", + "studyStartFromCustomPosition": "Rozpocznij z ustawionej pozycji", + "studyLoadAGameByUrl": "Zaimportuj partię z linku", + "studyLoadAPositionFromFen": "Zaimportuj partię z FEN", + "studyLoadAGameFromPgn": "Zaimportuj partię z PGN", + "studyAutomatic": "Automatycznie", + "studyUrlOfTheGame": "Link do partii", + "studyLoadAGameFromXOrY": "Zaimportuj partię z {param1} lub {param2}", + "studyCreateChapter": "Stwórz rozdział", + "studyCreateStudy": "Stwórz opracowanie", + "studyEditStudy": "Edytuj opracowanie", + "studyVisibility": "Widoczność", + "studyPublic": "Publiczne", + "studyUnlisted": "Niepubliczne", + "studyInviteOnly": "Tylko zaproszeni", + "studyAllowCloning": "Pozwól kopiować", + "studyNobody": "Nikt", + "studyOnlyMe": "Tylko ja", + "studyContributors": "Współautorzy", + "studyMembers": "Uczestnicy", + "studyEveryone": "Każdy", + "studyEnableSync": "Włącz synchronizację", + "studyYesKeepEveryoneOnTheSamePosition": "Tak: utrzymaj wszystkich w tej samej pozycji", + "studyNoLetPeopleBrowseFreely": "Nie: pozwól oglądać wszystkim", + "studyPinnedStudyComment": "Przypięte komentarze", + "studyStart": "Rozpocznij", + "studySave": "Zapisz", + "studyClearChat": "Wyczyść czat", + "studyDeleteTheStudyChatHistory": "Usunąć historię czatu opracowania? Nie będzie można tego cofnąć!", + "studyDeleteStudy": "Usuń opracowanie", + "studyConfirmDeleteStudy": "Usunąć opracowanie? Nie będzie można go odzyskać! Wpisz nazwę opracowania, aby potwierdzić operację: {param}", + "studyWhereDoYouWantToStudyThat": "Gdzie chcesz się tego uczyć?", + "studyGoodMove": "Dobry ruch", + "studyMistake": "Pomyłka", + "studyBrilliantMove": "Świetny ruch", + "studyBlunder": "Błąd", + "studyInterestingMove": "Interesujący ruch", + "studyDubiousMove": "Wątpliwy ruch", + "studyOnlyMove": "Jedyny ruch", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Równa pozycja", + "studyUnclearPosition": "Niejasna pozycja", + "studyWhiteIsSlightlyBetter": "Białe stoją nieznacznie lepiej", + "studyBlackIsSlightlyBetter": "Czarne stoją nieznacznie lepiej", + "studyWhiteIsBetter": "Białe stoją lepiej", + "studyBlackIsBetter": "Czarne stoją lepiej", + "studyWhiteIsWinning": "Białe wygrywają", + "studyBlackIsWinning": "Czarne wygrywają", + "studyNovelty": "Nowość", + "studyDevelopment": "Rozwój", + "studyInitiative": "Inicjatywa", + "studyAttack": "Atak", + "studyCounterplay": "Przeciwdziałanie", + "studyTimeTrouble": "Problem z czasem", + "studyWithCompensation": "Z rekompensatą", + "studyWithTheIdea": "Z pomysłem", + "studyNextChapter": "Następny rozdział", + "studyPrevChapter": "Poprzedni rozdział", + "studyStudyActions": "Opcje opracowań", + "studyTopics": "Tematy", + "studyMyTopics": "Moje tematy", + "studyPopularTopics": "Popularne tematy", + "studyManageTopics": "Zarządzaj tematami", + "studyBack": "Powrót", + "studyPlayAgain": "Odtwórz ponownie", + "studyWhatWouldYouPlay": "Co byś zagrał w tej pozycji?", + "studyYouCompletedThisLesson": "Gratulacje! Ukończono tę lekcję.", + "studyPerPage": "{param} na stronie", + "studyNbChapters": "{count, plural, =1{{count} rozdział} few{{count} rozdziały} many{{count} rozdziałów} other{{count} rozdziałów}}", + "studyNbGames": "{count, plural, =1{{count} partia} few{{count} partie} many{{count} partii} other{{count} partii}}", + "studyNbMembers": "{count, plural, =1{{count} uczestnik} few{{count} uczestników} many{{count} uczestników} other{{count} uczestników}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Wklej tutaj swój PGN, max {count} partię} few{Wklej tutaj swój PGN, max {count} partie} many{Wklej tutaj swój PGN, max {count} partii} other{Wklej tutaj swój PGN, max {count} partii}}", + "timeagoJustNow": "właśnie teraz", + "timeagoRightNow": "w tej chwili", + "timeagoCompleted": "ukończone", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundę} few{za {count} sekundy} many{za {count} sekund} other{za {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutę} few{za {count} minuty} many{za {count} minuty} other{za {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{za {count} godzinę} few{za {count} godziny} many{za {count} godzin} other{za {count} godzin}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dzień} few{za {count} dni} many{za {count} dni} other{za {count} dni}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} tydzień} few{za {count} tygodnie} many{za {count} tygodni} other{za {count} tygodni}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} miesiąc} few{za {count} miesiące} many{za {count} miesięcy} other{za {count} miesięcy}}", + "timeagoInNbYears": "{count, plural, =1{za {count} rok} few{za {count} lata} many{za {count} lat} other{za {count} lat}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutę temu} few{{count} minuty temu} many{{count} minut temu} other{{count} minut temu}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} godzinę temu} few{{count} godziny temu} many{{count} godzin temu} other{{count} godzin temu}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dzień temu} few{{count} dni temu} many{{count} dni temu} other{{count} dni temu}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} tydzień temu} few{{count} tygodnie temu} many{{count} tygodni temu} other{{count} tygodni temu}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} miesiąc temu} few{{count} miesiące temu} many{{count} miesięcy temu} other{{count} miesięcy temu}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} rok temu} few{{count} lata temu} many{{count} lat temu} other{{count} lat temu}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Pozostała {count} minuta} few{Pozostały {count} minuty} many{Pozostało {count} minut} other{Pozostało {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Pozostała {count} godzina} few{Pozostały {count} godziny} many{Pozostało {count} godzin} other{Pozostało {count} godzin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt.arb b/lib/l10n/lila_pt.arb index 59f532eb24..7f5ab000d6 100644 --- a/lib/l10n/lila_pt.arb +++ b/lib/l10n/lila_pt.arb @@ -1,41 +1,47 @@ { + "mobileAllGames": "Todos os jogos", + "mobileAreYouSure": "Tens a certeza?", + "mobileBlindfoldMode": "De olhos vendados", + "mobileCancelTakebackOffer": "Cancelar pedido de voltar", + "mobileClearButton": "Limpar", + "mobileCorrespondenceClearSavedMove": "Limpar movimento salvo", + "mobileCustomGameJoinAGame": "Entrar num jogo", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Olá, {param}", + "mobileGreetingWithoutName": "Olá", + "mobileHideVariation": "Ocultar variação", "mobileHomeTab": "Início", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Assistir", - "mobileSettingsTab": "Definições", + "mobileLiveStreamers": "Streamers em direto", "mobileMustBeLoggedIn": "Tem de iniciar sessão para visualizar esta página.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Sem resultados", + "mobileNotFollowingAnyUser": "Não segues nenhum utilizador.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Jogadores com \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça arrastada", + "mobilePuzzleStormConfirmEndRun": "Queres terminar esta corrida?", + "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar, por favor, altera os filtros", + "mobilePuzzleStormNothingToShow": "Nada para mostrar. Joga alguns Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resolve quantos problemas for possível em 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderas a tua sequência atual e a pontuação será salva.", + "mobilePuzzleThemesSubtitle": "Joga problemas das tuas aberturas favoritas, ou escolhe um tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Pesquisas recentes", "mobileSettingsHapticFeedback": "Feedback tátil", "mobileSettingsImmersiveMode": "Modo imersivo", "mobileSettingsImmersiveModeSubtitle": "Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.", - "mobileNotFollowingAnyUser": "Não segues nenhum utilizador.", - "mobileAllGames": "Todos os jogos", - "mobileRecentSearches": "Pesquisas recentes", - "mobileClearButton": "Limpar", - "mobilePlayersMatchingSearchTerm": "Jogadores com \"{param}\"", - "mobileNoSearchResults": "Sem resultados", - "mobileAreYouSure": "Tens a certeza?", - "mobilePuzzleStreakAbortWarning": "Perderas a tua sequência atual e a pontuação será salva.", - "mobilePuzzleStormNothingToShow": "Nada para mostrar. Joga alguns Puzzle Storm.", - "mobileSharePuzzle": "Partilhar este problema", - "mobileShareGameURL": "Partilhar o URL do jogo", + "mobileSettingsTab": "Definições", "mobileShareGamePGN": "Partilhar PGN", + "mobileShareGameURL": "Partilhar o URL do jogo", "mobileSharePositionAsFEN": "Partilhar posição como FEN", - "mobileShowVariations": "Mostrar variações", - "mobileHideVariation": "Ocultar variação", + "mobileSharePuzzle": "Partilhar este problema", "mobileShowComments": "Mostrar comentários", - "mobilePuzzleStormConfirmEndRun": "Queres terminar esta corrida?", - "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar, por favor, altera os filtros", - "mobileCancelTakebackOffer": "Cancelar pedido de voltar", - "mobileWaitingForOpponentToJoin": "À espera do adversário entrar...", - "mobileBlindfoldMode": "De olhos vendados", - "mobileLiveStreamers": "Streamers em direto", - "mobileCustomGameJoinAGame": "Entrar num jogo", - "mobileCorrespondenceClearSavedMove": "Limpar movimento salvo", + "mobileShowResult": "Mostrar resultado", + "mobileShowVariations": "Mostrar variações", "mobileSomethingWentWrong": "Algo deu errado.", + "mobileSystemColors": "Cores do sistema", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "À espera do adversário entrar...", + "mobileWatchTab": "Assistir", "activityActivity": "Atividade", "activityHostedALiveStream": "Criou uma livestream", "activityRankedInSwissTournament": "Classificado #{param1} em {param2}", @@ -48,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Fez {count} jogada} other{Fez {count} jogadas}}", "activityInNbCorrespondenceGames": "{count, plural, =1{em {count} jogo por correspondência} other{em {count} jogos por correspondência}}", "activityCompletedNbGames": "{count, plural, =1{Completou {count} jogo por correspondência} other{Completou {count} jogos por correspondência}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completou {count} jogo {param2} por correspondência} other{Completou {count} jogos {param2} por correspondência}}", "activityFollowedNbPlayers": "{count, plural, =1{Começou a seguir {count} jogador} other{Começou a seguir {count} jogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Ganhou {count} novo seguidor} other{Ganhou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Criou {count} exibição simultânea} other{Criou {count} exibições simultâneas}}", @@ -58,7 +65,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu em {count} torneio suíço} other{Competiu em {count} torneios suíços}}", "activityJoinedNbTeams": "{count, plural, =1{Entrou em {count} equipa} other{Entrou em {count} equipas}}", "broadcastBroadcasts": "Transmissões", + "broadcastMyBroadcasts": "As minhas transmissões", "broadcastLiveBroadcasts": "Transmissões do torneio em direto", + "broadcastBroadcastCalendar": "Calendário de transmissão", + "broadcastNewBroadcast": "Nova transmissão em direto", + "broadcastSubscribedBroadcasts": "Transmissões subscritas", + "broadcastAboutBroadcasts": "Sobre Transmissões", + "broadcastHowToUseLichessBroadcasts": "Como usar as Transmissões do Lichess.", + "broadcastTheNewRoundHelp": "A nova ronda terá os mesmos membros e contribuidores que a anterior.", + "broadcastAddRound": "Adicionar uma ronda", + "broadcastOngoing": "A decorrer", + "broadcastUpcoming": "Brevemente", + "broadcastCompleted": "Concluído", + "broadcastCompletedHelp": "Lichess deteta a conclusão da ronda baseada nos jogos da fonte. Use essa opção se não houver fonte.", + "broadcastRoundName": "Nome da ronda", + "broadcastRoundNumber": "Número da ronda", + "broadcastTournamentName": "Nome do torneio", + "broadcastTournamentDescription": "Breve descrição do torneio", + "broadcastFullDescription": "Descrição completa do evento", + "broadcastFullDescriptionHelp": "Descrição longa do evento opcional da transmissão. {param1} está disponível. Tem de ter menos que {param2} carácteres.", + "broadcastSourceSingleUrl": "URL da fonte PGN", + "broadcastSourceUrlHelp": "Link que o Lichess vai verificar para obter atualizações da PGN. Deve ser acessível ao público a partir da internet.", + "broadcastSourceGameIds": "Até 64 IDs de jogo Lichess, separados por espaços.", + "broadcastStartDateTimeZone": "Data de início no fuso horário local do torneio: {param}", + "broadcastStartDateHelp": "Opcional, se souberes quando começa o evento", + "broadcastCurrentGameUrl": "Link da partida atual", + "broadcastDownloadAllRounds": "Transferir todas as rondas", + "broadcastResetRound": "Reiniciar esta ronda", + "broadcastDeleteRound": "Apagar esta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente a ronda e os seus jogos.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todos os jogos desta ronda. A fonte deverá estar ativa para poder recriá-los.", + "broadcastEditRoundStudy": "Editar estudo da ronda", + "broadcastDeleteTournament": "Eliminar este torneio", + "broadcastDefinitivelyDeleteTournament": "Excluir definitivamente todo o torneio, todas as rondas e todos os jogos.", + "broadcastShowScores": "Mostra as pontuações dos jogadores com base nos resultados dos jogos", + "broadcastReplacePlayerTags": "Opcional: substituir nomes de jogadores, avaliações e títulos", + "broadcastFideFederations": "Federações FIDE", + "broadcastTop10Rating": "10 melhores classificações", + "broadcastFidePlayers": "Jogadores FIDE", + "broadcastFidePlayerNotFound": "Jogador FIDE não encontrado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federação", + "broadcastAgeThisYear": "Idade neste ano", + "broadcastUnrated": "Sem classificação", + "broadcastRecentTournaments": "Torneio recentes", + "broadcastOpenLichess": "Abrir no Lichess", + "broadcastTeams": "Equipas", + "broadcastBoards": "Tabuleiros", + "broadcastOverview": "Visão geral", + "broadcastSubscribeTitle": "Subscreva para ser notificado quando cada ronda começar. Podes ativar o sino ou as notificações push para transmissões nas preferências da tua conta.", + "broadcastUploadImage": "Carregar imagem do torneio", + "broadcastNoBoardsYet": "Ainda não há tabuleiros. Estes aparecerão assim que os jogos forem carregados.", + "broadcastBoardsCanBeLoaded": "Os tabuleiros podem ser carregados com uma fonte ou através do {param}", + "broadcastStartsAfter": "Começa após {param}", + "broadcastStartVerySoon": "A transmissão terá início muito em breve.", + "broadcastNotYetStarted": "A transmissão ainda não começou.", + "broadcastOfficialWebsite": "Website oficial", + "broadcastStandings": "Classificações", + "broadcastOfficialStandings": "Classificações oficiais", + "broadcastIframeHelp": "Mais opções na {param}", + "broadcastWebmastersPage": "página webmasters", + "broadcastPgnSourceHelp": "Uma fonte PGN pública em tempo real para esta ronda. Oferecemos também a {param} para uma sincronização mais rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incorporar esta transmissão no teu website", + "broadcastEmbedThisRound": "Incorporar {param} no teu website", + "broadcastRatingDiff": "Diferença de Elo", + "broadcastGamesThisTournament": "Jogos deste torneio", + "broadcastScore": "Pontuação", + "broadcastAllTeams": "Todas as equipas", + "broadcastTournamentFormat": "Formato do torneio", + "broadcastTournamentLocation": "Localização do Torneio", + "broadcastTopPlayers": "Melhores jogadores", + "broadcastTimezone": "Fuso horário", + "broadcastFideRatingCategory": "Categoria do Elo FIDE", + "broadcastOptionalDetails": "Detalhes opcionais", + "broadcastPastBroadcasts": "Transmissões anteriores", + "broadcastAllBroadcastsByMonth": "Ver todas as transmissões por mês", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", "challengeChallengeDeclined": "Desafio recusado", @@ -182,6 +264,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Som da notificação", + "preferencesBlindfold": "De olhos vendados", "puzzlePuzzles": "Problemas", "puzzlePuzzleThemes": "Temas de problemas", "puzzleRecommended": "Recomendado", @@ -377,8 +460,8 @@ "puzzleThemeXRayAttackDescription": "Uma peça ataque ou defende uma casa através de uma peça inimiga.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.", - "puzzleThemeHealthyMix": "Mistura saudável", - "puzzleThemeHealthyMixDescription": "Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.", + "puzzleThemeMix": "Mistura saudável", + "puzzleThemeMixDescription": "Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.", "puzzleThemePlayerGames": "Jogos de jogadores", "puzzleThemePlayerGamesDescription": "Procura problemas gerados a partir dos teus jogos ou de jogos de outro jogador.", "puzzleThemePuzzleDownloadInformation": "Esses problemas são do domínio público e podem ser obtidos em {param}.", @@ -499,7 +582,6 @@ "replayMode": "Modo de repetição", "realtimeReplay": "Tempo real", "byCPL": "Por CPL", - "openStudy": "Abrir estudo", "enable": "Ativar", "bestMoveArrow": "Seta de melhor movimento", "showVariationArrows": "Ver setas de variação", @@ -509,7 +591,6 @@ "memory": "Memória", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Remove o limite de profundidade e mantém o teu computador quente", - "engineManager": "Gestão do motor", "blunder": "Erro grave", "mistake": "Erro", "inaccuracy": "Imprecisão", @@ -535,6 +616,7 @@ "latestForumPosts": "Últimas publicações no fórum", "players": "Jogadores", "friends": "Amigos", + "otherPlayers": "outros jogadores", "discussions": "Conversas", "today": "Hoje", "yesterday": "Ontem", @@ -590,6 +672,7 @@ "rank": "Classificação", "rankX": "Posição: {param}", "gamesPlayed": "Partidas jogadas", + "ok": "OK", "cancel": "Cancelar", "whiteTimeOut": "Acabou o tempo das brancas", "blackTimeOut": "Acabou o tempo das pretas", @@ -695,7 +778,7 @@ "whiteCheckmatesInOneMove": "As brancas dão mate em um movimento", "blackCheckmatesInOneMove": "As pretas dão mate em um movimento", "retry": "Tentar novamente", - "reconnecting": "Reconectando", + "reconnecting": "A reconectar", "noNetwork": "Desligado", "favoriteOpponents": "Adversários favoritos", "follow": "Seguir", @@ -706,7 +789,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Segue-te", "xStartedFollowingY": "{param1} começou a seguir {param2}", "more": "Mais", "memberSince": "Membro desde", @@ -792,6 +874,8 @@ "descPrivateHelp": "Texto que apenas está visível para os membros da equipa. Se definido, substitui a descrição pública dos membros da equipa.", "no": "Não", "yes": "Sim", + "website": "Site", + "mobile": "Telemóvel", "help": "Ajuda:", "createANewTopic": "Criar um novo tópico", "topics": "Tópicos", @@ -810,7 +894,9 @@ "cheat": "Batota", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.", + "reportCheatBoostHelp": "Cola o(s) link(s) do(s) jogo(s) e explica o que está errado no comportamento deste utilizador. Não digas apenas “eles fazem batota”, mas diz-nos como chegaste a essa conclusão.", + "reportUsernameHelp": "Explica o que este nome de utilizador tem de ofensivo. Não digas apenas “é ofensivo/inapropriado”, mas diz-nos como chegaste a essa conclusão, especialmente se o insulto for ofuscado, não estiver em inglês, estiver em calão ou for uma referência histórica/cultural.", + "reportProcessedFasterInEnglish": "A tua denúncia será processado mais rapidamente se estiver escrito em inglês.", "error_provideOneCheatedGameLink": "Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -1003,7 +1089,7 @@ "zeroAdvertisement": "Zero anúncios", "fullFeatured": "Cheio de recursos", "phoneAndTablet": "Telemóvel e tablet", - "bulletBlitzClassical": "Bullet, blitz, clássico", + "bulletBlitzClassical": "Bullet, rápida, clássica", "correspondenceChess": "Xadrez por correspondência", "onlineAndOfflinePlay": "Jogar online e offline", "viewTheSolution": "Ver a solução", @@ -1208,6 +1294,7 @@ "showMeEverything": "Mostra-me tudo", "lichessPatronInfo": "Lichess é uma instituição de caridade e software de código aberto totalmente livre.\nTodos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários.", "nothingToSeeHere": "Nada para ver aqui no momento.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O teu adversário deixou a partida. Podes reivindicar vitória em {count} segundo.} other{O teu adversário deixou a partida. Podes reivindicar vitória em {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Xeque-mate em {count} meio-movimento} other{Xeque-mate em {count} meio-movimentos}}", "nbBlunders": "{count, plural, =1{{count} erro grave} other{{count} erros graves}}", @@ -1304,6 +1391,178 @@ "stormXRuns": "{count, plural, =1{1 partida} other{{count} tentativas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Jogou uma partida de {param2}} other{Jogou {count} partidas de {param2}}}", "streamerLichessStreamers": "Streamers no Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Os meus estudos", + "studyStudiesIContributeTo": "Estudos para os quais contribui", + "studyMyPublicStudies": "Os meus estudos públicos", + "studyMyPrivateStudies": "Os meus estudos privados", + "studyMyFavoriteStudies": "Os meus estudos favoritos", + "studyWhatAreStudies": "O que são estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos criados por {param}", + "studyNoneYet": "Nenhum ainda.", + "studyHot": "Destaques", + "studyDateAddedNewest": "Data em que foi adicionado (mais recente)", + "studyDateAddedOldest": "Data em que foi adicionado (mais antigo)", + "studyRecentlyUpdated": "Atualizado recentemente", + "studyMostPopular": "Mais popular", + "studyAlphabetical": "Ordem alfabética", + "studyAddNewChapter": "Adicionar um novo capítulo", + "studyAddMembers": "Adicionar membros", + "studyInviteToTheStudy": "Convidar para o estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convida apenas pessoas que conheças e que querem participar ativamente neste estudo.", + "studySearchByUsername": "Pesquisar por nome de utilizador", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Sair do estudo", + "studyYouAreNowAContributor": "Agora és um colaborador", + "studyYouAreNowASpectator": "Agora és um espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gostar", + "studyUnlike": "Remover gosto", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar esta posição", + "studyCommentThisMove": "Comentar este lance", + "studyAnnotateWithGlyphs": "Anotar com símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é demasiado curto para ser analisado.", + "studyOnlyContributorsCanRequestAnalysis": "Apenas os colaboradores de estudo podem solicitar uma análise de computador.", + "studyGetAFullComputerAnalysis": "Obtém uma análise completa da linha principal pelo servidor.", + "studyMakeSureTheChapterIsComplete": "Certifica-te que o capítulo está completo. Só podes solicitar a análise uma vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros do SYNC permanecem na mesma posição", + "studyShareChanges": "Partilha as alterações com espectadores e guarda-as no servidor", + "studyPlaying": "A ser jogado", + "studyShowEvalBar": "Barras de avaliação", + "studyFirst": "Primeira", + "studyPrevious": "Anterior", + "studyNext": "Seguinte", + "studyLast": "Última", "studyShareAndExport": "Partilhar & exportar", - "studyStart": "Iniciar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN do estudo", + "studyDownloadAllGames": "Transferir todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Transferir partida", + "studyStudyUrl": "URL do estudo", + "studyCurrentChapterUrl": "URL do capítulo atual", + "studyYouCanPasteThisInTheForumToEmbed": "Podes colocar isto no fórum para o incorporares", + "studyStartAtInitialPosition": "Começar na posição inicial", + "studyStartAtX": "Começar em {param}", + "studyEmbedInYourWebsite": "Incorporar no teu site ou blog", + "studyReadMoreAboutEmbedding": "Ler mais sobre incorporação", + "studyOnlyPublicStudiesCanBeEmbedded": "Só estudos públicos é que podem ser incorporados!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1}, trazido a si pelo {param2}", + "studyStudyNotFound": "Estudo não encontrado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientação", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentário de capítulo afixado", + "studySaveChapter": "Guardar capítulo", + "studyClearAnnotations": "Limpar anotações", + "studyClearVariations": "Limpar variações", + "studyDeleteChapter": "Eliminar capítulo", + "studyDeleteThisChapter": "Eliminar este capítulo? Não há volta atrás!", + "studyClearAllCommentsInThisChapter": "Apagar todos os comentários e símbolos após lances neste capítulo?", + "studyRightUnderTheBoard": "Mesmo por baixo do tabuleiro", + "studyNoPinnedComment": "Nenhum", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar os próximos movimentos", + "studyInteractiveLesson": "Lição interativa", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vazio", + "studyStartFromInitialPosition": "Começar da posição inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Iniciar de uma posição personalizada", + "studyLoadAGameByUrl": "Carregar um jogo por URL", + "studyLoadAPositionFromFen": "Carregar uma posição por FEN", + "studyLoadAGameFromPgn": "Carregar um jogo por PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "URL do jogo", + "studyLoadAGameFromXOrY": "Carregar um jogo do {param1} ou de {param2}", + "studyCreateChapter": "Criar capítulo", + "studyCreateStudy": "Criar estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Não listado", + "studyInviteOnly": "Apenas por convite", + "studyAllowCloning": "Permitir clonagem", + "studyNobody": "Ninguém", + "studyOnlyMe": "Apenas eu", + "studyContributors": "Contribuidores", + "studyMembers": "Membros", + "studyEveryone": "Toda a gente", + "studyEnableSync": "Ativar sincronização", + "studyYesKeepEveryoneOnTheSamePosition": "Sim: mantenha toda a gente na mesma posição", + "studyNoLetPeopleBrowseFreely": "Não: deixa as pessoas navegarem livremente", + "studyPinnedStudyComment": "Comentário de estudo fixo", + "studyStart": "Iniciar", + "studySave": "Guardar", + "studyClearChat": "Limpar o chat", + "studyDeleteTheStudyChatHistory": "Apagar o histórico do chat do estudo? Não há volta atrás!", + "studyDeleteStudy": "Eliminar estudo", + "studyConfirmDeleteStudy": "Eliminar todo o estudo? Não há volta atrás! Digite o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde queres estudar isso?", + "studyGoodMove": "Boa jogada", + "studyMistake": "Erro", + "studyBrilliantMove": "Jogada brilhante", + "studyBlunder": "Erro grave", + "studyInterestingMove": "Lance interessante", + "studyDubiousMove": "Lance duvidoso", + "studyOnlyMove": "Lance único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posição igual", + "studyUnclearPosition": "Posição não clara", + "studyWhiteIsSlightlyBetter": "As brancas estão ligeiramente melhor", + "studyBlackIsSlightlyBetter": "As pretas estão ligeiramente melhor", + "studyWhiteIsBetter": "As brancas estão melhor", + "studyBlackIsBetter": "As pretas estão melhor", + "studyWhiteIsWinning": "Brancas estão ganhando", + "studyBlackIsWinning": "Pretas estão ganhando", + "studyNovelty": "Novidade teórica", + "studyDevelopment": "Desenvolvimento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contra-jogo", + "studyTimeTrouble": "Pouco tempo", + "studyWithCompensation": "Com compensação", + "studyWithTheIdea": "Com a ideia", + "studyNextChapter": "Próximo capítulo", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Opções de estudo", + "studyTopics": "Tópicos", + "studyMyTopics": "Os meus tópicos", + "studyPopularTopics": "Tópicos populares", + "studyManageTopics": "Gerir tópicos", + "studyBack": "Voltar", + "studyPlayAgain": "Jogar novamente", + "studyWhatWouldYouPlay": "O que jogaria nessa situação?", + "studyYouCompletedThisLesson": "Parabéns! Completou esta lição.", + "studyPerPage": "{param} por página", + "studyNbChapters": "{count, plural, =1{{count} capítulo} other{{count} capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", + "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}", + "timeagoJustNow": "agora mesmo", + "timeagoRightNow": "agora mesmo", + "timeagoCompleted": "concluído", + "timeagoInNbSeconds": "{count, plural, =1{em {count} segundos} other{em {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{dentro de {count} minutos} other{em {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{em {count} hora} other{em {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{em {count} dia} other{em {count} dias}}", + "timeagoInNbWeeks": "{count, plural, =1{em {count} semana} other{em {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{em {count} mês} other{em {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{em {count} ano} other{em {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{há {count} minuto} other{há {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{há {count} hora} other{há {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{há {count} dia} other{há {count} dias}}", + "timeagoNbWeeksAgo": "{count, plural, =1{há {count} semana} other{há {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{há {count} mês} other{há {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{há {count} ano} other{há {count} anos}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt_BR.arb b/lib/l10n/lila_pt_BR.arb index 85cfe6863c..97c88fff21 100644 --- a/lib/l10n/lila_pt_BR.arb +++ b/lib/l10n/lila_pt_BR.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todos os jogos", + "mobileAreYouSure": "Você tem certeza?", + "mobileBlindfoldMode": "Venda", + "mobileCancelTakebackOffer": "Cancelar oferta de revanche", + "mobileClearButton": "Limpar", + "mobileCorrespondenceClearSavedMove": "Limpar movimento salvos", + "mobileCustomGameJoinAGame": "Entrar em um jogo", + "mobileFeedbackButton": "Comentários", + "mobileGreeting": "Olá, {param}", + "mobileGreetingWithoutName": "Olá", + "mobileHideVariation": "Ocultar variante forçada", "mobileHomeTab": "Início", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Ferramentas", - "mobileWatchTab": "Assistir", - "mobileSettingsTab": "Ajustes", + "mobileLiveStreamers": "Streamers do Lichess", "mobileMustBeLoggedIn": "Você precisa estar logado para ver essa pagina.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Comentários", + "mobileNoSearchResults": "Sem Resultados", + "mobileNotFollowingAnyUser": "Você não está seguindo nenhum usuário.", "mobileOkButton": "Ok", - "mobileSettingsHapticFeedback": "Vibrar ao trocar", - "mobileSettingsImmersiveMode": "Modo imerssivo", - "mobileSettingsImmersiveModeSubtitle": "Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se as telas dos jogos e desafios.", - "mobileNotFollowingAnyUser": "Você não estar seguindo nenhum usuário.", - "mobileAllGames": "Todos os jogos", - "mobileRecentSearches": "Pesquisas recentes", - "mobileClearButton": "Limpar", "mobilePlayersMatchingSearchTerm": "Usuários com \"{param}\"", - "mobileNoSearchResults": "Sem Resultados", - "mobileAreYouSure": "Você tem certeza?", - "mobilePuzzleStreakAbortWarning": "Você perderá a sua sequência atual e sua pontuação será salva.", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça segurada", + "mobilePuzzleStormConfirmEndRun": "Você quer terminar o turno?", + "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar aqui, por favor, altere os filtros", "mobilePuzzleStormNothingToShow": "Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.", - "mobileSharePuzzle": "Tentar novamente este quebra-cabeça", - "mobileShareGameURL": "Compartilhar URL do jogo", + "mobilePuzzleStormSubtitle": "Resolva quantos quebra-cabeças for possível em 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Você perderá a sua sequência atual e sua pontuação será salva.", + "mobilePuzzleThemesSubtitle": "Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Pesquisas recentes", + "mobileSettingsHapticFeedback": "Vibrar ao trocar", + "mobileSettingsImmersiveMode": "Modo imersivo", + "mobileSettingsImmersiveModeSubtitle": "Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.", + "mobileSettingsTab": "Ajustes", "mobileShareGamePGN": "Compartilhar PGN", + "mobileShareGameURL": "Compartilhar URL do jogo", "mobileSharePositionAsFEN": "Compartilhar posição como FEN", - "mobileShowVariations": "Mostrar setas da variantes", - "mobileHideVariation": "Ocultar variante forçada", + "mobileSharePuzzle": "Compartilhar este quebra-cabeça", "mobileShowComments": "Mostrar comentários", - "mobilePuzzleStormConfirmEndRun": "Você quer terminar o turno?", - "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar aqui, por favor, altere os filtros", - "mobileCancelTakebackOffer": "Cancelar oferta de revanche", - "mobileCancelDrawOffer": "Cancelar oferta de revanche", - "mobileWaitingForOpponentToJoin": "Esperando por um oponente...", - "mobileBlindfoldMode": "Venda", - "mobileLiveStreamers": "Streamers do Lichess", - "mobileCustomGameJoinAGame": "Entrar em um jogo", - "mobileCorrespondenceClearSavedMove": "Limpar movimento salvos", - "mobileSomethingWentWrong": "Houve algum problema.", "mobileShowResult": "Mostrar resultado", - "mobilePuzzleThemesSubtitle": "Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.", - "mobilePuzzleStormSubtitle": "Resolva quantos quebra-cabeças for possível em 3 minutos.", - "mobileGreeting": "Olá, {param}", - "mobileGreetingWithoutName": "Olá", + "mobileShowVariations": "Mostrar setas de variantes", + "mobileSomethingWentWrong": "Houve algum problema.", + "mobileSystemColors": "Cores do sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Ferramentas", + "mobileWaitingForOpponentToJoin": "Esperando por um oponente...", + "mobileWatchTab": "Assistir", "activityActivity": "Atividade", "activityHostedALiveStream": "Iniciou uma transmissão ao vivo", "activityRankedInSwissTournament": "Classificado #{param1} entre {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Jogou {count} movimento} other{Jogou {count} movimentos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{em {count} jogo por correspondência} other{em {count} jogos por correspondência}}", "activityCompletedNbGames": "{count, plural, =1{Completou {count} jogo por correspondência} other{Completou {count} jogos por correspondência}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completou {count} {param2} partida por correspondência} other{{count} {param2} partidas por correspondência finalizadas}}", "activityFollowedNbPlayers": "{count, plural, =1{Começou a seguir {count} jogador} other{Começou a seguir {count} jogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Ganhou {count} novo seguidor} other{Ganhou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Hospedou {count} exibição simultânea} other{Hospedou {count} exibições simultâneas}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu em {count} torneio suíço} other{Competiu em {count} torneios suíços}}", "activityJoinedNbTeams": "{count, plural, =1{Entrou na {count} equipe} other{Entrou nas {count} equipes}}", "broadcastBroadcasts": "Transmissões", + "broadcastMyBroadcasts": "Minhas transmissões", "broadcastLiveBroadcasts": "Transmissões ao vivo do torneio", + "broadcastBroadcastCalendar": "Calendário das transmissões", + "broadcastNewBroadcast": "Nova transmissão ao vivo", + "broadcastSubscribedBroadcasts": "Transmissões em que você se inscreveu", + "broadcastAboutBroadcasts": "Sobre as transmissões", + "broadcastHowToUseLichessBroadcasts": "Como usar as transmissões do Lichess.", + "broadcastTheNewRoundHelp": "A nova rodada terá os mesmos membros e colaboradores que a anterior.", + "broadcastAddRound": "Adicionar uma rodada", + "broadcastOngoing": "Em andamento", + "broadcastUpcoming": "Próximos", + "broadcastCompleted": "Concluído", + "broadcastCompletedHelp": "O Lichess detecta o fim da rodada baseado nos jogos fonte. Use essa opção se não houver fonte.", + "broadcastRoundName": "Nome da rodada", + "broadcastRoundNumber": "Número da rodada", + "broadcastTournamentName": "Nome do torneio", + "broadcastTournamentDescription": "Descrição curta do torneio", + "broadcastFullDescription": "Descrição completa do evento", + "broadcastFullDescriptionHelp": "Descrição longa e opcional da transmissão. {param1} está disponível. O tamanho deve ser menor que {param2} caracteres.", + "broadcastSourceSingleUrl": "URL de origem de PGN", + "broadcastSourceUrlHelp": "URL que Lichess irá verificar para obter atualizações PGN. Deve ser acessível ao público a partir da Internet.", + "broadcastSourceGameIds": "Até 64 IDs de partidas do Lichess, separados por espaços.", + "broadcastStartDateTimeZone": "Data de início no horário local do torneio: {param}", + "broadcastStartDateHelp": "Opcional, se você sabe quando o evento começa", + "broadcastCurrentGameUrl": "URL da partida atual", + "broadcastDownloadAllRounds": "Baixar todas as rodadas", + "broadcastResetRound": "Reiniciar esta rodada", + "broadcastDeleteRound": "Excluir esta rodada", + "broadcastDefinitivelyDeleteRound": "Deletar permanentemente todas as partidas desta rodada.", + "broadcastDeleteAllGamesOfThisRound": "Deletar todas as partidas desta rodada. A fonte deverá estar ativa para criá-las novamente.", + "broadcastEditRoundStudy": "Editar estudo da rodada", + "broadcastDeleteTournament": "Excluir este torneio", + "broadcastDefinitivelyDeleteTournament": "Excluir permanentemente todo o torneio, incluindo todas as rodadas e jogos.", + "broadcastShowScores": "Mostrar pontuações dos jogadores com base nos resultados das partidas", + "broadcastReplacePlayerTags": "Opcional: substituir nomes de jogador, ratings e títulos", + "broadcastFideFederations": "Federações FIDE", + "broadcastTop10Rating": "Classificação top 10", + "broadcastFidePlayers": "Jogadores FIDE", + "broadcastFidePlayerNotFound": "Jogador não encontrando na FIDE", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federação", + "broadcastAgeThisYear": "Idade atual", + "broadcastUnrated": "Sem rating", + "broadcastRecentTournaments": "Torneios recentes", + "broadcastOpenLichess": "Abrir no Lichess", + "broadcastTeams": "Equipes", + "broadcastBoards": "Tabuleiros", + "broadcastOverview": "Visão geral", + "broadcastSubscribeTitle": "Inscreva-se para ser notificado no início de cada rodada. Você pode configurar as notificações de transmissões nas suas preferências.", + "broadcastUploadImage": "Enviar imagem de torneio", + "broadcastNoBoardsYet": "Sem tabuleiros ainda. Eles vão aparecer quando os jogos forem enviados.", + "broadcastBoardsCanBeLoaded": "Tabuleiros são carregados com uma fonte ou pelo {param}", + "broadcastStartsAfter": "Começa após {param}", + "broadcastStartVerySoon": "A transmissão começará em breve.", + "broadcastNotYetStarted": "A transmissão ainda não começou.", + "broadcastOfficialWebsite": "Site oficial", + "broadcastStandings": "Classificação", + "broadcastOfficialStandings": "Classificação oficial", + "broadcastIframeHelp": "Mais opções na {param}", + "broadcastWebmastersPage": "página dos webmasters", + "broadcastPgnSourceHelp": "Uma fonte PGN pública ao vivo desta rodada. Há também a {param} para uma sincronização mais rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incorporar essa transmissão em seu site", + "broadcastEmbedThisRound": "Incorporar {param} em seu site", + "broadcastRatingDiff": "Diferência de pontos", + "broadcastGamesThisTournament": "Jogos neste torneio", + "broadcastScore": "Pontuação", + "broadcastAllTeams": "Todas as equipes", + "broadcastTournamentFormat": "Formato do torneio", + "broadcastTournamentLocation": "Local do torneio", + "broadcastTopPlayers": "Melhores jogadores", + "broadcastTimezone": "Fuso horário", + "broadcastFideRatingCategory": "Categoria de rating FIDE", + "broadcastOptionalDetails": "Detalhes opcionais", + "broadcastPastBroadcasts": "Transmissões passadas", + "broadcastAllBroadcastsByMonth": "Ver todas as transmissões por mês", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", "challengeChallengeDeclined": "Desafio recusado", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Som da notificação", + "preferencesBlindfold": "Às cegas", "puzzlePuzzles": "Quebra-cabeças", "puzzlePuzzleThemes": "Temas de quebra-cabeça", "puzzleRecommended": "Recomendado", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.", - "puzzleThemeHealthyMix": "Combinação saudável", - "puzzleThemeHealthyMixDescription": "Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.", + "puzzleThemeMix": "Combinação saudável", + "puzzleThemeMixDescription": "Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.", "puzzleThemePlayerGames": "Partidas de jogadores", "puzzleThemePlayerGamesDescription": "Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.", "puzzleThemePuzzleDownloadInformation": "Esses quebra-cabeças estão em domínio público, e você pode baixá-los em {param}.", @@ -402,7 +480,7 @@ "toInviteSomeoneToPlayGiveThisUrl": "Para convidar alguém para jogar, envie este URL", "gameOver": "Fim da partida", "waitingForOpponent": "Aguardando oponente", - "orLetYourOpponentScanQrCode": "Ou deixe seu oponente ler este QR Code", + "orLetYourOpponentScanQrCode": "Ou deixe seu oponente ler este código QR", "waiting": "Aguardando", "yourTurn": "Sua vez", "aiNameLevelAiLevel": "{param1} nível {param2}", @@ -443,8 +521,8 @@ "blackResigned": "Pretas desistiram", "whiteLeftTheGame": "Brancas deixaram a partida", "blackLeftTheGame": "Pretas deixaram a partida", - "whiteDidntMove": "As brancas não se moveram", - "blackDidntMove": "As pretas não se moveram", + "whiteDidntMove": "Brancas não moveram", + "blackDidntMove": "Pretas não moveram", "requestAComputerAnalysis": "Solicitar uma análise do computador", "computerAnalysis": "Análise do computador", "computerAnalysisAvailable": "Análise de computador disponível", @@ -463,8 +541,8 @@ "promoteVariation": "Promover variante", "makeMainLine": "Transformar em linha principal", "deleteFromHere": "Excluir a partir daqui", - "collapseVariations": "Esconder variantes", - "expandVariations": "Mostrar variantes", + "collapseVariations": "Recolher variações", + "expandVariations": "Expandir variações", "forceVariation": "Variante forçada", "copyVariationPgn": "Copiar PGN da variante", "move": "Movimentos", @@ -484,7 +562,7 @@ "recentGames": "Partidas recentes", "topGames": "Melhores partidas", "masterDbExplanation": "Duas milhões de partidas de jogadores com pontuação FIDE acima de {param1}, desde {param2} a {param3}", - "dtzWithRounding": "DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão", + "dtzWithRounding": "DTZ50\" com arredondamento, baseado no número de lances até a próxima captura ou movimento de peão", "noGameFound": "Nenhuma partida encontrada", "maxDepthReached": "Profundidade máxima alcançada!", "maybeIncludeMoreGamesFromThePreferencesMenu": "Talvez você queira incluir mais jogos a partir do menu de preferências", @@ -505,7 +583,6 @@ "replayMode": "Rever a partida", "realtimeReplay": "Tempo Real", "byCPL": "Por erros", - "openStudy": "Abrir estudo", "enable": "Ativar", "bestMoveArrow": "Seta de melhor movimento", "showVariationArrows": "Mostrar setas das variantes", @@ -515,7 +592,6 @@ "memory": "Memória", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Remove o limite de profundidade, o que aquece seu computador", - "engineManager": "Gerenciador de engine", "blunder": "Capivarada", "mistake": "Erro", "inaccuracy": "Imprecisão", @@ -597,6 +673,7 @@ "rank": "Rank", "rankX": "Classificação: {param}", "gamesPlayed": "Partidas realizadas", + "ok": "OK", "cancel": "Cancelar", "whiteTimeOut": "Tempo das brancas esgotado", "blackTimeOut": "Tempo das pretas esgotado", @@ -690,7 +767,7 @@ "orUploadPgnFile": "Ou carregue um arquivo PGN", "fromPosition": "A partir da posição", "continueFromHere": "Continuar daqui", - "toStudy": "Estudo", + "toStudy": "Estudar", "importGame": "Importar partida", "importGameExplanation": "Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.", "importGameCaveat": "As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.", @@ -713,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Segue você", "xStartedFollowingY": "{param1} começou a seguir {param2}", "more": "Mais", "memberSince": "Membro desde", @@ -755,7 +831,7 @@ "blackWins": "Pretas venceram", "drawRate": "Taxa de empates", "draws": "Empates", - "nextXTournament": "Próximo torneio {param}:", + "nextXTournament": "Próximo torneio de {param}:", "averageOpponent": "Pontuação média adversários", "boardEditor": "Editor de tabuleiro", "setTheBoard": "Defina a posição", @@ -819,7 +895,9 @@ "cheat": "Trapaça", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.", + "reportCheatBoostHelp": "Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão.", + "reportUsernameHelp": "Explique porque este nome de usuário é ofensivo. Não diga apenas \"é ofensivo/inapropriado\", mas nos diga como chegou a essa conclusão especialmente se o insulto for ofuscado, não estiver em inglês, for uma gíria, ou for uma referência histórica/cultural.", + "reportProcessedFasterInEnglish": "A sua denúncia será processada mais rápido se for escrita em inglês.", "error_provideOneCheatedGameLink": "Por favor forneça ao menos um link para um jogo com suspeita de trapaça.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -838,7 +916,7 @@ "newPasswordAgain": "Nova senha (novamente)", "newPasswordsDontMatch": "As novas senhas não correspondem", "newPasswordStrength": "Senha forte", - "clockInitialTime": "Tempo de relógio", + "clockInitialTime": "Tempo inicial no relógio", "clockIncrement": "Incremento do relógio", "privacy": "Privacidade", "privacyPolicy": "Política de privacidade", @@ -1217,6 +1295,7 @@ "showMeEverything": "Mostrar tudo", "lichessPatronInfo": "Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.", "nothingToSeeHere": "Nada para ver aqui no momento.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O seu adversário deixou a partida. Você pode reivindicar vitória em {count} segundo.} other{O seu adversário deixou a partida. Você pode reivindicar vitória em {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate em {count} lance} other{Mate em {count} lances}}", "nbBlunders": "{count, plural, =1{{count} capivarada} other{{count} capivaradas}}", @@ -1224,12 +1303,12 @@ "nbInaccuracies": "{count, plural, =1{{count} imprecisão} other{{count} imprecisões}}", "nbPlayers": "{count, plural, =1{{count} jogadores conectados} other{{count} jogadores conectados}}", "nbGames": "{count, plural, =1{{count} partida} other{{count} partidas}}", - "ratingXOverYGames": "{count, plural, =1{Rating {count} em {param2} jogo} other{Rating {count} após {param2} partidas}}", + "ratingXOverYGames": "{count, plural, =1{Rating {count} após {param2} partida} other{Rating {count} após {param2} partidas}}", "nbBookmarks": "{count, plural, =1{{count} Favoritos} other{{count} Favoritos}}", "nbDays": "{count, plural, =1{{count} dias} other{{count} dias}}", "nbHours": "{count, plural, =1{{count} horas} other{{count} horas}}", "nbMinutes": "{count, plural, =1{{count} minuto} other{{count} minutos}}", - "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{O ranking é atualizado a cada {count} minutos} other{O ranking é atualizado a cada {count} minutos}}", + "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{O ranking é atualizado a cada {count} minuto} other{O ranking é atualizado a cada {count} minutos}}", "nbPuzzles": "{count, plural, =1{{count} quebra-cabeça} other{{count} problemas}}", "nbGamesWithYou": "{count, plural, =1{{count} partidas contra você} other{{count} partidas contra você}}", "nbRated": "{count, plural, =1{{count} valendo pontos} other{{count} valendo pontos}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 tentativa} other{{count} séries}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Jogou uma tentativa de {param2}} other{Jogou {count} tentativas de {param2}}}", "streamerLichessStreamers": "Streamers do Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Meus estudos", + "studyStudiesIContributeTo": "Estudos para os quais contribuí", + "studyMyPublicStudies": "Meus estudos públicos", + "studyMyPrivateStudies": "Meus estudos privados", + "studyMyFavoriteStudies": "Meus estudos favoritos", + "studyWhatAreStudies": "O que são estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos criados por {param}", + "studyNoneYet": "Nenhum ainda.", + "studyHot": "Em alta", + "studyDateAddedNewest": "Data de criação (mais recente)", + "studyDateAddedOldest": "Data de criação (mais antiga)", + "studyRecentlyUpdated": "Atualizado recentemente", + "studyMostPopular": "Mais populares", + "studyAlphabetical": "Em ordem alfabética", + "studyAddNewChapter": "Adicionar um novo capítulo", + "studyAddMembers": "Adicionar membros", + "studyInviteToTheStudy": "Convidar para o estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convide apenas pessoas que você conhece e que queiram participar efetivamente deste estudo.", + "studySearchByUsername": "Pesquisar por nome de usuário", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Sair deste estudo", + "studyYouAreNowAContributor": "Agora você é um(a) colaborador(a)", + "studyYouAreNowASpectator": "Você agora é um(a) espectador(a)", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gostei", + "studyUnlike": "Não gostei", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comente sobre esta posição", + "studyCommentThisMove": "Comente sobre este lance", + "studyAnnotateWithGlyphs": "Anotar com símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é muito curto para ser analisado.", + "studyOnlyContributorsCanRequestAnalysis": "Apenas os colaboradores de um estudo podem solicitar uma análise de computador.", + "studyGetAFullComputerAnalysis": "Obter uma análise completa dos movimentos pelo servidor.", + "studyMakeSureTheChapterIsComplete": "Certifique-se de que o capítulo está completo. Você só pode solicitar análise uma vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros em SYNC veem a mesma posição", + "studyShareChanges": "Compartilhar as alterações com os espectadores e salvá-las no servidor", + "studyPlaying": "Jogando", + "studyShowEvalBar": "Barras de avaliação", + "studyFirst": "Primeiro", + "studyPrevious": "Anterior", + "studyNext": "Próximo", + "studyLast": "Último", "studyShareAndExport": "Compartilhar & exportar", - "studyStart": "Iniciar" + "studyCloneStudy": "Duplicar", + "studyStudyPgn": "PGN de estudo", + "studyDownloadAllGames": "Baixar todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Baixar partida", + "studyStudyUrl": "URL de estudo", + "studyCurrentChapterUrl": "URL do capítulo atual", + "studyYouCanPasteThisInTheForumToEmbed": "Você pode colar isso no fórum para incluir o estudo na publicação", + "studyStartAtInitialPosition": "Começar na posição inicial", + "studyStartAtX": "Começar em {param}", + "studyEmbedInYourWebsite": "Incorporar em seu site ou blog", + "studyReadMoreAboutEmbedding": "Leia mais sobre como incorporar", + "studyOnlyPublicStudiesCanBeEmbedded": "Apenas os estudos públicos podem ser incorporados!", + "studyOpen": "Abertura", + "studyXBroughtToYouByY": "{param1}, disponibilizado por {param2}", + "studyStudyNotFound": "Estudo não encontrado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientação", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentário de capítulo afixado", + "studySaveChapter": "Salvar capítulo", + "studyClearAnnotations": "Remover anotações", + "studyClearVariations": "Limpar variantes", + "studyDeleteChapter": "Excluir capítulo", + "studyDeleteThisChapter": "Excluir este capítulo? Não há volta!", + "studyClearAllCommentsInThisChapter": "Remover todos os comentários e formas deste capítulo?", + "studyRightUnderTheBoard": "Logo abaixo do tabuleiro", + "studyNoPinnedComment": "Nenhum", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar próximos movimentos", + "studyInteractiveLesson": "Lição interativa", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vazio", + "studyStartFromInitialPosition": "Reiniciar para posição inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Iniciar com posição personalizada", + "studyLoadAGameByUrl": "Carregar um jogo por URL", + "studyLoadAPositionFromFen": "Carregar uma posição com FEN", + "studyLoadAGameFromPgn": "Carregar um jogo com PGN", + "studyAutomatic": "Automático", + "studyUrlOfTheGame": "URL do jogo", + "studyLoadAGameFromXOrY": "Carregar um jogo de {param1} ou {param2}", + "studyCreateChapter": "Criar capítulo", + "studyCreateStudy": "Criar estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Não listado", + "studyInviteOnly": "Apenas por convite", + "studyAllowCloning": "Permitir clonagem", + "studyNobody": "Ninguém", + "studyOnlyMe": "Apenas eu", + "studyContributors": "Colaboradores", + "studyMembers": "Membros", + "studyEveryone": "Todos", + "studyEnableSync": "Ativar sincronização", + "studyYesKeepEveryoneOnTheSamePosition": "Sim: mantenha todos na mesma posição", + "studyNoLetPeopleBrowseFreely": "Não: deixe as pessoas navegarem livremente", + "studyPinnedStudyComment": "Comentário de estudo afixado", + "studyStart": "Iniciar", + "studySave": "Salvar", + "studyClearChat": "Limpar conversação", + "studyDeleteTheStudyChatHistory": "Excluir o histórico de conversação do estudo? Não há volta!", + "studyDeleteStudy": "Excluir estudo", + "studyConfirmDeleteStudy": "Excluir todo o estudo? Não há volta! Digite o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde você quer estudar?", + "studyGoodMove": "Boa jogada", + "studyMistake": "Erro", + "studyBrilliantMove": "Jogada excelente", + "studyBlunder": "Erro grave", + "studyInterestingMove": "Jogada interessante", + "studyDubiousMove": "Lance questionável", + "studyOnlyMove": "Única jogada", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posição igual", + "studyUnclearPosition": "Posição incerta", + "studyWhiteIsSlightlyBetter": "Brancas estão um pouco melhor", + "studyBlackIsSlightlyBetter": "Pretas estão um pouco melhor", + "studyWhiteIsBetter": "Brancas estão melhor", + "studyBlackIsBetter": "Pretas estão melhor", + "studyWhiteIsWinning": "Brancas estão ganhando", + "studyBlackIsWinning": "Pretas estão ganhando", + "studyNovelty": "Novidade", + "studyDevelopment": "Desenvolvimento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contra-ataque", + "studyTimeTrouble": "Problema de tempo", + "studyWithCompensation": "Com compensação", + "studyWithTheIdea": "Com a ideia", + "studyNextChapter": "Próximo capítulo", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Opções de estudo", + "studyTopics": "Tópicos", + "studyMyTopics": "Meus tópicos", + "studyPopularTopics": "Tópicos populares", + "studyManageTopics": "Gerenciar tópicos", + "studyBack": "Voltar", + "studyPlayAgain": "Jogar novamente", + "studyWhatWouldYouPlay": "O que você jogaria nessa posição?", + "studyYouCompletedThisLesson": "Parabéns! Você completou essa lição.", + "studyPerPage": "{param} por página", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}", + "timeagoJustNow": "agora há pouco", + "timeagoRightNow": "agora mesmo", + "timeagoCompleted": "concluído", + "timeagoInNbSeconds": "{count, plural, =1{em {count} segundo} other{em {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{em {count} minuto} other{em {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{em {count} hora} other{em {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{em {count} dia} other{em {count} dias}}", + "timeagoInNbWeeks": "{count, plural, =1{em {count} semana} other{em {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{em {count} mês} other{em {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{em {count} ano} other{em {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuto atrás} other{{count} minutos atrás}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} hora atrás} other{{count} horas atrás}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dia atrás} other{{count} dias atrás}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} semana atrás} other{{count} semanas atrás}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} mês atrás} other{{count} meses atrás}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} ano atrás} other{{count} anos atrás}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ro.arb b/lib/l10n/lila_ro.arb index 06e6ade905..014dba0315 100644 --- a/lib/l10n/lila_ro.arb +++ b/lib/l10n/lila_ro.arb @@ -1,15 +1,48 @@ { + "mobileAllGames": "Toate jocurile", + "mobileAreYouSure": "Ești sigur?", + "mobileBlindfoldMode": "Legat la ochi", + "mobileCancelTakebackOffer": "Anulați propunerea de revanșă", + "mobileClearButton": "Resetare", + "mobileCorrespondenceClearSavedMove": "Șterge mutarea salvată", + "mobileCustomGameJoinAGame": "Alătură-te unui joc", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Salut, {param}", + "mobileGreetingWithoutName": "Salut", + "mobileHideVariation": "Ascunde variațiile", "mobileHomeTab": "Acasă", - "mobileSettingsTab": "Setări", + "mobileLiveStreamers": "Fluxuri live", "mobileMustBeLoggedIn": "Trebuie să te autentifici pentru a accesa această pagină.", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Niciun rezultat", + "mobileNotFollowingAnyUser": "Nu urmărești niciun utilizator.", "mobileOkButton": "OK", - "mobileAllGames": "Toate jocurile", + "mobilePlayersMatchingSearchTerm": "Jucători cu \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Mărește piesa trasă", + "mobilePuzzleStormConfirmEndRun": "Vrei să termini acest run?", + "mobilePuzzleStormFilterNothingToShow": "Nimic de afișat, vă rugăm să schimbați filtrele", + "mobilePuzzleStormNothingToShow": "Nimic de arătat. Jucați câteva partide de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Rezolvă cât mai multe puzzle-uri în 3 minute.", + "mobilePuzzleStreakAbortWarning": "Îți vei pierde streak-ul actual iar scorul va fi salvat.", + "mobilePuzzleThemesSubtitle": "Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.", + "mobilePuzzlesTab": "Puzzle-uri", + "mobileRecentSearches": "Căutări recente", + "mobileSettingsHapticFeedback": "Control tactil", + "mobileSettingsImmersiveMode": "Mod imersiv", + "mobileSettingsImmersiveModeSubtitle": "Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.", + "mobileSettingsTab": "Setări", + "mobileShareGamePGN": "Distribuie PGN", + "mobileShareGameURL": "Distribuie URL-ul jocului", + "mobileSharePositionAsFEN": "Distribuie poziția ca FEN", + "mobileSharePuzzle": "Distribuie acest puzzle", "mobileShowComments": "Afişează сomentarii", "mobileShowResult": "Arată rezultatul", - "mobilePuzzleStormSubtitle": "Rezolvă cât mai multe puzzle-uri în 3 minute.", - "mobileGreeting": "Salut, {param}", - "mobileGreetingWithoutName": "Salut", + "mobileShowVariations": "Arată variațiile", + "mobileSomethingWentWrong": "Ceva nu a mers bine. :(", + "mobileSystemColors": "Culori sistem", + "mobileTheme": "Tema", + "mobileToolsTab": "Unelte", + "mobileWaitingForOpponentToJoin": "În așteptarea unui jucător...", + "mobileWatchTab": "Vizionează", "activityActivity": "Activitate", "activityHostedALiveStream": "A găzduit un live stream", "activityRankedInSwissTournament": "Evaluat #{param1} în {param2}", @@ -22,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{A jucat {count} mutare} few{A jucat {count} mutări} other{A jucat {count} mutări}}", "activityInNbCorrespondenceGames": "{count, plural, =1{în {count} joc de corespondență} few{în {count} jocuri de corespondență} other{în {count} jocuri de corespondență}}", "activityCompletedNbGames": "{count, plural, =1{A completat {count} meci de corespondență} few{A completat {count} meciuri de corespondență} other{A completat {count} meciuri de corespondență}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Terminat {count} joc de tip {param2} de corespondență} few{Terminat {count} jocuri de tip {param2} de corespondență} other{Terminat {count} jocuri de tip {param2} de corespondență}}", "activityFollowedNbPlayers": "{count, plural, =1{A început să urmărească {count} jucător} few{A început să urmărească {count} jucători} other{A început să urmărească {count} jucători}}", "activityGainedNbFollowers": "{count, plural, =1{A obținut {count} urmăritor nou} few{A obținut {count} urmăritori noi} other{A obținut {count} urmăritori noi}}", "activityHostedNbSimuls": "{count, plural, =1{A găzduit {count} simultană de șah} few{A găzduit {count} simultane de șah} other{A găzduit {count} simultane de șah}}", @@ -32,7 +66,58 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{A concurat în {count} turneu elvețian} few{A concurat în {count} turnee elvețiene} other{A concurat în {count} turnee elvețiene}}", "activityJoinedNbTeams": "{count, plural, =1{S-a alăturat la {count} echipă} few{S-a alăturat la {count} echipe} other{S-a alăturat la {count} echipe}}", "broadcastBroadcasts": "Transmisiuni", + "broadcastMyBroadcasts": "Transmisiile mele", "broadcastLiveBroadcasts": "Difuzări de turnee în direct", + "broadcastNewBroadcast": "O nouă difuzare în direct", + "broadcastSubscribedBroadcasts": "Transmisii abonate", + "broadcastAboutBroadcasts": "Despre emisiuni", + "broadcastHowToUseLichessBroadcasts": "Cum să utilizați emisiunile Lichess.", + "broadcastTheNewRoundHelp": "Runda noua va avea aceiași membri și contribuitori ca cea anterioară.", + "broadcastAddRound": "Adaugă o rundă", + "broadcastOngoing": "În desfășurare", + "broadcastUpcoming": "Următoare", + "broadcastCompleted": "Terminate", + "broadcastCompletedHelp": "Lichess detectează finalizarea rundei pe baza jocurilor sursă. Utilizați această comutare dacă nu există nicio sursă.", + "broadcastRoundName": "Numele rundei", + "broadcastRoundNumber": "Număr rotund", + "broadcastTournamentName": "Numele turneului", + "broadcastTournamentDescription": "O descriere scurtă a turneului", + "broadcastFullDescription": "Întreaga descriere a evenimentului", + "broadcastFullDescriptionHelp": "Descriere lungă, opțională, a difuzării. {param1} este disponibil. Lungimea trebuie să fie mai mică decât {param2} caractere.", + "broadcastSourceSingleUrl": "URL sursă PGN", + "broadcastSourceUrlHelp": "URL-ul pe care Lichess îl va verifica pentru a obține actualizări al PGN-ului. Trebuie să fie public accesibil pe Internet.", + "broadcastSourceGameIds": "Până la 64 de ID-uri de joc Lichess, separate prin spații.", + "broadcastStartDateHelp": "Opțional, dacă știi când va începe evenimentul", + "broadcastCurrentGameUrl": "URL-ul partidei curente", + "broadcastDownloadAllRounds": "Descarcă toate rundele", + "broadcastResetRound": "Resetează această rundă", + "broadcastDeleteRound": "Șterge această rundă", + "broadcastDefinitivelyDeleteRound": "Șterge definitiv runda și jocurile sale.", + "broadcastDeleteAllGamesOfThisRound": "Șterge toate jocurile din această rundă. Sursa va trebui să fie activă pentru a le recrea.", + "broadcastEditRoundStudy": "Editare rundă de studiu", + "broadcastDeleteTournament": "Șterge acest turneu", + "broadcastDefinitivelyDeleteTournament": "Sigur doresc să ștergeți întregul turneu, toate rundele și toate jocurile sale.", + "broadcastShowScores": "Arată scorurile jucătorilor pe baza rezultatelor jocului", + "broadcastReplacePlayerTags": "Opțional: înlocuiește numele jucătorilor, ratingurile și titlurile", + "broadcastFideFederations": "Federații FIDE", + "broadcastTop10Rating": "Top 10 evaluări", + "broadcastFidePlayers": "Jucători FIDE", + "broadcastFidePlayerNotFound": "Jucătorul FIDE nu a fost găsit", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federație", + "broadcastAgeThisYear": "Vârsta în acest an", + "broadcastUnrated": "Fără rating", + "broadcastRecentTournaments": "Turnee recente", + "broadcastOpenLichess": "Deschide în Lichess", + "broadcastTeams": "Echipe", + "broadcastStandings": "Clasament", + "broadcastOfficialStandings": "Clasament oficial", + "broadcastScore": "Scor", + "broadcastAllTeams": "Toate echipele", + "broadcastTournamentFormat": "Format turneu", + "broadcastTournamentLocation": "Locație turneu", + "broadcastTimezone": "Fus orar", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisiune} few{{count} transmisiuni} other{{count} de transmisiuni}}", "challengeChallengesX": "Provocări: {param1}", "challengeChallengeToPlay": "Provoacă la o partidă", "challengeChallengeDeclined": "Provocare refuzată", @@ -156,6 +241,7 @@ "preferencesNotifyWeb": "Navigator", "preferencesNotifyDevice": "Dispozitiv", "preferencesBellNotificationSound": "Sunet de notificare", + "preferencesBlindfold": "Legat la ochi", "puzzlePuzzles": "Probleme de șah", "puzzlePuzzleThemes": "Teme pentru problemele de șah", "puzzleRecommended": "Recomandare", @@ -351,8 +437,8 @@ "puzzleThemeXRayAttackDescription": "O piesă atacă sau apară un patrat, printr-o piesă inamică.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.", - "puzzleThemeHealthyMix": "Amestec sănătos", - "puzzleThemeHealthyMixDescription": "Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.", + "puzzleThemeMix": "Mixt", + "puzzleThemeMixDescription": "Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.", "puzzleThemePlayerGames": "Partide jucători", "puzzleThemePlayerGamesDescription": "Caută puzzle-uri generate din partidele tale sau din partidele unui alt jucător.", "puzzleThemePuzzleDownloadInformation": "Aceste puzzle-uri sunt în domeniul public și pot fi descărcate de la {param}.", @@ -431,6 +517,8 @@ "promoteVariation": "Promovează variația", "makeMainLine": "Fă-o variația principală", "deleteFromHere": "Șterge de aici", + "collapseVariations": "Restrânge variațiile", + "expandVariations": "Extinde variațiile", "forceVariation": "Forțează variația", "copyVariationPgn": "Copiază varianta PGN", "move": "Mutare", @@ -471,7 +559,6 @@ "replayMode": "Modul de reluare", "realtimeReplay": "În timp real", "byCPL": "După CPL", - "openStudy": "Studiu deschis", "enable": "Activează", "bestMoveArrow": "Săgeată pentru cea mai bună mutare", "showVariationArrows": "Afișează săgețile variației", @@ -481,7 +568,6 @@ "memory": "Memorie", "infiniteAnalysis": "Analiză infinită", "removesTheDepthLimit": "Elimină limita de adâncime (și încălzește computerul)", - "engineManager": "Manager de motor", "blunder": "Gafă", "mistake": "Greșeală", "inaccuracy": "Inexactitate", @@ -563,6 +649,7 @@ "rank": "Clasificare", "rankX": "Loc în clasament: {param}", "gamesPlayed": "Partide jucate", + "ok": "OK", "cancel": "Anulare", "whiteTimeOut": "Timpul pentru alb a expirat", "blackTimeOut": "Timpul pentru negru a expirat", @@ -679,7 +766,6 @@ "block": "Blocare", "blocked": "Blocat", "unblock": "Deblocare", - "followsYou": "Vă urmărește", "xStartedFollowingY": "{param1} a început să vă urmărească {param2}", "more": "Mai mult", "memberSince": "Membru din", @@ -739,7 +825,9 @@ "profile": "Profil", "editProfile": "Editează profilul", "realName": "Nume real", + "setFlair": "Arată-ți stilul", "flair": "Pictograma personalizată", + "youCanHideFlair": "Există o setare pentru a ascunde flair-ul utilizatorilor pe întregului site.", "biography": "Biografie", "countryRegion": "Țara sau regiunea", "thankYou": "Mulţumesc!", @@ -756,12 +844,15 @@ "automaticallyProceedToNextGameAfterMoving": "Du-te automat la partida următoare după mutare", "autoSwitch": "Schimbă automat", "puzzles": "Probleme de șah", + "onlineBots": "Boți online", "name": "Nume", "description": "Descriere", "descPrivate": "Descriere privată", "descPrivateHelp": "Text pe care îl vor vedea doar membrii echipei. Dacă este setat, înlocuiește descrierea publică pentru membrii echipei.", "no": "Nu", "yes": "Da", + "website": "Pagină web", + "mobile": "Mobil", "help": "Ajutor:", "createANewTopic": "Creează un nou topic", "topics": "Subiecte", @@ -780,7 +871,9 @@ "cheat": "Trișează", "troll": "Troll", "other": "Altceva", - "reportDescriptionHelp": "Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar ”trișează”, ci spune-ne cum ai ajuns la această concluzie. Raportul tău va fi procesat mai rapid dacă este scris în engleză.", + "reportCheatBoostHelp": "Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar \"trișează\", ci spune-ne cum ai ajuns la această concluzie.", + "reportUsernameHelp": "Explică de ce acest nume de utilizator este jignitor. Nu spune doar \"e ofensiv/inadecvat\", ci spune-ne cum ai ajuns la această concluzie, mai ales în cazul în care insulta este obscură, nu este în engleză, este jargon sau este o referință istorică/culturală.", + "reportProcessedFasterInEnglish": "Raportul tău va fi procesat mai rapid dacă este scris în engleză.", "error_provideOneCheatedGameLink": "Te rugăm să furnizezi cel puțin un link către un joc în care s-a trișat.", "by": "de {param}", "importedByX": "Importat de {param}", @@ -813,6 +906,7 @@ "slow": "Lentă", "insideTheBoard": "Pe interiorul tablei", "outsideTheBoard": "În afara tablei", + "allSquaresOfTheBoard": "Toate pătratele de pe tablă", "onSlowGames": "În jocurile lente", "always": "Întotdeauna", "never": "Niciodată", @@ -913,7 +1007,9 @@ "keyPreviousBranch": "Ramura precedentă", "keyNextBranch": "Ramura următoare", "toggleVariationArrows": "Comută săgețile de variație", + "cyclePreviousOrNextVariation": "Ciclu de variație precedentă/următoare", "toggleGlyphAnnotations": "Comută adnotările gilfelor", + "togglePositionAnnotations": "Activează/Dezactivează adnotările pozițiilor", "variationArrowsInfo": "Săgețile de variație vă permit să navigați fără a utiliza lista de mutare.", "playSelectedMove": "joacă mutarea selectată", "newTournament": "Turneu nou", @@ -1174,6 +1270,8 @@ "instructions": "Instrucțiuni", "showMeEverything": "Afișează-mi tot", "lichessPatronInfo": "Lichess este o asociație non-profit și un software gratuit și open-source.\nToate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor.", + "nothingToSeeHere": "Nimic de văzut aici momentan.", + "stats": "Statistici", "opponentLeftCounter": "{count, plural, =1{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secundă.} few{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secunde.} other{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secunde.}}", "mateInXHalfMoves": "{count, plural, =1{Mat la prima mutare} few{Mat în {count} mutări} other{Mat în {count} mutări}}", "nbBlunders": "{count, plural, =1{{count} gafă} few{{count} gafe} other{{count} de gafe}}", @@ -1270,6 +1368,178 @@ "stormXRuns": "{count, plural, =1{O încercare} few{{count} încercări} other{{count} încercări}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{A jucat o încercare de {param2}} few{A jucat {count} încercări de {param2}} other{A jucat {count} încercări de {param2}}}", "streamerLichessStreamers": "Lichess streameri", + "studyPrivate": "Privat", + "studyMyStudies": "Studiile mele", + "studyStudiesIContributeTo": "Studiile la care contribui", + "studyMyPublicStudies": "Studiile mele publice", + "studyMyPrivateStudies": "Studiile mele private", + "studyMyFavoriteStudies": "Studiile mele preferate", + "studyWhatAreStudies": "Ce sunt studiile?", + "studyAllStudies": "Toate studiile", + "studyStudiesCreatedByX": "Studii create de {param}", + "studyNoneYet": "Niciunul încă.", + "studyHot": "Populare", + "studyDateAddedNewest": "Data adăugată (cele mai noi)", + "studyDateAddedOldest": "Data adăugată (cele mai vechi)", + "studyRecentlyUpdated": "Încărcate recent", + "studyMostPopular": "Cele mai populare", + "studyAlphabetical": "Alfabetic", + "studyAddNewChapter": "Adaugă un nou capitol", + "studyAddMembers": "Adaugă membri", + "studyInviteToTheStudy": "Invită la studiu", + "studyPleaseOnlyInvitePeopleYouKnow": "Vă rugăm să invitați doar persoanele pe care le cunoașteți și care vor în mod activ să se alăture studiului.", + "studySearchByUsername": "Caută după numele de utilizator", + "studySpectator": "Spectator", + "studyContributor": "Contribuitor", + "studyKick": "Înlătură", + "studyLeaveTheStudy": "Părăsește studiul", + "studyYouAreNowAContributor": "Acum ești un contribuitor", + "studyYouAreNowASpectator": "Acum ești un spectator", + "studyPgnTags": "Etichete PGN", + "studyLike": "Apreciază", + "studyUnlike": "Nu îmi mai place", + "studyNewTag": "Etichetă nouă", + "studyCommentThisPosition": "Comentează această poziție", + "studyCommentThisMove": "Comentează această mutare", + "studyAnnotateWithGlyphs": "Adnotează cu simboluri", + "studyTheChapterIsTooShortToBeAnalysed": "Capitolul este prea mic pentru a fi analizat.", + "studyOnlyContributorsCanRequestAnalysis": "Numai contribuitorii studiului pot solicita o analiză a computerului.", + "studyGetAFullComputerAnalysis": "Obțineți o întreagă analiză server-side a computerului a variației principale.", + "studyMakeSureTheChapterIsComplete": "Asigurați-vă că acest capitol este complet. Puteți solicita o analiză doar o singură dată.", + "studyAllSyncMembersRemainOnTheSamePosition": "Toți membri sincronizați rămân la aceeași poziție", + "studyShareChanges": "Împărtășește modificările cu spectatorii și salvează-le pe server", + "studyPlaying": "În desfășurare", + "studyShowEvalBar": "Bară de evaluare", + "studyFirst": "Prima", + "studyPrevious": "Precedentă", + "studyNext": "Următoarea", + "studyLast": "Ultima", "studyShareAndExport": "Împărtășește & exportă", - "studyStart": "Începe" + "studyCloneStudy": "Clonează", + "studyStudyPgn": "PGN-ul studiului", + "studyDownloadAllGames": "Descarcă toate partidele", + "studyChapterPgn": "PGN-ul capitolului", + "studyCopyChapterPgn": "Copiază PGN", + "studyDownloadGame": "Descarcă partida", + "studyStudyUrl": "URL-ul studiului", + "studyCurrentChapterUrl": "URL-ul capitolului curent", + "studyYouCanPasteThisInTheForumToEmbed": "Poți lipi acest cod în forum pentru a îngloba", + "studyStartAtInitialPosition": "Începeți de la poziția inițială", + "studyStartAtX": "Începeți la {param}", + "studyEmbedInYourWebsite": "Înglobează pe site-ul sau blog-ul tău", + "studyReadMoreAboutEmbedding": "Citește mai multe despre înglobare", + "studyOnlyPublicStudiesCanBeEmbedded": "Numai studii publice pot fi înglobate!", + "studyOpen": "Deschideți", + "studyXBroughtToYouByY": "{param1}, oferit pentru dvs. de {param2}", + "studyStudyNotFound": "Studiul nu a fost găsit", + "studyEditChapter": "Editează capitolul", + "studyNewChapter": "Capitol nou", + "studyImportFromChapterX": "Importă din {param}", + "studyOrientation": "Orientare", + "studyAnalysisMode": "Tip de analiză", + "studyPinnedChapterComment": "Comentariu fixat", + "studySaveChapter": "Salvează capitolul", + "studyClearAnnotations": "Curățați adnotările", + "studyClearVariations": "Curățați variațiile", + "studyDeleteChapter": "Ștergeți capitolul", + "studyDeleteThisChapter": "Ștergeți acest capitol? Nu există cale de întoarcere!", + "studyClearAllCommentsInThisChapter": "Ștergeți toate comentariile, simbolurile și figurile desenate din acest capitol?", + "studyRightUnderTheBoard": "Fix sub tablă", + "studyNoPinnedComment": "Niciunul", + "studyNormalAnalysis": "Analiză normală", + "studyHideNextMoves": "Ascunde următoarele mutări", + "studyInteractiveLesson": "Lecție interactivă", + "studyChapterX": "Capitolul {param}", + "studyEmpty": "Gol", + "studyStartFromInitialPosition": "Începeți de la poziția inițială", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Începeți de la o poziție personalizată", + "studyLoadAGameByUrl": "Încărcați meciul din URL", + "studyLoadAPositionFromFen": "Încărcați o poziție din FEN", + "studyLoadAGameFromPgn": "Încărcați un joc din PGN", + "studyAutomatic": "Automată", + "studyUrlOfTheGame": "URL-ul jocului", + "studyLoadAGameFromXOrY": "Încărcați un joc de pe {param1} sau {param2}", + "studyCreateChapter": "Creați capitolul", + "studyCreateStudy": "Creați studiul", + "studyEditStudy": "Editați studiul", + "studyVisibility": "Vizibilitate", + "studyPublic": "Public", + "studyUnlisted": "Nelistat", + "studyInviteOnly": "Doar invitați", + "studyAllowCloning": "Permiteți clonarea", + "studyNobody": "Nimeni", + "studyOnlyMe": "Doar eu", + "studyContributors": "Contribuitori", + "studyMembers": "Membri", + "studyEveryone": "Toată lumea", + "studyEnableSync": "Activați sincronizarea", + "studyYesKeepEveryoneOnTheSamePosition": "Da: menține-i pe toți la aceeași poziție", + "studyNoLetPeopleBrowseFreely": "Nu: permite navigarea liberă", + "studyPinnedStudyComment": "Comentariu fixat", + "studyStart": "Începe", + "studySave": "Salvează", + "studyClearChat": "Șterge conversația", + "studyDeleteTheStudyChatHistory": "Ștergeți istoricul chatului? Nu există cale de întoarcere!", + "studyDeleteStudy": "Ștergeți studiul", + "studyConfirmDeleteStudy": "Ștergeți întregul studiu? Nu există cale de întoarcere! Introduceți numele studiului pentru a confirma: {param}", + "studyWhereDoYouWantToStudyThat": "Unde vreți s-o studiați?", + "studyGoodMove": "Mutare bună", + "studyMistake": "Greșeală", + "studyBrilliantMove": "Mișcare genială", + "studyBlunder": "Gafă", + "studyInterestingMove": "Mișcare interesantă", + "studyDubiousMove": "Mutare dubioasă", + "studyOnlyMove": "Singura mișcare posibilă", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Poziție egală", + "studyUnclearPosition": "Poziție neclară", + "studyWhiteIsSlightlyBetter": "Albul este puțin mai bun", + "studyBlackIsSlightlyBetter": "Negrul este puțin mai bun", + "studyWhiteIsBetter": "Albul este mai bun", + "studyBlackIsBetter": "Negrul este mai bun", + "studyWhiteIsWinning": "Albul câștigă", + "studyBlackIsWinning": "Negrul câștigă", + "studyNovelty": "Noutate", + "studyDevelopment": "Dezvoltare", + "studyInitiative": "Inițiativă", + "studyAttack": "Atac", + "studyCounterplay": "Contraatac", + "studyTimeTrouble": "Probleme de timp", + "studyWithCompensation": "Cu compensații", + "studyWithTheIdea": "Cu ideea", + "studyNextChapter": "Capitolul următor", + "studyPrevChapter": "Capitolul precedent", + "studyStudyActions": "Acţiuni de studiu", + "studyTopics": "Subiecte", + "studyMyTopics": "Subiectele mele", + "studyPopularTopics": "Subiecte populare", + "studyManageTopics": "Gestionează subiecte", + "studyBack": "Înapoi", + "studyPlayAgain": "Joacă din nou", + "studyWhatWouldYouPlay": "Ce ai juca în această poziție?", + "studyYouCompletedThisLesson": "Felicitări! Ai terminat această lecție.", + "studyPerPage": "{param} pe pagină", + "studyNbChapters": "{count, plural, =1{{count} capitol} few{{count} capitole} other{{count} capitole}}", + "studyNbGames": "{count, plural, =1{{count} partidă} few{{count} partide} other{{count} partide}}", + "studyNbMembers": "{count, plural, =1{{count} membru} few{{count} membri} other{{count} membri}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Lipiți textul PGN aici, până la {count} meci} few{Lipiți textul PGN aici, până la {count} meciuri} other{Lipiți textul PGN aici, până la {count} meciuri}}", + "timeagoJustNow": "chiar acum", + "timeagoRightNow": "chiar acum", + "timeagoCompleted": "completat", + "timeagoInNbSeconds": "{count, plural, =1{în {count} secundă} few{în {count} secunde} other{în {count} de secunde}}", + "timeagoInNbMinutes": "{count, plural, =1{în {count} minut} few{în {count} minute} other{în {count} de minute}}", + "timeagoInNbHours": "{count, plural, =1{în {count} oră} few{în {count} ore} other{în {count} de ore}}", + "timeagoInNbDays": "{count, plural, =1{în {count} zi} few{în {count} zile} other{în {count} de zile}}", + "timeagoInNbWeeks": "{count, plural, =1{în {count} săptămână} few{în {count} săptămâni} other{în {count} de săptămâni}}", + "timeagoInNbMonths": "{count, plural, =1{în {count} lună} few{în {count} luni} other{în {count} de luni}}", + "timeagoInNbYears": "{count, plural, =1{în {count} an} few{în {count} ani} other{în {count} de ani}}", + "timeagoNbMinutesAgo": "{count, plural, =1{cu {count} minut în urmă} few{cu {count} minute în urmă} other{cu {count} de minute în urmă}}", + "timeagoNbHoursAgo": "{count, plural, =1{cu {count} oră în urmă} few{cu {count} ore în urmă} other{cu {count} de ore în urmă}}", + "timeagoNbDaysAgo": "{count, plural, =1{cu {count} zi în urmă} few{cu {count} zile în urmă} other{cu {count} de zile în urmă}}", + "timeagoNbWeeksAgo": "{count, plural, =1{cu {count} săptămână în urmă} few{cu {count} săptămâni în urmă} other{cu {count} de săptămâni în urmă}}", + "timeagoNbMonthsAgo": "{count, plural, =1{cu {count} lună în urmă} few{cu {count} luni în urmă} other{cu {count} de luni în urmă}}", + "timeagoNbYearsAgo": "{count, plural, =1{cu {count} an în urmă} few{cu {count} ani în urmă} other{cu {count} de ani în urmă}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut rămas} few{{count} minute rămase} other{{count} minute rămase}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} oră rămasă} few{{count} ore rămase} other{{count} ore rămase}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ru.arb b/lib/l10n/lila_ru.arb index f27d6066a3..c98d6638fd 100644 --- a/lib/l10n/lila_ru.arb +++ b/lib/l10n/lila_ru.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Все игры", + "mobileAreYouSure": "Вы уверены?", + "mobileBlindfoldMode": "Игра вслепую", + "mobileCancelTakebackOffer": "Отменить предложение о возврате хода", + "mobileClearButton": "Очистить", + "mobileCorrespondenceClearSavedMove": "Очистить сохранённый ход", + "mobileCustomGameJoinAGame": "Присоединиться к игре", + "mobileFeedbackButton": "Отзыв", + "mobileGreeting": "Привет, {param}", + "mobileGreetingWithoutName": "Привет", + "mobileHideVariation": "Скрыть варианты", "mobileHomeTab": "Главная", - "mobilePuzzlesTab": "Задачи", - "mobileToolsTab": "Анализ", - "mobileWatchTab": "Просмотр", - "mobileSettingsTab": "Настройки", + "mobileLiveStreamers": "Стримеры в эфире", "mobileMustBeLoggedIn": "Вы должны войти для просмотра этой страницы.", - "mobileSystemColors": "Цвет интерфейса", - "mobileFeedbackButton": "Отзыв", + "mobileNoSearchResults": "Ничего не найденo", + "mobileNotFollowingAnyUser": "Вы не подписаны на других пользователей.", "mobileOkButton": "ОК", + "mobilePlayersMatchingSearchTerm": "Игроки, содержащие «{param}»", + "mobilePrefMagnifyDraggedPiece": "Увеличивать перетаскиваемую фигуру", + "mobilePuzzleStormConfirmEndRun": "Хотите закончить эту попытку?", + "mobilePuzzleStormFilterNothingToShow": "Ничего не найдено, измените фильтры, пожалуйста", + "mobilePuzzleStormNothingToShow": "Ничего нет. Сыграйте несколько попыток.", + "mobilePuzzleStormSubtitle": "Решите как можно больше задач за 3 минуты.", + "mobilePuzzleStreakAbortWarning": "Вы потеряете свою текущую серию, и результаты будут сохранены.", + "mobilePuzzleThemesSubtitle": "Решайте задачи по вашим любимым дебютам или выберите тему.", + "mobilePuzzlesTab": "Задачи", + "mobileRecentSearches": "Последние запросы", "mobileSettingsHapticFeedback": "Виброотклик", "mobileSettingsImmersiveMode": "Полноэкранный режим", "mobileSettingsImmersiveModeSubtitle": "Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.", - "mobileNotFollowingAnyUser": "Вы не подписаны на других пользователей.", - "mobileAllGames": "Все игры", - "mobileRecentSearches": "Последние запросы", - "mobileClearButton": "Очистить", - "mobilePlayersMatchingSearchTerm": "Игроки, содержащие «{param}»", - "mobileNoSearchResults": "Ничего не найденo", - "mobileAreYouSure": "Вы уверены?", - "mobilePuzzleStreakAbortWarning": "Вы потеряете свою текущую серию, и результаты будут сохранены.", - "mobilePuzzleStormNothingToShow": "Ничего нет. Сыграйте несколько попыток.", - "mobileSharePuzzle": "Поделиться задачей", - "mobileShareGameURL": "Поделиться ссылкой на игру", + "mobileSettingsTab": "Настройки", "mobileShareGamePGN": "Поделиться PGN", + "mobileShareGameURL": "Поделиться ссылкой на игру", "mobileSharePositionAsFEN": "Поделиться FEN", - "mobileShowVariations": "Показывать варианты", - "mobileHideVariation": "Скрыть варианты", + "mobileSharePuzzle": "Поделиться задачей", "mobileShowComments": "Показать комментарии", - "mobilePuzzleStormConfirmEndRun": "Хотите закончить эту попытку?", - "mobilePuzzleStormFilterNothingToShow": "Ничего не найдено, измените фильтры, пожалуйста", - "mobileCancelTakebackOffer": "Отменить предложение о возврате хода", - "mobileCancelDrawOffer": "Отменить предложение ничьей", - "mobileWaitingForOpponentToJoin": "Ожидание соперника...", - "mobileBlindfoldMode": "Игра вслепую", - "mobileLiveStreamers": "Стримеры в эфире", - "mobileCustomGameJoinAGame": "Присоединиться к игре", - "mobileCorrespondenceClearSavedMove": "Очистить сохранённый ход", - "mobileSomethingWentWrong": "Что-то пошло не так.", "mobileShowResult": "Показать результат", - "mobilePuzzleThemesSubtitle": "Решайте задачи по вашим любимым дебютам или выберите тему.", - "mobilePuzzleStormSubtitle": "Решите как можно больше задач за 3 минуты.", - "mobileGreeting": "Привет, {param}", - "mobileGreetingWithoutName": "Привет", + "mobileShowVariations": "Показывать варианты", + "mobileSomethingWentWrong": "Что-то пошло не так.", + "mobileSystemColors": "Цвет интерфейса", + "mobileTheme": "Оформление", + "mobileToolsTab": "Анализ", + "mobileWaitingForOpponentToJoin": "Ожидание соперника...", + "mobileWatchTab": "Просмотр", "activityActivity": "Активность", "activityHostedALiveStream": "Проведён стрим", "activityRankedInSwissTournament": "Занято {param1} место в {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Сделан {count} ход} few{Сделано {count} хода} many{Сделано {count} ходов} other{Сделано {count} ходов}}", "activityInNbCorrespondenceGames": "{count, plural, =1{в {count} игре по переписке} few{в {count} играх по переписке} many{в {count} играх по переписке} other{в {count} играх по переписке}}", "activityCompletedNbGames": "{count, plural, =1{Завершена {count} игра по переписке} few{Завершены {count} игры по переписке} many{Завершены {count} игр по переписке} other{Завершены {count} игр по переписке}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Завершена {count} {param2} игра по переписке} few{Завершены {count} {param2} игры по переписке} many{Завершены {count} {param2} игр по переписке} other{Завершены {count} {param2} игр по переписке}}", "activityFollowedNbPlayers": "{count, plural, =1{{count} игрок добавлен в подписку} few{{count} игрока добавлены в подписку} many{{count} игроков добавлены в подписку} other{{count} игроков добавлены в подписку}}", "activityGainedNbFollowers": "{count, plural, =1{Добавился {count} новый подписчик} few{Добавились {count} новых подписчика} many{Добавились {count} новых подписчиков} other{Добавились {count} новых подписчиков}}", "activityHostedNbSimuls": "{count, plural, =1{Проведён {count} сеанс одновременной игры} few{Проведены {count} сеанса одновременной игры} many{Проведены {count} сеансов одновременной игры} other{Проведены {count} сеансов одновременной игры}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Завершён {count} турнир по швейцарской системе} few{Завершено {count} турнира по швейцарской системе} many{Завершено {count} турниров по швейцарской системе} other{Завершено {count} турниров по швейцарской системе}}", "activityJoinedNbTeams": "{count, plural, =1{Принят в {count} клуб} few{Принят в {count} клуба} many{Принят в {count} клубов} other{Принят в {count} клубов}}", "broadcastBroadcasts": "Трансляции", + "broadcastMyBroadcasts": "Мои трансляции", "broadcastLiveBroadcasts": "Прямые трансляции турнира", + "broadcastBroadcastCalendar": "Календарь трансляций", + "broadcastNewBroadcast": "Новая прямая трансляция", + "broadcastSubscribedBroadcasts": "Подписанные рассылки", + "broadcastAboutBroadcasts": "О трансляции", + "broadcastHowToUseLichessBroadcasts": "Как пользоваться трансляциями Lichess.", + "broadcastTheNewRoundHelp": "В новом туре примут участие те же участники и редакторы, что и в предыдущем туре.", + "broadcastAddRound": "Добавить тур", + "broadcastOngoing": "Текущие", + "broadcastUpcoming": "Предстоящие", + "broadcastCompleted": "Завершённые", + "broadcastCompletedHelp": "Lichess определяет завершение тура на основе источника партий. Используйте этот переключатель, если нет источника.", + "broadcastRoundName": "Название тура", + "broadcastRoundNumber": "Номер тура", + "broadcastTournamentName": "Название турнира", + "broadcastTournamentDescription": "Краткое описание турнира", + "broadcastFullDescription": "Полное описание события", + "broadcastFullDescriptionHelp": "Необязательное полное описание трансляции. Доступна разметка {param1}. Длина должна быть меньше {param2} символов.", + "broadcastSourceSingleUrl": "Исходный URL PGN", + "broadcastSourceUrlHelp": "URL-адрес, с которого Lichess будет получать обновление PGN. Он должен быть доступен для получения из Интернета.", + "broadcastSourceGameIds": "До 64 идентификаторов (ID) игр Lichess, разделённых пробелами.", + "broadcastStartDateTimeZone": "Дата начала турнира в местном часовом поясе: {param}", + "broadcastStartDateHelp": "Дополнительно, если вы знаете, когда событие начнётся", + "broadcastCurrentGameUrl": "URL-адрес текущей партии", + "broadcastDownloadAllRounds": "Скачать все туры", + "broadcastResetRound": "Сбросить тур", + "broadcastDeleteRound": "Удалить этот тур", + "broadcastDefinitivelyDeleteRound": "Определенно удалить тур и его партии.", + "broadcastDeleteAllGamesOfThisRound": "Удалить все партии этого тура. Для их пересоздания потребуется активный источник.", + "broadcastEditRoundStudy": "Редактировать студию тура", + "broadcastDeleteTournament": "Удалить этот турнир", + "broadcastDefinitivelyDeleteTournament": "Окончательно удалить весь турнир, его туры и партии.", + "broadcastShowScores": "Показать очки игроков по результатам партий", + "broadcastReplacePlayerTags": "Необязательно: заменить имена игроков, рейтинги и звания", + "broadcastFideFederations": "Федерации FIDE", + "broadcastTop10Rating": "Топ-10", + "broadcastFidePlayers": "Игроки FIDE", + "broadcastFidePlayerNotFound": "Профиль FIDE не найден", + "broadcastFideProfile": "Профиль FIDE", + "broadcastFederation": "Федерация", + "broadcastAgeThisYear": "Возраст в этом году", + "broadcastUnrated": "Без рейтинга", + "broadcastRecentTournaments": "Недавние турниры", + "broadcastOpenLichess": "Открыть в Lichess", + "broadcastTeams": "Клубы", + "broadcastBoards": "Доски", + "broadcastOverview": "Обзор", + "broadcastSubscribeTitle": "Подпишитесь, чтобы получать уведомления о начале каждого раунда. Вы можете включить звуковое или пуш-уведомление для трансляций в своих настройках.", + "broadcastUploadImage": "Загрузить изображение турнира", + "broadcastNoBoardsYet": "Пока нет досок. Они появятся после загрузки партий.", + "broadcastBoardsCanBeLoaded": "Доски могут быть загружены из источника или с помощью {param}", + "broadcastStartsAfter": "Начало после {param}", + "broadcastStartVerySoon": "Трансляция начнётся совсем скоро.", + "broadcastNotYetStarted": "Трансляция ещё не началась.", + "broadcastOfficialWebsite": "Официальный веб-сайт", + "broadcastStandings": "Турнирная таблица", + "broadcastOfficialStandings": "Официальная турнирная таблица", + "broadcastIframeHelp": "Больше опций на {param}", + "broadcastWebmastersPage": "странице веб-мастера", + "broadcastPgnSourceHelp": "Публичный PGN-источник для этого раунда в реальном времени. Мы также предлагаем {param} для более быстрой и эффективной синхронизации.", + "broadcastEmbedThisBroadcast": "Встройте эту трансляцию на ваш сайт", + "broadcastEmbedThisRound": "Встроить {param} на свой сайт", + "broadcastRatingDiff": "Разница в рейтингах", + "broadcastGamesThisTournament": "Партии этого турнира", + "broadcastScore": "Очки", + "broadcastAllTeams": "Все клубы", + "broadcastTournamentFormat": "Формат турнира", + "broadcastTournamentLocation": "Местоположение турнира", + "broadcastTopPlayers": "Лучшие игроки", + "broadcastTimezone": "Часовой пояс", + "broadcastFideRatingCategory": "Категория рейтинга FIDE", + "broadcastOptionalDetails": "Необязательные данные", + "broadcastPastBroadcasts": "Завершённые трансляции", + "broadcastAllBroadcastsByMonth": "Просмотр всех трансляций за месяц", + "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляция} few{{count} трансляции} many{{count} трансляций} other{{count} трансляций}}", "challengeChallengesX": "Вызовов: {param1}", "challengeChallengeToPlay": "Вызвать на игру", "challengeChallengeDeclined": "Вызов отклонён", @@ -184,10 +261,11 @@ "preferencesNotifyTournamentSoon": "Турнир скоро начнётся", "preferencesNotifyTimeAlarm": "В игре по переписке скоро упадёт флажок", "preferencesNotifyBell": "Звуковое оповещение на Личесс", - "preferencesNotifyPush": "Оповещение на устройстве, когда Вы не находитесь на сайте Личесс", + "preferencesNotifyPush": "Оповещение на устройстве, когда вы не находитесь на сайте Lichess", "preferencesNotifyWeb": "Браузер", "preferencesNotifyDevice": "Устройство", "preferencesBellNotificationSound": "Звук колокольчика уведомлений", + "preferencesBlindfold": "Игра вслепую", "puzzlePuzzles": "Задачи", "puzzlePuzzleThemes": "Темы задач", "puzzleRecommended": "Рекомендуемые", @@ -261,7 +339,7 @@ "puzzleStrengthDescription": "Вы показываете лучшие результаты в этих темах", "puzzlePlayedXTimes": "{count, plural, =1{Решено {count} раз} few{Решено {count} раза} many{Решено {count} раз} other{Решено {count} раз}}", "puzzleNbPointsBelowYourPuzzleRating": "{count, plural, =1{Один балл ниже вашего рейтинга в задачах} few{{count} баллов ниже вашего рейтинга в задачах} many{{count} баллов ниже вашего рейтинга в задачах} other{{count} баллов ниже вашего рейтинга в задачах}}", - "puzzleNbPointsAboveYourPuzzleRating": "{count, plural, =1{Один балл выше вашего рейтинга в пазлах} few{{count} баллов выше вашего рейтинга в задачах} many{{count} баллов выше вашего рейтинга в задачах} other{{count} баллов выше вашего рейтинга в задачах}}", + "puzzleNbPointsAboveYourPuzzleRating": "{count, plural, =1{Один балл выше вашего рейтинга в задачах} few{{count} баллов выше вашего рейтинга в задачах} many{{count} баллов выше вашего рейтинга в задачах} other{{count} баллов выше вашего рейтинга в задачах}}", "puzzleNbPlayed": "{count, plural, =1{{count} решена} few{{count} решены} many{{count} решены} other{{count} решено}}", "puzzleNbToReplay": "{count, plural, =1{{count} повторить} few{{count} повторить} many{{count} повторить} other{{count} повторить}}", "puzzleThemeAdvancedPawn": "Продвинутая пешка", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Ситуация, когда на линии нападения или защиты дальнобойной фигуры стоит фигура противника.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Противник вынужден сделать один из немногих возможных ходов, но любой ход ведёт к ухудшению его положения.", - "puzzleThemeHealthyMix": "Сборная солянка", - "puzzleThemeHealthyMixDescription": "Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.", + "puzzleThemeMix": "Сборная солянка", + "puzzleThemeMixDescription": "Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.", "puzzleThemePlayerGames": "Партии игрока", "puzzleThemePlayerGamesDescription": "Найти задачи, созданные из ваших партий, или партий других игроков.", "puzzleThemePuzzleDownloadInformation": "Эти задачи находятся в общественном достоянии и вы можете скачать их: {param}.", @@ -492,12 +570,12 @@ "openingExplorer": "База дебютов", "openingEndgameExplorer": "База дебютов/окончаний", "xOpeningExplorer": "База дебютов для {param}", - "playFirstOpeningEndgameExplorerMove": "Играть первый ход изучателя дебютов/эндшпилей", + "playFirstOpeningEndgameExplorerMove": "Играть первый ход изучения дебютов/эндшпилей", "winPreventedBy50MoveRule": "Не удаётся победить из-за правила 50 ходов", "lossSavedBy50MoveRule": "Удаётся избежать поражения из-за правила 50 ходов", "winOr50MovesByPriorMistake": "Победа или правило 50 ходов", "lossOr50MovesByPriorMistake": "Поражение или 50 ходов после последней ошибки", - "unknownDueToRounding": "Победа/поражение гарантируется только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.", + "unknownDueToRounding": "Победа/поражение гарантируется, только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.", "allSet": "Готово!", "importPgn": "Импортировать в PGN", "delete": "Удалить", @@ -505,7 +583,6 @@ "replayMode": "Смотреть в повторе", "realtimeReplay": "Как в партии", "byCPL": "По ошибкам", - "openStudy": "Открыть в студии", "enable": "Включить", "bestMoveArrow": "Показывать лучшие ходы стрелками", "showVariationArrows": "Показать стрелки вариантов", @@ -515,7 +592,6 @@ "memory": "Память", "infiniteAnalysis": "Бесконечный анализ", "removesTheDepthLimit": "Снимает ограничение на глубину анализа, но заставляет поработать ваш компьютер", - "engineManager": "Менеджер движка", "blunder": "Зевок", "mistake": "Ошибка", "inaccuracy": "Неточность", @@ -597,6 +673,7 @@ "rank": "Ранг", "rankX": "Место: {param}", "gamesPlayed": "Сыграно партий", + "ok": "ОК", "cancel": "Отменить", "whiteTimeOut": "Белые просрочили время", "blackTimeOut": "Чёрные просрочили время", @@ -713,7 +790,6 @@ "block": "Заблокировать", "blocked": "Заблокированные", "unblock": "Разблокировать", - "followsYou": "Подписан на вас", "xStartedFollowingY": "{param1} подписался на {param2}", "more": "Ещё", "memberSince": "Дата регистрации", @@ -796,7 +872,7 @@ "name": "Имя", "description": "Описание", "descPrivate": "Описание для членов команды", - "descPrivateHelp": "Текст, который будут видеть только члены команды(добавленный текст заменит публичное описание для членов команды).", + "descPrivateHelp": "Описание, которое будут видеть только члены клуба. Если установлено, то заменяет публичное описание для всех членов клуба.", "no": "Нет", "yes": "Да", "website": "Сайт", @@ -819,7 +895,9 @@ "cheat": "Жульничество", "troll": "Троллинг", "other": "Другое", - "reportDescriptionHelp": "Поделитесь с нами ссылками на игры, где, как вам кажется, были нарушены правила, и опишите, в чём дело. Недостаточно просто написать «он мухлюет», пожалуйста, опишите, как вы пришли к такому выводу. Мы сработаем оперативнее, если вы напишете на английском языке.", + "reportCheatBoostHelp": "Вставьте ссылку на игру (или несколько игр) и объясните, что не так в поведении этого пользователя. Не надо просто писать «он жульничал», лучше распишите, как вы пришли к такому выводу.", + "reportUsernameHelp": "Объясните, что в этом имени пользователя является оскорбительным. Не надо просто писать «оно оскорбительно или неподобающе», лучше расскажите, как вы пришли к такому выводу, особенно если оскорбление завуалировано, не на английском языке, является сленгом, или же является исторической или культурной отсылкой.", + "reportProcessedFasterInEnglish": "Ваша жалоба будет рассмотрена быстрее, если она будет написана на английском языке.", "error_provideOneCheatedGameLink": "Пожалуйста, добавьте ссылку хотя бы на одну игру, где по вашему мнению были нарушены правила.", "by": "{param}", "importedByX": "Импортировано {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Показать всё", "lichessPatronInfo": "Lichess - это благотворительное и полностью бесплатное программное обеспечение с открытым исходным кодом.\nВсе эксплуатационные расходы, разработка и контент финансируются исключительно за счет пожертвований пользователей.", "nothingToSeeHere": "Здесь ничего нет пока.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунду.} few{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунды.} many{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунд.} other{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунд.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} полуход} few{Мат в {count} полухода} many{Мат в {count} полуходов} other{Мат в {count} полуходов}}", "nbBlunders": "{count, plural, =1{{count} зевок} few{{count} зевка} many{{count} зевков} other{{count} зевков}}", @@ -1228,7 +1307,7 @@ "nbBookmarks": "{count, plural, =1{{count} отмеченная} few{{count} отмеченные} many{{count} отмеченных} other{{count} отмеченных}}", "nbDays": "{count, plural, =1{{count} день} few{{count} дня} many{{count} дней} other{{count} дней}}", "nbHours": "{count, plural, =1{{count} час} few{{count} часа} many{{count} часов} other{{count} часов}}", - "nbMinutes": "{count, plural, =1{{count} минута} few{{count} минуты} many{{count} минут} other{{count} минут}}", + "nbMinutes": "{count, plural, =1{{count} Одна минута} few{{count} Минуты} many{{count} минут} other{{count} минут}}", "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{Место обновляется ежеминутно} few{Место обновляется каждые {count} минуты} many{Место обновляется каждые {count} минут} other{Место обновляется каждые {count} минут}}", "nbPuzzles": "{count, plural, =1{{count} задача} few{{count} задачи} many{{count} задач} other{{count} задач}}", "nbGamesWithYou": "{count, plural, =1{{count} партия с вами} few{{count} партии с вами} many{{count} партий с вами} other{{count} партий с вами}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 попытка} few{{count} попытки} many{{count} попыток} other{{count} попыток}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Сыграна {count} серия в {param2}} few{Сыграны {count} серии в {param2}} many{Сыграны {count} серий в {param2}} other{Сыграно {count} серий в {param2}}}", "streamerLichessStreamers": "Стримеры Lichess", + "studyPrivate": "Частная", + "studyMyStudies": "Мои студии", + "studyStudiesIContributeTo": "Студии с моим участием", + "studyMyPublicStudies": "Мои публичные студии", + "studyMyPrivateStudies": "Мои частные студии", + "studyMyFavoriteStudies": "Мои отмеченные студии", + "studyWhatAreStudies": "Что такое «студии»?", + "studyAllStudies": "Все студии", + "studyStudiesCreatedByX": "Студии, созданные {param}", + "studyNoneYet": "Пока ничего.", + "studyHot": "Самые активные", + "studyDateAddedNewest": "Недавно добавленные", + "studyDateAddedOldest": "Давно добавленные", + "studyRecentlyUpdated": "Недавно обновлённые", + "studyMostPopular": "Самые популярные", + "studyAlphabetical": "По алфавиту", + "studyAddNewChapter": "Добавить новую главу", + "studyAddMembers": "Добавить участников", + "studyInviteToTheStudy": "Пригласить в студию", + "studyPleaseOnlyInvitePeopleYouKnow": "Приглашайте только тех участников, которых вы знаете, и кто активно желает участвовать в этой студии.", + "studySearchByUsername": "Поиск по имени", + "studySpectator": "Зритель", + "studyContributor": "Редактор", + "studyKick": "Выгнать", + "studyLeaveTheStudy": "Покинуть студию", + "studyYouAreNowAContributor": "Теперь вы редактор", + "studyYouAreNowASpectator": "Теперь вы зритель", + "studyPgnTags": "Теги PGN", + "studyLike": "Нравится", + "studyUnlike": "Не нравится", + "studyNewTag": "Новый тег", + "studyCommentThisPosition": "Комментировать эту позицию", + "studyCommentThisMove": "Комментировать этот ход", + "studyAnnotateWithGlyphs": "Добавить символьную аннотацию", + "studyTheChapterIsTooShortToBeAnalysed": "Глава слишком короткая для анализа.", + "studyOnlyContributorsCanRequestAnalysis": "Только редакторы студии могут запросить компьютерный анализ.", + "studyGetAFullComputerAnalysis": "Получить с сервера полный компьютерный анализ главной линии.", + "studyMakeSureTheChapterIsComplete": "Убедитесь, что глава завершена. Вы можете запросить анализ только один раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Все синхронизированные участники остаются на той же позиции", + "studyShareChanges": "Поделиться изменениями со зрителями и сохранить их на сервере", + "studyPlaying": "Активные", + "studyShowEvalBar": "Шкалы оценки", + "studyFirst": "Первая", + "studyPrevious": "Назад", + "studyNext": "Дальше", + "studyLast": "Последняя", "studyShareAndExport": "Поделиться и экспортировать", - "studyStart": "Начать" + "studyCloneStudy": "Клонировать", + "studyStudyPgn": "PGN студии", + "studyDownloadAllGames": "Скачать все партии", + "studyChapterPgn": "PGN главы", + "studyCopyChapterPgn": "Копировать PGN", + "studyDownloadGame": "Скачать партию", + "studyStudyUrl": "Ссылка на студию", + "studyCurrentChapterUrl": "Ссылка на эту главу", + "studyYouCanPasteThisInTheForumToEmbed": "Вставьте этот код на форум для вставки", + "studyStartAtInitialPosition": "Открыть в начальной позиции", + "studyStartAtX": "Начать с {param}", + "studyEmbedInYourWebsite": "Вставить в свой сайт или блог", + "studyReadMoreAboutEmbedding": "Подробнее о вставке на сайт", + "studyOnlyPublicStudiesCanBeEmbedded": "Вставлять на сайт можно только публичные студии!", + "studyOpen": "Открыть", + "studyXBroughtToYouByY": "{param1} на {param2}", + "studyStudyNotFound": "Студия не найдена", + "studyEditChapter": "Редактировать главу", + "studyNewChapter": "Новая глава", + "studyImportFromChapterX": "Импорт из {param}", + "studyOrientation": "Ориентация", + "studyAnalysisMode": "Режим анализа", + "studyPinnedChapterComment": "Закреплённый комментарий главы", + "studySaveChapter": "Сохранить главу", + "studyClearAnnotations": "Очистить аннотацию", + "studyClearVariations": "Очистить варианты", + "studyDeleteChapter": "Удалить главу", + "studyDeleteThisChapter": "Удалить эту главу? Её нельзя будет вернуть!", + "studyClearAllCommentsInThisChapter": "Очистить все комментарии и обозначения этой главы?", + "studyRightUnderTheBoard": "Прямо под доской", + "studyNoPinnedComment": "Нет", + "studyNormalAnalysis": "Обычный анализ", + "studyHideNextMoves": "Скрыть последующие ходы", + "studyInteractiveLesson": "Интерактивный урок", + "studyChapterX": "Глава {param}", + "studyEmpty": "Пусто", + "studyStartFromInitialPosition": "Начать с исходной позиции", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Начать со своей позиции", + "studyLoadAGameByUrl": "Загрузить игру по URL", + "studyLoadAPositionFromFen": "Загрузить позицию из FEN", + "studyLoadAGameFromPgn": "Загрузить игру из PGN", + "studyAutomatic": "Автоматически", + "studyUrlOfTheGame": "URL игры", + "studyLoadAGameFromXOrY": "Загрузить игру из {param1} или {param2}", + "studyCreateChapter": "Создать главу", + "studyCreateStudy": "Создать студию", + "studyEditStudy": "Изменить студию", + "studyVisibility": "Доступно к просмотру", + "studyPublic": "Публичная", + "studyUnlisted": "Доступ по ссылке", + "studyInviteOnly": "Только по приглашению", + "studyAllowCloning": "Разрешить копирование", + "studyNobody": "Никто", + "studyOnlyMe": "Только я", + "studyContributors": "Соавторы", + "studyMembers": "Участники", + "studyEveryone": "Все", + "studyEnableSync": "Включить синхронизацию", + "studyYesKeepEveryoneOnTheSamePosition": "Да: устанавливать всем одинаковую позицию", + "studyNoLetPeopleBrowseFreely": "Нет: позволить участникам свободно изучать все позиции", + "studyPinnedStudyComment": "Закреплённый комментарий студии", + "studyStart": "Начать", + "studySave": "Сохранить", + "studyClearChat": "Очистить чат", + "studyDeleteTheStudyChatHistory": "Удалить чат студии? Восстановить будет невозможно!", + "studyDeleteStudy": "Удалить студию", + "studyConfirmDeleteStudy": "Удалить всю студию? Удаление необратимо! Введите название студии для подтверждения: {param}", + "studyWhereDoYouWantToStudyThat": "Где вы хотите создать студию?", + "studyGoodMove": "Хороший ход", + "studyMistake": "Ошибка", + "studyBrilliantMove": "Отличный ход", + "studyBlunder": "Зевок", + "studyInterestingMove": "Интересный ход", + "studyDubiousMove": "Сомнительный ход", + "studyOnlyMove": "Единственный ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Равная позиция", + "studyUnclearPosition": "Неясная позиция", + "studyWhiteIsSlightlyBetter": "У белых немного лучше", + "studyBlackIsSlightlyBetter": "У чёрных немного лучше", + "studyWhiteIsBetter": "У белых лучше", + "studyBlackIsBetter": "У чёрных лучше", + "studyWhiteIsWinning": "Белые побеждают", + "studyBlackIsWinning": "Чёрные побеждают", + "studyNovelty": "Новинка", + "studyDevelopment": "Развитие", + "studyInitiative": "Инициатива", + "studyAttack": "Атака", + "studyCounterplay": "Контригра", + "studyTimeTrouble": "Цейтнот", + "studyWithCompensation": "С компенсацией", + "studyWithTheIdea": "С идеей", + "studyNextChapter": "Следующая глава", + "studyPrevChapter": "Предыдущая глава", + "studyStudyActions": "Действия в студии", + "studyTopics": "Темы", + "studyMyTopics": "Мои темы", + "studyPopularTopics": "Популярные темы", + "studyManageTopics": "Управление темами", + "studyBack": "Назад", + "studyPlayAgain": "Сыграть снова", + "studyWhatWouldYouPlay": "Как бы вы сыграли в этой позиции?", + "studyYouCompletedThisLesson": "Поздравляем! Вы прошли этот урок.", + "studyPerPage": "{param} на страницу", + "studyNbChapters": "{count, plural, =1{{count} глава} few{{count} главы} many{{count} глав} other{{count} глав}}", + "studyNbGames": "{count, plural, =1{{count} партия} few{{count} партии} many{{count} партий} other{{count} партий}}", + "studyNbMembers": "{count, plural, =1{{count} участник} few{{count} участника} many{{count} участников} other{{count} участников}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставьте текст в формате PGN, не больше {count} игры} few{Вставьте текст в формате PGN, не больше {count} игр} many{Вставьте текст в формате PGN, не больше {count} игр} other{Вставьте текст в формате PGN, не больше {count} игр}}", + "timeagoJustNow": "только что", + "timeagoRightNow": "прямо сейчас", + "timeagoCompleted": "завершено", + "timeagoInNbSeconds": "{count, plural, =1{через {count} секунду} few{через {count} секунды} many{через {count} секунд} other{через {count} секунд}}", + "timeagoInNbMinutes": "{count, plural, =1{через {count} минуту} few{через {count} минуты} many{через {count} минут} other{через {count} минут}}", + "timeagoInNbHours": "{count, plural, =1{через {count} час} few{через {count} часа} many{через {count} часов} other{через {count} часов}}", + "timeagoInNbDays": "{count, plural, =1{через {count} день} few{через {count} дня} many{через {count} дней} other{через {count} дней}}", + "timeagoInNbWeeks": "{count, plural, =1{через {count} неделю} few{через {count} недели} many{через {count} недель} other{через {count} недель}}", + "timeagoInNbMonths": "{count, plural, =1{через {count} месяц} few{через {count} месяца} many{через {count} месяцев} other{через {count} месяцев}}", + "timeagoInNbYears": "{count, plural, =1{через {count} год} few{через {count} года} many{через {count} лет} other{через {count} лет}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} минуту назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минут назад}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часов назад}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} день назад} few{{count} дня назад} many{{count} дней назад} other{{count} дней назад}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} неделю назад} few{{count} недели назад} many{{count} недель назад} other{{count} недель назад}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} месяц назад} few{{count} месяца назад} many{{count} месяцев назад} other{{count} месяцев назад}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} год назад} few{{count} года назад} many{{count} лет назад} other{{count} лет назад}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{осталась {count} минута} few{осталось {count} минуты} many{осталось {count} минут} other{осталось {count} минут}}", + "timeagoNbHoursRemaining": "{count, plural, =1{остался {count} час} few{осталось {count} часа} many{осталось {count} часов} other{осталось {count} часов}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sk.arb b/lib/l10n/lila_sk.arb index a65796c05f..5508e92125 100644 --- a/lib/l10n/lila_sk.arb +++ b/lib/l10n/lila_sk.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Všetky partie", + "mobileAreYouSure": "Ste si istý?", + "mobileBlindfoldMode": "Naslepo", + "mobileCancelTakebackOffer": "Zrušiť žiadosť o vrátenie ťahu", + "mobileClearButton": "Odstrániť", + "mobileCorrespondenceClearSavedMove": "Vymazať uložený ťah", + "mobileCustomGameJoinAGame": "Pripojiť sa k partii", + "mobileFeedbackButton": "Spätná väzba", + "mobileGreeting": "Ahoj, {param}", + "mobileGreetingWithoutName": "Ahoj", + "mobileHideVariation": "Skryť varianty", "mobileHomeTab": "Domov", - "mobilePuzzlesTab": "Úlohy", - "mobileToolsTab": "Nástroje", - "mobileWatchTab": "Sledovať", - "mobileSettingsTab": "Nastavenia", + "mobileLiveStreamers": "Vysielajúci strímeri", "mobileMustBeLoggedIn": "Na zobrazenie tejto stránky musíte byť prihlásený.", - "mobileSystemColors": "Farby operačného systému", - "mobileFeedbackButton": "Spätná väzba", + "mobileNoSearchResults": "Nič sa nenašlo", + "mobileNotFollowingAnyUser": "Nesledujete žiadneho používateľa.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Zväčšiť uchopenú figúrku", + "mobilePuzzleStormConfirmEndRun": "Chcete ukončiť tento pokus?", + "mobilePuzzleStormFilterNothingToShow": "Niet čo zobraziť, prosím, zmeňte filtre", + "mobilePuzzleStormNothingToShow": "Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Vyriešte čo najviac úloh za 3 minúty.", + "mobilePuzzleStreakAbortWarning": "Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.", + "mobilePuzzleThemesSubtitle": "Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.", + "mobilePuzzlesTab": "Úlohy", + "mobileRecentSearches": "Posledné vyhľadávania", "mobileSettingsHapticFeedback": "Vibrovanie zariadenia", "mobileSettingsImmersiveMode": "Režim celej obrazovky", "mobileSettingsImmersiveModeSubtitle": "Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.", - "mobileNotFollowingAnyUser": "Nesledujete žiadneho používateľa.", - "mobileAllGames": "Všetky partie", - "mobileRecentSearches": "Posledné vyhľadávania", - "mobileClearButton": "Odstrániť", - "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", - "mobileNoSearchResults": "Nič sa nenašlo", - "mobileAreYouSure": "Ste si istý?", - "mobilePuzzleStreakAbortWarning": "Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.", - "mobilePuzzleStormNothingToShow": "Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.", - "mobileSharePuzzle": "Zdieľať túto úlohu", - "mobileShareGameURL": "Zdieľať URL partie", + "mobileSettingsTab": "Nastavenia", "mobileShareGamePGN": "Zdieľať PGN", + "mobileShareGameURL": "Zdieľať URL partie", "mobileSharePositionAsFEN": "Zdieľať pozíciu vo formáte FEN", - "mobileShowVariations": "Zobraziť varianty", - "mobileHideVariation": "Skryť varianty", + "mobileSharePuzzle": "Zdieľať túto úlohu", "mobileShowComments": "Zobraziť komentáre", - "mobilePuzzleStormConfirmEndRun": "Chcete ukončiť tento pokus?", - "mobilePuzzleStormFilterNothingToShow": "Niet čo zobraziť, prosím, zmeňte filtre", - "mobileCancelTakebackOffer": "Zrušiť žiadosť o vrátenie ťahu", - "mobileCancelDrawOffer": "Zrušiť navrhnutie remízy", - "mobileWaitingForOpponentToJoin": "Čaká sa na pripojenie súpera...", - "mobileBlindfoldMode": "Naslepo", - "mobileLiveStreamers": "Vysielajúci strímeri", - "mobileCustomGameJoinAGame": "Pripojiť sa k partii", - "mobileCorrespondenceClearSavedMove": "Vymazať uložený ťah", - "mobileSomethingWentWrong": "Došlo k chybe.", "mobileShowResult": "Zobraziť výsledok", - "mobilePuzzleThemesSubtitle": "Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.", - "mobilePuzzleStormSubtitle": "Vyriešte čo najviac úloh za 3 minúty.", - "mobileGreeting": "Ahoj, {param}", - "mobileGreetingWithoutName": "Ahoj", + "mobileShowVariations": "Zobraziť varianty", + "mobileSomethingWentWrong": "Došlo k chybe.", + "mobileSystemColors": "Farby operačného systému", + "mobileTheme": "Vzhľad", + "mobileToolsTab": "Nástroje", + "mobileWaitingForOpponentToJoin": "Čaká sa na pripojenie súpera...", + "mobileWatchTab": "Sledovať", "activityActivity": "Aktivita", "activityHostedALiveStream": "Vysielal naživo", "activityRankedInSwissTournament": "Umiestnený ako #{param1} v {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Urobil {count} ťah} few{Zahral {count} ťahy} many{Zharal {count} ťahov} other{Zahral {count} ťahov}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} korešpondenčnej hre} few{v {count} korešpondenčných hrách} many{v {count} korešpondenčných hrách} other{v {count} korešpondenčných hrách}}", "activityCompletedNbGames": "{count, plural, =1{Dohraná {count} korešpondenčná partia} few{Dohrané {count} korešpondenčné partie} many{Dohraných {count} korešpondenčných partií} other{Dohraných {count} korešpondenčných partií}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Odohraná {count} {param2} korešpondenčná partia} few{Odohrané {count} {param2} korešpondenčné partie} many{Odohraných {count} {param2} korešpondenčných partií} other{Odohraných {count} {param2} korešpondenčných partií}}", "activityFollowedNbPlayers": "{count, plural, =1{Začiatok sledovania {count} hráča} few{Začal sledovať {count} hráčov} many{Začal sledovať {count} hráčov} other{Začal sledovať {count} hráčov}}", "activityGainedNbFollowers": "{count, plural, =1{Získal {count} nového sledovateľa} few{Získal {count} nových sledovateľov} many{Získal {count} nových sledovateľov} other{Získal {count} nových sledovateľov}}", "activityHostedNbSimuls": "{count, plural, =1{Usporiadanie a odohranie {count} simultánky} few{Usporiadanie a odohranie {count} simultánok} many{Usporiadanie a odohranie {count} simultánok} other{Usporiadanie a odohranie {count} simultánok}}", @@ -64,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Odohraný {count} turnaj švajčiarskym systémom} few{Odohrané {count} turnaje švajčiarskym systémom} many{Odohraných {count} turnajov švajčiarskym systémom} other{Odohraných {count} turnajov švajčiarskym systémom}}", "activityJoinedNbTeams": "{count, plural, =1{Vstup do {count} družstva} few{Vstup do {count} družstiev} many{Vstup do {count} družstiev} other{Vstup do {count} družstiev}}", "broadcastBroadcasts": "Vysielanie", + "broadcastMyBroadcasts": "Moje vysielania", "broadcastLiveBroadcasts": "Živé vysielanie turnaja", + "broadcastBroadcastCalendar": "Kalendár vysielaní", + "broadcastNewBroadcast": "Nové živé vysielanie", + "broadcastSubscribedBroadcasts": "Odoberané vysielania", + "broadcastAboutBroadcasts": "O vysielaní", + "broadcastHowToUseLichessBroadcasts": "Ako používať Lichess vysielanie.", + "broadcastTheNewRoundHelp": "Nové kolo bude mať tých istých členov a prispievateľov ako to predchádzajúce.", + "broadcastAddRound": "Pridať kolo", + "broadcastOngoing": "Prebiehajúci", + "broadcastUpcoming": "Blížiace sa", + "broadcastCompleted": "Ukončené", + "broadcastCompletedHelp": "Lichess rozpozná dokončenie kola, ale môže sa pomýliť. Pomocou tejto funkcie ho môžete nastaviť ručne.", + "broadcastRoundName": "Názov kola", + "broadcastRoundNumber": "Číslo kola", + "broadcastTournamentName": "Názov turnaja", + "broadcastTournamentDescription": "Krátky popis turnaja", + "broadcastFullDescription": "Úplný popis turnaja", + "broadcastFullDescriptionHelp": "Voliteľný dlhý popis vysielania. {param1} je dostupný. Dĺžka musí byť menej ako {param2} znakov.", + "broadcastSourceSingleUrl": "Zdrojová URL pre PGN súbor", + "broadcastSourceUrlHelp": "URL, ktorú bude Lichess kontrolovať, aby získal aktualizácie PGN. Musí byť verejne prístupná z internetu.", + "broadcastSourceGameIds": "Až do 64 identifikátorov Lichess partií oddelených medzerami.", + "broadcastStartDateTimeZone": "Dátum začiatku v miestnej časovej zóne turnaja: {param}", + "broadcastStartDateHelp": "Voliteľné, ak viete kedy sa udalosť začne", + "broadcastCurrentGameUrl": "Adresa URL aktuálnej partie", + "broadcastDownloadAllRounds": "Stiahnuť všetky kolá", + "broadcastResetRound": "Resetovať toto kolo", + "broadcastDeleteRound": "Vymazať toto kolo", + "broadcastDefinitivelyDeleteRound": "Definitívne vymazať kolo a partie tohto kola.", + "broadcastDeleteAllGamesOfThisRound": "Vymazať všetky partie tohto kola. K opätovnému vytvoreniu partií bude potrebné aby bol zdroj aktívny.", + "broadcastEditRoundStudy": "Upraviť kolo štúdií", + "broadcastDeleteTournament": "Vymazať tento turnaj", + "broadcastDefinitivelyDeleteTournament": "Definitívne odstrániť celý turnaj so všetkými kolami a všetkými partiami.", + "broadcastShowScores": "Zobraziť skóre hráčov na základe výsledkov partií", + "broadcastReplacePlayerTags": "Voliteľné: nahradiť mená hráčov, hodnotenia a tituly", + "broadcastFideFederations": "FIDE federácie", + "broadcastTop10Rating": "10 najlepšie hodnotených", + "broadcastFidePlayers": "FIDE šachisti", + "broadcastFidePlayerNotFound": "FIDE šachista sa nenašiel", + "broadcastFideProfile": "FIDE profil", + "broadcastFederation": "Federácia", + "broadcastAgeThisYear": "Vek tento rok", + "broadcastUnrated": "Bez hodnotenia", + "broadcastRecentTournaments": "Posledné turnaje", + "broadcastOpenLichess": "Otvoriť na Lichess", + "broadcastTeams": "Tímy", + "broadcastBoards": "Šachovnice", + "broadcastOverview": "Prehľad", + "broadcastSubscribeTitle": "Prihláste sa, aby ste boli informovaní o začiatku každého kola. V nastaveniach účtu môžete prepnúť zvončekové alebo push upozornenia na vysielanie.", + "broadcastUploadImage": "Nahrať obrázok pre turnaj", + "broadcastNoBoardsYet": "Zatiaľ žiadne šachovnice. Objavia sa po nahratí partií.", + "broadcastBoardsCanBeLoaded": "Šachovnice možno načítať pomocou zdroja alebo pomocou {param}", + "broadcastStartsAfter": "Začína po {param}", + "broadcastStartVerySoon": "Vysielanie sa začne čoskoro.", + "broadcastNotYetStarted": "Vysielanie sa ešte nezačalo.", + "broadcastOfficialWebsite": "Oficiálna webstránka", + "broadcastStandings": "Poradie", + "broadcastOfficialStandings": "Oficiálne poradie", + "broadcastIframeHelp": "Viac možností nájdete na {param}", + "broadcastWebmastersPage": "stránke tvorcu", + "broadcastPgnSourceHelp": "Verejný zdroj PGN v reálnom čase pre toto kolo. Ponúkame tiež {param} na rýchlejšiu a efektívnejšiu synchronizáciu.", + "broadcastEmbedThisBroadcast": "Vložiť toto vysielanie na webovú stránku", + "broadcastEmbedThisRound": "Vložiť {param} na webovú stránku", + "broadcastRatingDiff": "Ratingový rozdiel", + "broadcastGamesThisTournament": "Partie tohto turnaja", + "broadcastScore": "Skóre", + "broadcastAllTeams": "Všetky tímy", + "broadcastTournamentFormat": "Formát turnaja", + "broadcastTournamentLocation": "Miesto konania turnaja", + "broadcastTopPlayers": "Najlepší hráči", + "broadcastTimezone": "Časové pásmo", + "broadcastFideRatingCategory": "Kategória FIDE ratingu", + "broadcastOptionalDetails": "Nepovinné údaje", + "broadcastPastBroadcasts": "Predchádzajúce vysielania", + "broadcastAllBroadcastsByMonth": "Zobraziť všetky vysielania podľa mesiacov", + "broadcastNbBroadcasts": "{count, plural, =1{{count} vysielanie} few{{count} vysielania} many{{count} vysielaní} other{{count} vysielaní}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvať na partiu", "challengeChallengeDeclined": "Výzva odmietnutá", @@ -188,6 +265,7 @@ "preferencesNotifyWeb": "Prehliadač", "preferencesNotifyDevice": "Zariadenie", "preferencesBellNotificationSound": "Zvuk upozornenia", + "preferencesBlindfold": "Naslepo", "puzzlePuzzles": "Šachové úlohy", "puzzlePuzzleThemes": "Kategórie úloh", "puzzleRecommended": "Odporúčané", @@ -383,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Figúra bráni alebo útočí na pole cez súperovu figúru.", "puzzleThemeZugzwang": "Nevýhoda ťahu", "puzzleThemeZugzwangDescription": "Súper je limitovaný vo svojich ťahoch a každý ťah zhorší jeho pozíciu.", - "puzzleThemeHealthyMix": "Zdravý mix", - "puzzleThemeHealthyMixDescription": "Zmes úloh. Neviete čo očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.", + "puzzleThemeMix": "Zdravá zmes", + "puzzleThemeMixDescription": "Od všetkého trochu. Neviete, čo môžete očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.", "puzzleThemePlayerGames": "Vaše partie", "puzzleThemePlayerGamesDescription": "Vyhľadajte si úlohy vygenerované z Vašich partií alebo z partií iných hráčov.", "puzzleThemePuzzleDownloadInformation": "Tieto úlohy sú voľne dostupné a môžete si ich stiahnuť z {param}.", @@ -505,7 +583,6 @@ "replayMode": "Mód prehrávania", "realtimeReplay": "Ako pri hre", "byCPL": "CHYBY", - "openStudy": "Otvoriť štúdie", "enable": "Povoliť analýzu", "bestMoveArrow": "Šípka pre najlepší ťah", "showVariationArrows": "Zobraziť šípky variantov", @@ -515,7 +592,6 @@ "memory": "Pamäť", "infiniteAnalysis": "Nekonečná analýza", "removesTheDepthLimit": "Odstráni obmedzenie hĺbky analýzy a spôsobí zahrievanie Vášho počítača", - "engineManager": "Správa motorov", "blunder": "Hrubá chyba", "mistake": "Chyba", "inaccuracy": "Nepresnosť", @@ -597,6 +673,7 @@ "rank": "Poradie", "rankX": "Poradie: {param}", "gamesPlayed": "Odohraných partií", + "ok": "OK", "cancel": "Zrušiť", "whiteTimeOut": "Bielemu došiel čas", "blackTimeOut": "Čiernemu došiel čas", @@ -713,7 +790,6 @@ "block": "Blokovať", "blocked": "Blokovaný", "unblock": "Odblokovať", - "followsYou": "Sleduje Vás", "xStartedFollowingY": "{param1} začal sledovať {param2}", "more": "Viac", "memberSince": "Členom od", @@ -819,7 +895,9 @@ "cheat": "Podvod", "troll": "Troll", "other": "Iné", - "reportDescriptionHelp": "Vložte odkaz na hru/y, a vysvetlite, čo je zlé na tomto správaní používateľa.", + "reportCheatBoostHelp": "Vložte odkaz na partiu(/e) a vysvetlite, čo je na správaní tohto používateľa zlé. Nehovorte len „podvádza“, ale povedzte, ako ste k tomuto záveru dospeli.", + "reportUsernameHelp": "Vysvetlite, čo je na tomto používateľskom mene urážlivé. Nehovorte len „je to urážlivé/nevhodné“, ale povedzte nám, ako ste k tomuto záveru dospeli, najmä ak je urážka významovo zahmlená, nie je v angličtine, je v slangu alebo odkazuje na niečo z historie/kultúry.", + "reportProcessedFasterInEnglish": "Vaša správa bude spracovaná rýchlejšie, ak bude napísaná v angličtine.", "error_provideOneCheatedGameLink": "Prosím, uveďte aspoň jeden odkaz na partiu, v ktorej sa podvádzalo.", "by": "od {param}", "importedByX": "Importoval {param}", @@ -1217,6 +1295,7 @@ "showMeEverything": "Ukázať všetko", "lichessPatronInfo": "Lichess je bezplatný a úplne slobodný/nezávislý softvér s otvoreným zdrojovým kódom. Všetky prevádzkové náklady, vývoj a obsah sú financované výlučne z darov používateľov.", "nothingToSeeHere": "Momentálne tu nie je nič k zobrazeniu.", + "stats": "Štatistiky", "opponentLeftCounter": "{count, plural, =1{Váš súper odišiel od šachovnice. O {count} sekundu si môžete nárokovať výhru.} few{Váš súper odišiel od šachovnice. O {count} sekundy si môžete nárokovať výhru.} many{Váš súper odišiel od šachovnice. O {count} sekúnd si môžete nárokovať výhru.} other{Váš súper odišiel od šachovnice. O {count} sekúnd si môžete nárokovať výhru.}}", "mateInXHalfMoves": "{count, plural, =1{Mat v {count}. polťahu} few{Mat v {count}. polťahu} many{Mat v {count}. polťahu} other{Mat v {count}. polťahu}}", "nbBlunders": "{count, plural, =1{{count} hrubá chyba} few{{count} hrubé chyby} many{{count} hrubých chýb} other{{count} hrubých chýb}}", @@ -1313,6 +1392,178 @@ "stormXRuns": "{count, plural, =1{1 kolo} few{{count} kolá} many{{count} kôl} other{{count} kôl}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odohrané jedeno kolo {param2}} few{Obohrané {count} kolá {param2}} many{Odohraných {count} kôl {param2}} other{Odohraných {count} kôl {param2}}}", "streamerLichessStreamers": "Lichess streameri", + "studyPrivate": "Súkromné", + "studyMyStudies": "Moje štúdie", + "studyStudiesIContributeTo": "Učivo, ku ktorému prispievam", + "studyMyPublicStudies": "Moje verejné štúdie", + "studyMyPrivateStudies": "Moje súkromné učivo", + "studyMyFavoriteStudies": "Moje obľúbené štúdie", + "studyWhatAreStudies": "Čo sú štúdie?", + "studyAllStudies": "Všetko učivo", + "studyStudiesCreatedByX": "Štúdie vytvorené {param}", + "studyNoneYet": "Zatiaľ žiadne.", + "studyHot": "Teraz populárne", + "studyDateAddedNewest": "Dátum pridania (najnovšie)", + "studyDateAddedOldest": "Dátum pridania (najstaršie)", + "studyRecentlyUpdated": "Nedávno aktualizované", + "studyMostPopular": "Najpopulárnejšie", + "studyAlphabetical": "Abecedne", + "studyAddNewChapter": "Pridať novú kapitolu", + "studyAddMembers": "Pridať členov", + "studyInviteToTheStudy": "Pozvať k štúdii", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosím pozývajte iba ľudí ktorých poznáte a o ktorých viete, že chcú túto štúdiu vidieť.", + "studySearchByUsername": "Hľadať podľa použív. mena", + "studySpectator": "Divák", + "studyContributor": "Prispievateľ", + "studyKick": "Vyhodiť", + "studyLeaveTheStudy": "Opustiť štúdiu", + "studyYouAreNowAContributor": "Od teraz ste prispievateľom", + "studyYouAreNowASpectator": "Od teraz ste divákom", + "studyPgnTags": "PGN značka", + "studyLike": "Páči sa mi", + "studyUnlike": "Nepáči sa mi", + "studyNewTag": "Nová značka", + "studyCommentThisPosition": "Komentovať túto pozíciu", + "studyCommentThisMove": "Komentovať tento ťah", + "studyAnnotateWithGlyphs": "Anotovať pomocou glyphov", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitola je príliš krátka na analýzu.", + "studyOnlyContributorsCanRequestAnalysis": "Oba prispievatelia k tejto štúdii môžu požiadať a počítačovú analýzu.", + "studyGetAFullComputerAnalysis": "Získajte úplnú počítačovú analýzu hlavného variantu na strane servera.", + "studyMakeSureTheChapterIsComplete": "Uistite sa, že kapitola je kompletná. Požiadať o analýzu môžete iba raz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Všetci zosynchronizovaní členovia uvidia rovnakú pozíciu", + "studyShareChanges": "Zdieľajte zmeny s divákmi a uložte ich na server", + "studyPlaying": "Práve sa hrá", + "studyShowEvalBar": "Ukazovatele hodnotenia", + "studyFirst": "Prvá", + "studyPrevious": "Späť", + "studyNext": "Ďalej", + "studyLast": "Posledná", "studyShareAndExport": "Zdielať & export", - "studyStart": "Štart" + "studyCloneStudy": "Naklonovať", + "studyStudyPgn": "PGN štúdie", + "studyDownloadAllGames": "Stiahnuť všetky partie", + "studyChapterPgn": "PGN kapitoly", + "studyCopyChapterPgn": "Kopírovať PGN", + "studyDownloadGame": "Stiahnúť hru", + "studyStudyUrl": "URL štúdie", + "studyCurrentChapterUrl": "URL aktuálnej kapitoly", + "studyYouCanPasteThisInTheForumToEmbed": "Vložte pre zobrazenie vo fóre", + "studyStartAtInitialPosition": "Začať zo základného postavenia", + "studyStartAtX": "Začať na {param}", + "studyEmbedInYourWebsite": "Vložte na svoju webstránku alebo blog", + "studyReadMoreAboutEmbedding": "Prečítajte si viac o vkladaní", + "studyOnlyPublicStudiesCanBeEmbedded": "Vložené môžu byť iba verejné štúdie!", + "studyOpen": "Otvoriť", + "studyXBroughtToYouByY": "{param1} Vám priniesol {param2}", + "studyStudyNotFound": "Štúdia sa nenašla", + "studyEditChapter": "Upraviť kapitolu", + "studyNewChapter": "Nová kapitola", + "studyImportFromChapterX": "Importovať z {param}", + "studyOrientation": "Orientácia", + "studyAnalysisMode": "Mód analýzy", + "studyPinnedChapterComment": "Pripnutý komentár ku kapitole", + "studySaveChapter": "Uložiť kapitolu", + "studyClearAnnotations": "Vymazať anotácie", + "studyClearVariations": "Vymazať varianty", + "studyDeleteChapter": "Vymazať kapitolu", + "studyDeleteThisChapter": "Chcete vymazať túto kapitolu? Táto akcia sa nedá vratit späť!", + "studyClearAllCommentsInThisChapter": "Vymazať všetky komentáre, piktogramy a nakreslené tvary v tejto kapitole?", + "studyRightUnderTheBoard": "Pod hraciu dosku", + "studyNoPinnedComment": "Nikam", + "studyNormalAnalysis": "Normálna analýza", + "studyHideNextMoves": "Skryť nasledujuce ťahy", + "studyInteractiveLesson": "Interaktívna lekcia", + "studyChapterX": "Kapitola {param}", + "studyEmpty": "Prázdne", + "studyStartFromInitialPosition": "Začať z počiatočnej pozície", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Začať z vlastnej pozície", + "studyLoadAGameByUrl": "Načítať hru z URL", + "studyLoadAPositionFromFen": "Načítať pozíciu z FEN", + "studyLoadAGameFromPgn": "Načítať hru z PGN", + "studyAutomatic": "Automatická", + "studyUrlOfTheGame": "URL hry", + "studyLoadAGameFromXOrY": "Načítať hru z {param1} alebo z {param2}", + "studyCreateChapter": "Vytvoriť kapitolu", + "studyCreateStudy": "Vytvoriť štúdiu", + "studyEditStudy": "Upraviť učivo", + "studyVisibility": "Viditeľnosť", + "studyPublic": "Verejné", + "studyUnlisted": "Nezapísané", + "studyInviteOnly": "Iba na pozvanie", + "studyAllowCloning": "Povoliť klonovanie", + "studyNobody": "Nikto", + "studyOnlyMe": "Iba ja", + "studyContributors": "Prispievatelia", + "studyMembers": "Členovia", + "studyEveryone": "Všetci", + "studyEnableSync": "Povoliť synchronizáciu", + "studyYesKeepEveryoneOnTheSamePosition": "Ano: ponechajte každého na tej istej pozícii", + "studyNoLetPeopleBrowseFreely": "No: povoľte ľudom voľne prehľadávať", + "studyPinnedStudyComment": "Pripnutý komentár k učivu", + "studyStart": "Štart", + "studySave": "Uložiť", + "studyClearChat": "Vymazať čet", + "studyDeleteTheStudyChatHistory": "Definitívne vymazať históriu četu k tejto štúdii? Táto akcia sa nedá vrátit späť!", + "studyDeleteStudy": "Vymazať štúdiu", + "studyConfirmDeleteStudy": "Definitívne vymazať štúdiu? Táto akcia sa nedá vrátit späť! Napíšte názov štúdie pre potvrdenie: {param}", + "studyWhereDoYouWantToStudyThat": "Kde to chcete študovať?", + "studyGoodMove": "Dobrý ťah", + "studyMistake": "Chyba", + "studyBrilliantMove": "Veľmi dobrý ťah", + "studyBlunder": "Hrubá chyba", + "studyInterestingMove": "Zaujímavý ťah", + "studyDubiousMove": "Pochybný ťah", + "studyOnlyMove": "Jediný možný ťah", + "studyZugzwang": "Nevýhoda ťahu", + "studyEqualPosition": "Rovnocenná pozícia", + "studyUnclearPosition": "Nejasná pozícia", + "studyWhiteIsSlightlyBetter": "Biely stojí o trochu lepšie", + "studyBlackIsSlightlyBetter": "Čierny stojí o trochu lepšie", + "studyWhiteIsBetter": "Biely stojí lepšie", + "studyBlackIsBetter": "Čierny stojí lepšie", + "studyWhiteIsWinning": "Biely stojí na výhru", + "studyBlackIsWinning": "Čierny stojí na výhru", + "studyNovelty": "Novinka", + "studyDevelopment": "Vývin", + "studyInitiative": "Iniciatíva", + "studyAttack": "Útok", + "studyCounterplay": "Protiútok", + "studyTimeTrouble": "Časová tieseň", + "studyWithCompensation": "S výhodou", + "studyWithTheIdea": "S myšlienkou", + "studyNextChapter": "Ďalšia kapitola", + "studyPrevChapter": "Predchádzajúca kapitola", + "studyStudyActions": "Úkony pri štúdii", + "studyTopics": "Témy", + "studyMyTopics": "Moje témy", + "studyPopularTopics": "Populárne témy", + "studyManageTopics": "Spravovať témy", + "studyBack": "Späť", + "studyPlayAgain": "Hrať znova", + "studyWhatWouldYouPlay": "Čo by ste hrali v tejto pozícii?", + "studyYouCompletedThisLesson": "Gratulujeme! Túto lekciu ste ukončili.", + "studyPerPage": "{param} na stránku", + "studyNbChapters": "{count, plural, =1{{count} Kapitola} few{{count} Kapitoly} many{{count} Kapitol} other{{count} Kapitol}}", + "studyNbGames": "{count, plural, =1{{count} Partia} few{{count} Partie} many{{count} Partií} other{{count} Partií}}", + "studyNbMembers": "{count, plural, =1{{count} Člen} few{{count} Členovia} many{{count} Členov} other{{count} Členov}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Váš PGN text vložte sem, maximálne {count} partiu} few{Váš PGN text vložte sem, maximálne {count} partie} many{Váš PGN text vložte sem, maximálne {count} partií} other{Váš PGN text vložte sem, maximálne {count} partií}}", + "timeagoJustNow": "práve teraz", + "timeagoRightNow": "práve teraz", + "timeagoCompleted": "ukončené", + "timeagoInNbSeconds": "{count, plural, =1{o {count} sekundu} few{o {count} sekundy} many{o {count} sekúnd} other{o {count} sekúnd}}", + "timeagoInNbMinutes": "{count, plural, =1{o {count} minútu} few{o {count} minút} many{o {count} minút} other{o {count} minút}}", + "timeagoInNbHours": "{count, plural, =1{o {count} hodinu} few{o {count} hodiny} many{o {count} hodín} other{o {count} hodín}}", + "timeagoInNbDays": "{count, plural, =1{o {count} deň} few{o {count} dni} many{o {count} dní} other{o {count} dní}}", + "timeagoInNbWeeks": "{count, plural, =1{o {count} týždeň} few{o {count} týždne} many{o {count} týždňov} other{o {count} týždňov}}", + "timeagoInNbMonths": "{count, plural, =1{o {count} mesiac} few{o {count} mesiace} many{o {count} mesiacov} other{o {count} mesiacov}}", + "timeagoInNbYears": "{count, plural, =1{o {count} rok} few{o {count} roky} many{o {count} rokov} other{o {count} rokov}}", + "timeagoNbMinutesAgo": "{count, plural, =1{pred {count} minútou} few{pred {count} minútami} many{pred {count} minútami} other{pred {count} minútami}}", + "timeagoNbHoursAgo": "{count, plural, =1{pred {count} hodinou} few{pred {count} hodinami} many{pred {count} hodinami} other{pred {count} hodinami}}", + "timeagoNbDaysAgo": "{count, plural, =1{pred {count} dňom} few{pred {count} dňami} many{pred {count} dňami} other{pred {count} dňami}}", + "timeagoNbWeeksAgo": "{count, plural, =1{pred {count} týždňom} few{pred {count} týždňami} many{pred {count} týždňami} other{pred {count} týždňami}}", + "timeagoNbMonthsAgo": "{count, plural, =1{pred {count} mesiacom} few{pred {count} mesiacmi} many{pred {count} mesiacmi} other{pred {count} mesiacmi}}", + "timeagoNbYearsAgo": "{count, plural, =1{pred {count} rokom} few{pred {count} rokmi} many{pred {count} rokmi} other{pred {count} rokmi}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{ostáva {count} minúta} few{ostávajú {count} minúty} many{ostáva {count} minút} other{ostáva {count} minút}}", + "timeagoNbHoursRemaining": "{count, plural, =1{ostáva {count} hodina} few{ostávajú {count} hodiny} many{ostáva {count} hodín} other{ostáva {count} hodín}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sl.arb b/lib/l10n/lila_sl.arb index 37501bdd02..483f68f94a 100644 --- a/lib/l10n/lila_sl.arb +++ b/lib/l10n/lila_sl.arb @@ -1,9 +1,21 @@ { - "mobileShowResult": "Pokaži rezultat", - "mobilePuzzleThemesSubtitle": "Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.", - "mobilePuzzleStormSubtitle": "V 3 minutah rešite čim več ugank.", + "mobileBlindfoldMode": "Šah z zavezanimi očmi", + "mobileFeedbackButton": "Povratne informacije", "mobileGreeting": "Pozdravljeni {param}", "mobileGreetingWithoutName": "Živjo", + "mobileHomeTab": "Domov", + "mobileMustBeLoggedIn": "Predenj lahko dostopaš do te strani, se je potrebno prijaviti.", + "mobileOkButton": "OK", + "mobilePrefMagnifyDraggedPiece": "Povečaj vlečeno figuro", + "mobilePuzzleStormSubtitle": "V 3 minutah rešite čim več ugank.", + "mobilePuzzleThemesSubtitle": "Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.", + "mobilePuzzlesTab": "Problemi", + "mobileSettingsTab": "Nastavitve", + "mobileShowResult": "Pokaži rezultat", + "mobileSystemColors": "Barve sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Orodja", + "mobileWatchTab": "Glej", "activityActivity": "Aktivnost", "activityHostedALiveStream": "Gostil prenos v živo", "activityRankedInSwissTournament": "Uvrščen #{param1} v {param2}", @@ -16,6 +28,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Potegnil {count} potezo} =2{Potegnil {count} potezi} few{Potegnil {count} poteze} other{Potegnil {count} potez}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} dopisni partiji} =2{v {count} dopisnih partijah} few{v {count} dopisnih partijah} other{v {count} dopisnih partijah}}", "activityCompletedNbGames": "{count, plural, =1{Končal {count} dopisno partijo} =2{Končal {count} dopisni partiji} few{Končal {count} dopisne partije} other{Končal {count} dopisnih partij}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Dokončana {count} {param2} korespondenčna igra} =2{Dokončani {count} {param2} korespondenčni igri} few{Dokončane {count} {param2} korespondenčne igre} other{Dokončanih {count} {param2} korespondenčnih iger}}", "activityFollowedNbPlayers": "{count, plural, =1{Sledi {count} igralcu} =2{Sledi {count} igralcem} few{Sledi {count} igralcem} other{Sledi {count} igralcem}}", "activityGainedNbFollowers": "{count, plural, =1{Ima {count} novega sledilca} =2{Ima {count} nova sledilca} few{Ima {count} nove sledilce} other{Ima {count} novih sledilcev}}", "activityHostedNbSimuls": "{count, plural, =1{Gostil {count} simultanko} =2{Gostil {count} simultanki} few{Gostil {count} simultanke} other{Gostil {count} simultank}}", @@ -26,7 +39,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Tekmoval na {count} švicarskem turnirju} =2{Tekmoval na {count} švicarskih turnirjih} few{Tekmoval na {count} švicarskih turnirjih} other{Tekmoval na {count} švicarskih turnirjih}}", "activityJoinedNbTeams": "{count, plural, =1{Pridružen {count} ekipi} =2{Pridružen {count} ekipam} few{Pridružen {count} ekipam} other{Pridružen {count} ekipam}}", "broadcastBroadcasts": "Prenosi", + "broadcastMyBroadcasts": "Moje oddajanja", "broadcastLiveBroadcasts": "Prenos turnirjev v živo", + "broadcastBroadcastCalendar": "Koledar oddaj", + "broadcastNewBroadcast": "Nov prenos v živo", + "broadcastSubscribedBroadcasts": "Naročene oddaje", + "broadcastAboutBroadcasts": "O oddaji", + "broadcastHowToUseLichessBroadcasts": "Kako uporabljati Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "Novi krog bo imel iste člane in sodelavce kot prejšnji.", + "broadcastAddRound": "Dodajte krog", + "broadcastOngoing": "V teku", + "broadcastUpcoming": "Prihajajoči", + "broadcastCompleted": "Zaključeno", + "broadcastCompletedHelp": "Lichess zazna zaključek kroga na podlagi izvornih iger. Uporabite ta preklop, če ni vira.", + "broadcastRoundName": "Ime kroga", + "broadcastRoundNumber": "Številka kroga", + "broadcastTournamentName": "Turnirsko ime", + "broadcastTournamentDescription": "Kratek opis turnirja", + "broadcastFullDescription": "Polni opis dogodka", + "broadcastFullDescriptionHelp": "Neobvezen dolg opis prenosa. {param1} je na voljo. Dolžina mora biti manjša od {param2} znakov.", + "broadcastSourceSingleUrl": "Vir partije v PGN formatu", + "broadcastSourceUrlHelp": "URL, ki ga bo Lichess preveril, da bo prejel PGN posodobitve. Javno mora biti dostopen preko interneta.", + "broadcastStartDateTimeZone": "Začetni datum v lokalnem časovnem pasu turnirja: {param}", + "broadcastStartDateHelp": "Izbirno, če veste, kdaj se dogodek začne", + "broadcastCurrentGameUrl": "URL trenutno igrane igre", + "broadcastDownloadAllRounds": "Prenesite vse kroge", + "broadcastResetRound": "Ponastavi ta krog", + "broadcastDeleteRound": "Izbriši ta krog", + "broadcastDefinitivelyDeleteRound": "Dokončno izbrišite krog in njegove igre.", + "broadcastDeleteAllGamesOfThisRound": "Izbriši vse igre tega kroga. Vir bo moral biti aktiven, da jih lahko znova ustvarite.", + "broadcastEditRoundStudy": "Uredi krog študije", + "broadcastDeleteTournament": "Zbrišite ta turnir", + "broadcastDefinitivelyDeleteTournament": "Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre.", + "broadcastShowScores": "Prikaži rezultate igralcev na podlagi rezultatov igre", + "broadcastReplacePlayerTags": "Izbirno: zamenjajte imena igralcev, ratinge in nazive", + "broadcastOfficialStandings": "Uradna lestvica", + "broadcastAllTeams": "Vse ekipe", + "broadcastTournamentFormat": "Oblika turnirja", + "broadcastTournamentLocation": "Lokacija turnirja", + "broadcastTopPlayers": "Najboljši igralci", + "broadcastTimezone": "Časovni pas", + "broadcastFideRatingCategory": "FIDE rating kategorija", + "broadcastOptionalDetails": "Neobvezne podrobnosti", + "broadcastPastBroadcasts": "Pretekle oddaje", + "broadcastAllBroadcastsByMonth": "Oglejte si vse oddaje po mesecih", + "broadcastNbBroadcasts": "{count, plural, =1{{count} oddaja} =2{{count} oddaji} few{{count} oddaje} other{{count} oddaj}}", "challengeChallengesX": "Izzivi:{param1}", "challengeChallengeToPlay": "Izzovi na partijo", "challengeChallengeDeclined": "Izziv zavrnjen", @@ -147,6 +204,7 @@ "preferencesNotifyWeb": "Brskalnik", "preferencesNotifyDevice": "Naprava", "preferencesBellNotificationSound": "Zvok obvestila zvonca", + "preferencesBlindfold": "Šah z zavezanimi očmi", "puzzlePuzzles": "Šahovski problemi", "puzzlePuzzleThemes": "Teme ugank", "puzzleRecommended": "Priporočeno", @@ -342,8 +400,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ali brani polje skozi nasprotnikovo figuro.", "puzzleThemeZugzwang": "Nujnica", "puzzleThemeZugzwangDescription": "Nasprotnik ima omejene poteze in vsaka poslabša njegovo pozicijo.", - "puzzleThemeHealthyMix": "Zdrava mešanica", - "puzzleThemeHealthyMixDescription": "Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.", + "puzzleThemeMix": "Zdrava mešanica", + "puzzleThemeMixDescription": "Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.", "puzzleThemePlayerGames": "Igralske igre", "puzzleThemePlayerGamesDescription": "Iskanje ugank, ustvarjenih iz vaših iger ali iz iger drugega igralca.", "puzzleThemePuzzleDownloadInformation": "Te uganke so v javni lasti in jih je mogoče prenesti s spletnega mesta {param}.", @@ -464,16 +522,15 @@ "replayMode": "Način predvajanja", "realtimeReplay": "Realnočasovno", "byCPL": "Za stotinko kmeta", - "openStudy": "Odpri študij", "enable": "Omogoči", "bestMoveArrow": "Puščica najboljše poteze", + "showVariationArrows": "Prikaži puščice z variacijami", "evaluationGauge": "Kazalnik ocene", "multipleLines": "Več variant", "cpus": "CPU-ji", "memory": "Spomin", "infiniteAnalysis": "Neskončna analiza", "removesTheDepthLimit": "Odstrani omejitev globine in ohrani računalnik topel", - "engineManager": "Vodja motorja", "blunder": "Spodrsljaj", "mistake": "Napaka", "inaccuracy": "Nenatančnost", @@ -499,6 +556,7 @@ "latestForumPosts": "Zadnje objave na forumu", "players": "Igralci", "friends": "Prijatelji", + "otherPlayers": "drugi igralci", "discussions": "Pogovori", "today": "Danes", "yesterday": "Včeraj", @@ -554,6 +612,7 @@ "rank": "Uvrstitev", "rankX": "Uvrstitev: {param}", "gamesPlayed": "Odigranih iger", + "ok": "V redu", "cancel": "Prekliči", "whiteTimeOut": "Belemu se je čas iztekel", "blackTimeOut": "Črnemu se je čas iztekel", @@ -670,7 +729,6 @@ "block": "Blokiraj", "blocked": "Blokiran", "unblock": "Odblokiraj", - "followsYou": "Sledi vam", "xStartedFollowingY": "{param1} je začel slediti {param2}", "more": "Več", "memberSince": "Član od", @@ -729,6 +787,7 @@ "ifNoneLeaveEmpty": "Če ni, pustite prazno", "profile": "Profil", "editProfile": "Uredi profil", + "realName": "Pravo ime", "setFlair": "Določite svoj okus", "flair": "Simbol", "youCanHideFlair": "Obstaja nastavitev za skrivanje vseh uporabniških čustev na celotnem spletnem mestu.", @@ -755,6 +814,8 @@ "descPrivateHelp": "Besedilo, ki ga bodo videli samo člani ekipe. Če je nastavljen, nadomesti javni opis za člane ekipe.", "no": "Ne", "yes": "Da", + "website": "Spletna stran", + "mobile": "Mobilna aplikacija", "help": "Pomoč:", "createANewTopic": "Ustvari novo temo", "topics": "Teme", @@ -773,7 +834,7 @@ "cheat": "Goljufija", "troll": "Provokacija", "other": "Drugo", - "reportDescriptionHelp": "Prilepite povezave do igre (ali iger) in pojasnite kaj je narobe z obnašanjem uporabnika. Ne napišite samo \"uporabnik goljufa\" temveč pojasnite zakaj mislite tako. Prijava bo obdelana hitreje če bo napisana v angleščini.", + "reportCheatBoostHelp": "Prilepite povezavo do igre (ali iger) in pojasnite, kaj je narobe z nasprotnikovim načinom igranja. Ne napišite le, da \"nasprotnik goljufa\", ampak pojasnite, kako ste prišli do te ugotovitve.", "error_provideOneCheatedGameLink": "Navedite vsaj eno povezavo do igre s primerom goljufanja.", "by": "od {param}", "importedByX": "Uvozil je {param}", @@ -1255,6 +1316,177 @@ "stormXRuns": "{count, plural, =1{1 poskus} =2{{count} poskusa} few{{count} poskusi} other{{count} poskusov}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Opravljen en poskus {param2}} =2{Opravljena {count} poskusa {param2}} few{Opravljeni {count} poskusi {param2}} other{Opravljenih {count} poskusov {param2}}}", "streamerLichessStreamers": "Lichess voditelji prenosa", + "studyPrivate": "Zasebno", + "studyMyStudies": "Moje študije", + "studyStudiesIContributeTo": "Študije h katerim prispevam", + "studyMyPublicStudies": "Moje javne študije", + "studyMyPrivateStudies": "Moje zasebne študije", + "studyMyFavoriteStudies": "Moje najljubše študije", + "studyWhatAreStudies": "Kaj so študije?", + "studyAllStudies": "Vse študije", + "studyStudiesCreatedByX": "Študije, ki jih je ustvaril {param}", + "studyNoneYet": "Še nič.", + "studyHot": "Vroče", + "studyDateAddedNewest": "Dodano (novejše)", + "studyDateAddedOldest": "Dodano (starejše)", + "studyRecentlyUpdated": "Nazadnje objavljeno", + "studyMostPopular": "Najbolj popularno", + "studyAlphabetical": "Po abecednem redu", + "studyAddNewChapter": "Dodaj poglavje", + "studyAddMembers": "Dodaj člane", + "studyInviteToTheStudy": "Povabi na študijo", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosimo, povabite samo tiste ljudi, ki jih poznate in ki bi se želeli pridružiti tej študiji.", + "studySearchByUsername": "Iskanje po uporabniškem imenu", + "studySpectator": "Opazovalec", + "studyContributor": "Sodelovalec", + "studyKick": "Odstrani", + "studyLeaveTheStudy": "Zapusti študijo", + "studyYouAreNowAContributor": "Ste nov sodelovalec", + "studyYouAreNowASpectator": "Sedaj ste opazovalec", + "studyPgnTags": "PGN oznake", + "studyLike": "Všečkaj", + "studyUnlike": "Ni mi všeč", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentiraj to pozicijo", + "studyCommentThisMove": "Komentiraj to potezo", + "studyAnnotateWithGlyphs": "Označi s simbolom", + "studyTheChapterIsTooShortToBeAnalysed": "To poglavje je prekratko, da bi se analiziralo.", + "studyOnlyContributorsCanRequestAnalysis": "Samo sodelovalci v študiji lahko zahtevajo računalniško analizo.", + "studyGetAFullComputerAnalysis": "Pridobi na računalniškem strežniku izvedeno računalniško analizo glavne varjante.", + "studyMakeSureTheChapterIsComplete": "Poskrbite, da bo poglavje zaključeno. Analizo lahko zahtevate samo enkrat.", + "studyAllSyncMembersRemainOnTheSamePosition": "Vsi sinhronizirani člani so v isti poziciji", + "studyShareChanges": "Deli spremembe z gledalci in jih shrani na strežnik", + "studyPlaying": "V teku", + "studyFirst": "Prva stran", + "studyPrevious": "Prejšnja stran", + "studyNext": "Naslednja stran", + "studyLast": "Zadnja stran", "studyShareAndExport": "Deli in Izvozi podatke", - "studyStart": "Začni" + "studyCloneStudy": "Kloniraj", + "studyStudyPgn": "PGN študije", + "studyDownloadAllGames": "Prenesi vse igre", + "studyChapterPgn": "PGN poglavja", + "studyCopyChapterPgn": "Kopiraj PGN", + "studyDownloadGame": "Prenesi igro", + "studyStudyUrl": "URL študije", + "studyCurrentChapterUrl": "URL trenutnega poglavja", + "studyYouCanPasteThisInTheForumToEmbed": "To lahko prilepite na forum, da vstavite", + "studyStartAtInitialPosition": "Začni v začetni poziciji", + "studyStartAtX": "Začni z {param}", + "studyEmbedInYourWebsite": "Vstavite v vašo spletno stran ali blog", + "studyReadMoreAboutEmbedding": "Preberite več o vstavljanju", + "studyOnlyPublicStudiesCanBeEmbedded": "Vdelati je mogoče le javni študij!", + "studyOpen": "Odpri", + "studyXBroughtToYouByY": "{param1} vam ponuja {param2}", + "studyStudyNotFound": "Študije nismo našli", + "studyEditChapter": "Uredi poglavje", + "studyNewChapter": "Novo poglavje", + "studyImportFromChapterX": "Uvozi iz {param}", + "studyOrientation": "Smer", + "studyAnalysisMode": "Analizni način", + "studyPinnedChapterComment": "Pripet komentar poglavja", + "studySaveChapter": "Shrani poglavje", + "studyClearAnnotations": "Zbriši oznake", + "studyClearVariations": "Izbriši variante", + "studyDeleteChapter": "Izbriši poglavje", + "studyDeleteThisChapter": "Izbriši to poglavje? Poti nazaj ni več!", + "studyClearAllCommentsInThisChapter": "Izbriši vse komentarje in oblike v tem poglavju?", + "studyRightUnderTheBoard": "Takoj pod šahovnico", + "studyNoPinnedComment": "Brez", + "studyNormalAnalysis": "Običajna analiza", + "studyHideNextMoves": "Skrij naslednje poteze", + "studyInteractiveLesson": "Interaktivne lekcije", + "studyChapterX": "Poglavje: {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Začni v začetni poziciji", + "studyEditor": "Urejevalnik", + "studyStartFromCustomPosition": "Začni v prilagojeni poziciji", + "studyLoadAGameByUrl": "Naloži partijo iz URL", + "studyLoadAPositionFromFen": "Naloži pozicijo iz FEN", + "studyLoadAGameFromPgn": "Naloži partijo iz PGN", + "studyAutomatic": "Samodejno", + "studyUrlOfTheGame": "URL igre", + "studyLoadAGameFromXOrY": "Naloži partijo iz {param1} ali {param2}", + "studyCreateChapter": "Ustvari poglavje", + "studyCreateStudy": "Ustvarite študijo", + "studyEditStudy": "Uredite študijo", + "studyVisibility": "Vidnost", + "studyPublic": "Javno", + "studyUnlisted": "Ni na seznamu", + "studyInviteOnly": "Samo na povabilo", + "studyAllowCloning": "Dovoli kloniranje", + "studyNobody": "Nihče", + "studyOnlyMe": "Samo jaz", + "studyContributors": "Prispevali so", + "studyMembers": "Člani", + "studyEveryone": "Kdorkoli", + "studyEnableSync": "Omogoči sinhronizacijo", + "studyYesKeepEveryoneOnTheSamePosition": "Da: vse obdrži v isti poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: naj uporabniki prosto raziskujejo", + "studyPinnedStudyComment": "Označen komentar študije", + "studyStart": "Začni", + "studySave": "Shrani", + "studyClearChat": "Počisti klepet", + "studyDeleteTheStudyChatHistory": "Brisanje zgodovine klepeta? Poti nazaj več ni!", + "studyDeleteStudy": "Izbriši študijo", + "studyConfirmDeleteStudy": "Želite izbrisati celotno študijo? Ni poti nazaj! Za potrditev vnesite ime študije: {param}", + "studyWhereDoYouWantToStudyThat": "Kje želite to študirati?", + "studyGoodMove": "Dobra poteza", + "studyMistake": "Napakica", + "studyBrilliantMove": "Briljantna poteza", + "studyBlunder": "Napaka", + "studyInterestingMove": "Zanimiva poteza", + "studyDubiousMove": "Dvomljiva poteza", + "studyOnlyMove": "Edina poteza", + "studyZugzwang": "Nujnica", + "studyEqualPosition": "Enaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Beli je nekoliko boljši", + "studyBlackIsSlightlyBetter": "Črni je nekoliko boljši", + "studyWhiteIsBetter": "Beli je boljši", + "studyBlackIsBetter": "Črni je boljši", + "studyWhiteIsWinning": "Beli zmaguje", + "studyBlackIsWinning": "Črni zmaguje", + "studyNovelty": "Novost", + "studyDevelopment": "Razvoj", + "studyInitiative": "Iniciativa", + "studyAttack": "Napad", + "studyCounterplay": "Protinapad", + "studyTimeTrouble": "Časovna stiska", + "studyWithCompensation": "S kompenzacijo", + "studyWithTheIdea": "Z idejo", + "studyNextChapter": "Naslednje poglavje", + "studyPrevChapter": "Prejšnje poglavje", + "studyStudyActions": "Študijske akcije", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Priljubljene teme", + "studyManageTopics": "Upravljaj teme", + "studyBack": "Nazaj", + "studyPlayAgain": "Igrajte ponovno", + "studyWhatWouldYouPlay": "Kaj bi igrali v tem položaju?", + "studyYouCompletedThisLesson": "Čestitke! Končali ste to lekcijo.", + "studyPerPage": "{param} na stran", + "studyNbChapters": "{count, plural, =1{{count} Poglavje} =2{{count} Poglavji} few{{count} Poglavja} other{{count} poglavij}}", + "studyNbGames": "{count, plural, =1{{count} Partija} =2{{count} Partiji} few{{count} Partije} other{{count} Partij}}", + "studyNbMembers": "{count, plural, =1{{count} Član} =2{{count} Člana} few{{count} Člani} other{{count} Članov}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Prilepite PGN besedilo, z največ {count} partijo} =2{Prilepite PGN besedilo, z največ {count} partijama} few{Prilepite PGN besedilo, z največ {count} partijami} other{Prilepite PGN besedilo, z največ {count} partijami}}", + "timeagoJustNow": "pravkar", + "timeagoRightNow": "ta trenutek", + "timeagoCompleted": "končano", + "timeagoInNbSeconds": "{count, plural, =1{čez {count} sekund} =2{čez {count} sekundi} few{čez {count} sekunde} other{čez {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{čez {count} minuto} =2{čez {count} minuti} few{čez {count} minute} other{čez {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{čez {count} uro} =2{čez {count} uri} few{čez {count} ure} other{čez {count} ur}}", + "timeagoInNbDays": "{count, plural, =1{čez {count} dan} =2{čez {count} dneva} few{čez {count} dnevih} other{čez {count} dni}}", + "timeagoInNbWeeks": "{count, plural, =1{čez {count} teden} =2{čez {count} tedna} few{čez {count} tedne} other{čez {count} tednov}}", + "timeagoInNbMonths": "{count, plural, =1{čez {count} mesec} =2{čez {count} meseca} few{čez {count} mesece} other{čez {count} mesecev}}", + "timeagoInNbYears": "{count, plural, =1{čez {count} leto} =2{čez {count} leti} few{čez {count} leta} other{čez {count} let}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Pred {count} minuto} =2{Pred {count} minutama} few{Pred {count} minutami} other{Pred {count} minutami}}", + "timeagoNbHoursAgo": "{count, plural, =1{Pred {count} uro} =2{Pred {count} urama} few{Pred {count} urami} other{Pred {count} urami}}", + "timeagoNbDaysAgo": "{count, plural, =1{Pred {count} dnevom} =2{Pred {count} dnevoma} few{Pred {count} dnevi} other{Pred {count} dnevi}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Pred {count} tednom} =2{Pred {count} tednoma} few{Pred {count} tedni} other{Pred {count} tedni}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Pred {count} mesecem} =2{Pred {count} mesecema} few{Pred {count} meseci} other{Pred {count} meseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{Pred {count} letom} =2{Pred {count} letoma} few{Pred {count} leti} other{Pred {count} leti}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{še {count} minuta} =2{še {count} minuti} few{še {count} minute} other{še {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{še {count} ura} =2{še {count} uri} few{še {count} ure} other{še {count} ur}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sq.arb b/lib/l10n/lila_sq.arb index e3d0905673..7d3f7a9259 100644 --- a/lib/l10n/lila_sq.arb +++ b/lib/l10n/lila_sq.arb @@ -1,44 +1,46 @@ { + "mobileAllGames": "Krejt lojërat", + "mobileAreYouSure": "Jeni i sigurt?", + "mobileBlindfoldMode": "Me sytë lidhur", + "mobileCancelTakebackOffer": "Anulojeni ofertën për prapakthim", + "mobileClearButton": "Spastroje", + "mobileCorrespondenceClearSavedMove": "Spastroje lëvizjen e ruajtur", + "mobileCustomGameJoinAGame": "Merrni pjesë në një lojë", + "mobileFeedbackButton": "Përshtypje", + "mobileGreeting": "Tungjatjeta, {param}", + "mobileGreetingWithoutName": "Tungjatjeta", + "mobileHideVariation": "Fshihe variantin", "mobileHomeTab": "Kreu", - "mobileToolsTab": "Mjete", - "mobileWatchTab": "Shiheni", - "mobileSettingsTab": "Rregullime", + "mobileLiveStreamers": "Transmetues drejtpërsëdrejti", "mobileMustBeLoggedIn": "Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.", - "mobileSystemColors": "Ngjyra sistemi", - "mobileFeedbackButton": "Përshtypje", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Dridhje gjatë lëvizjesh", - "mobileSettingsImmersiveModeSubtitle": "Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.", + "mobileNoSearchResults": "S’ka përfundime", "mobileNotFollowingAnyUser": "S’ndiqni ndonjë përdorues.", - "mobileAllGames": "Krejt lojërat", - "mobileRecentSearches": "Kërkime së fundi", - "mobileClearButton": "Spastroje", + "mobileOkButton": "OK", "mobilePlayersMatchingSearchTerm": "Lojëtarë me “{param}”", - "mobileNoSearchResults": "S’ka përfundime", - "mobileAreYouSure": "Jeni i sigurt?", + "mobilePrefMagnifyDraggedPiece": "Zmadho gurin e tërhequr", + "mobilePuzzleStormConfirmEndRun": "Doni të përfundohen ku raund?", + "mobilePuzzleStormFilterNothingToShow": "S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat", "mobilePuzzleStormNothingToShow": "S’ka gjë për shfaqje. Luani ndonjë raund Puzzle Storm.", - "mobileSharePuzzle": "Ndajeni këtë ushtrim me të tjerët", - "mobileShareGameURL": "Ndani URL loje me të tjerë", + "mobilePuzzleStormSubtitle": "Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.", + "mobilePuzzleThemesSubtitle": "Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.", + "mobilePuzzlesTab": "Ushtrime", + "mobileRecentSearches": "Kërkime së fundi", + "mobileSettingsHapticFeedback": "Dridhje gjatë lëvizjesh", + "mobileSettingsImmersiveModeSubtitle": "Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.", + "mobileSettingsTab": "Rregullime", "mobileShareGamePGN": "Ndani PGN me të tjerë", + "mobileShareGameURL": "Ndani URL loje me të tjerë", "mobileSharePositionAsFEN": "Tregojuni të tjerëve pozicionin si FEN", - "mobileShowVariations": "Shfaq variante", - "mobileHideVariation": "Fshihe variantin", + "mobileSharePuzzle": "Ndajeni këtë ushtrim me të tjerët", "mobileShowComments": "Shfaq komente", - "mobilePuzzleStormConfirmEndRun": "Doni të përfundohen ku raund?", - "mobilePuzzleStormFilterNothingToShow": "S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat", - "mobileCancelTakebackOffer": "Anulojeni ofertën për prapakthim", - "mobileCancelDrawOffer": "Anulojeni ofertën për barazim", - "mobileWaitingForOpponentToJoin": "Po pritet që të vijë kundërshtari…", - "mobileBlindfoldMode": "Me sytë lidhur", - "mobileLiveStreamers": "Transmetues drejtpërsëdrejti", - "mobileCustomGameJoinAGame": "Merrni pjesë në një lojë", - "mobileCorrespondenceClearSavedMove": "Spastroje lëvizjen e ruajtur", - "mobileSomethingWentWrong": "Diç shkoi ters.", "mobileShowResult": "Shfaq përfundimin", - "mobilePuzzleThemesSubtitle": "Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.", - "mobilePuzzleStormSubtitle": "Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.", - "mobileGreeting": "Tungjatjeta, {param}", - "mobileGreetingWithoutName": "Tungjatjeta", + "mobileShowVariations": "Shfaq variante", + "mobileSomethingWentWrong": "Diç shkoi ters.", + "mobileSystemColors": "Ngjyra sistemi", + "mobileTheme": "Temë", + "mobileToolsTab": "Mjete", + "mobileWaitingForOpponentToJoin": "Po pritet që të vijë kundërshtari…", + "mobileWatchTab": "Shiheni", "activityActivity": "Aktiviteti", "activityHostedALiveStream": "Priti një transmetim të drejtpërdrejtë", "activityRankedInSwissTournament": "Renditur #{param1} në {param2}", @@ -61,8 +63,80 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Konkuroi në turneun zviceran {count}} other{Ndeshur në {count} turne zviceranë}}", "activityJoinedNbTeams": "{count, plural, =1{U bashkua me ekipin {count}} other{U bë pjesë e {count} ekipive}}", "broadcastBroadcasts": "Transmetime", + "broadcastMyBroadcasts": "Transmetimet e mia", "broadcastLiveBroadcasts": "Transmetime të drejtpërdrejta turnesh", - "challengeChallengeToPlay": "Sfidoni në një lojë", + "broadcastBroadcastCalendar": "Kalendar transmetimesh", + "broadcastNewBroadcast": "Transmetim i ri i drejtpërdrejtë", + "broadcastSubscribedBroadcasts": "Transmetime me pajtim", + "broadcastAboutBroadcasts": "Rreth transmetimeve", + "broadcastHowToUseLichessBroadcasts": "Si të përdoren Transmetimet Lichess.", + "broadcastTheNewRoundHelp": "Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi.", + "broadcastAddRound": "Shtoni një raund", + "broadcastOngoing": "Në zhvillim", + "broadcastUpcoming": "I ardhshëm", + "broadcastCompleted": "I mbaruar", + "broadcastCompletedHelp": "Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim.", + "broadcastRoundName": "Emër raundi", + "broadcastRoundNumber": "Numër raundi", + "broadcastTournamentName": "Emër turneu", + "broadcastTournamentDescription": "Përshkrim i shkurtër i turneut", + "broadcastFullDescription": "Përshkrim i plotë i turneut", + "broadcastFullDescriptionHelp": "Përshkrim i gjatë opsional i turneut. {param1} është e disponueshme. Gjatësia duhet të jetë më pak se {param2} shenja.", + "broadcastSourceSingleUrl": "URL Burimi PGN-je", + "broadcastSourceUrlHelp": "URL-ja që do të kontrollojë Lichess-i për të marrë përditësime PGN-sh. Duhet të jetë e përdorshme publikisht që nga Interneti.", + "broadcastSourceGameIds": "Deri në 64 ID lojërash Lichess, ndarë me hapësira.", + "broadcastStartDateTimeZone": "Datë fillimi në zonën kohore vendore të turneut: {param}", + "broadcastStartDateHelp": "Opsionale, nëse e dini kur fillon veprimtaria", + "broadcastCurrentGameUrl": "URL e lojës së tanishme", + "broadcastDownloadAllRounds": "Shkarko krejt raundet", + "broadcastDeleteRound": "Fshije këtë raund", + "broadcastDefinitivelyDeleteRound": "Fshije përfundimisht raundin dhe lojërat e tij.", + "broadcastDeleteAllGamesOfThisRound": "Fshi krejt lojërat e këtij raundi. Burimi do të duhet të jetë aktiv, që të mund të rikrijohen ato.", + "broadcastEditRoundStudy": "Përpunoni analizë raundi", + "broadcastDeleteTournament": "Fshije këtë turne", + "broadcastDefinitivelyDeleteTournament": "Fshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të.", + "broadcastShowScores": "Shfaq pikë lojtatësh bazuar në përfundime lojërash", + "broadcastReplacePlayerTags": "Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj", + "broadcastFideFederations": "Federata FIDE", + "broadcastTop10Rating": "10 vlerësimet kryesuese", + "broadcastFidePlayers": "Lojtarë FIDE", + "broadcastFidePlayerNotFound": "S’u gjet lojtar FIDE", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federim", + "broadcastAgeThisYear": "Moshë këtë vit", + "broadcastUnrated": "Pa pikë", + "broadcastRecentTournaments": "Turne së fundi", + "broadcastOpenLichess": "Hape në Lichess", + "broadcastTeams": "Ekipe", + "broadcastBoards": "Fusha", + "broadcastOverview": "Përmbledhje", + "broadcastSubscribeTitle": "Pajtohuni, që të noftoheni se kur fillon çdo raund. Mund të aktivizoni/çaktivizoni zilen, ose njoftimet “push” për transmetime, që nga parapëlqimet për llogarinë tuaj.", + "broadcastUploadImage": "Ngarkoni figurë turneu", + "broadcastNoBoardsYet": "Ende pa fusha. Këto do të shfaqen sapo të ngrkohen lojërat.", + "broadcastBoardsCanBeLoaded": "Fushat mund të ngarkohen me një burim, ose përmes {param}", + "broadcastStartsAfter": "Fillon pas {param}", + "broadcastStartVerySoon": "Transmetimi do të fillojë shumë shpejt.", + "broadcastNotYetStarted": "Transmetimi s’ka filluar ende.", + "broadcastOfficialWebsite": "Sajti zyrtar", + "broadcastIframeHelp": "Më tepër mundësi te {param}", + "broadcastWebmastersPage": "faqe webmaster-ësh", + "broadcastPgnSourceHelp": "Një burim publik,, PGN, i atypëratyshëm për këtë raund. Ofrojmë gjithashtu edhe një {param}, për njëkohësim më të shpejtë dhe më efikas.", + "broadcastEmbedThisBroadcast": "Trupëzojeni këtë transmetim në sajtin tuaj", + "broadcastEmbedThisRound": "Trupëzojeni {param} në sajtin tuaj", + "broadcastGamesThisTournament": "Lojëra në këtë turne", + "broadcastScore": "Përfundim", + "broadcastAllTeams": "Krejt ekipet", + "broadcastTournamentFormat": "Format turneu", + "broadcastTournamentLocation": "Vendndodhje Turney", + "broadcastTopPlayers": "Lojtarët kryesues", + "broadcastTimezone": "Zonë kohore", + "broadcastFideRatingCategory": "Kategori vlerësimi FIDE", + "broadcastOptionalDetails": "Hollësi opsionale", + "broadcastPastBroadcasts": "Transmetime të kaluara", + "broadcastAllBroadcastsByMonth": "Shihni krejt transmetimet sipas muajsh", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmetim} other{{count} transmetime}}", + "challengeChallengesX": "Sfida: {param1}", + "challengeChallengeToPlay": "Sfidoni me një lojë", "challengeChallengeDeclined": "Sfida u refuzua", "challengeChallengeAccepted": "Sfida u pranua!", "challengeChallengeCanceled": "Sfida u anulua.", @@ -172,12 +246,14 @@ "preferencesNotifyForumMention": "Koment forumi ku përmendeni", "preferencesNotifyInvitedStudy": "Ftesë për ushtrim", "preferencesNotifyGameEvent": "Përditësime loje me korrespondencë", + "preferencesNotifyChallenge": "Sfida", "preferencesNotifyTournamentSoon": "Turne që fillon së shpejti", "preferencesNotifyBell": "Njoftim zileje brenda Lichess-it", "preferencesNotifyPush": "Njoftim pajisjeje kur s’gjendeni në Lichess", "preferencesNotifyWeb": "Shfletues", "preferencesNotifyDevice": "Pajisje", "preferencesBellNotificationSound": "Tingull zileje njoftimesh", + "preferencesBlindfold": "Me sytë lidhur", "puzzlePuzzles": "Ushtrime", "puzzlePuzzleThemes": "Tema ushtrimesh", "puzzleRecommended": "Të rekomanduara", @@ -371,8 +447,8 @@ "puzzleThemeXRayAttackDescription": "Një gur sulmon ose mbron një kuadrat, përmes një guri të kundërshtarit.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Kundërshtari është i kufizuar në lëvizjet që mund të bëjë dhe krejt lëvizjet përkeqësojnë pozicionin e tij.", - "puzzleThemeHealthyMix": "Përzierje e ushtrimeve", - "puzzleThemeHealthyMixDescription": "Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.", + "puzzleThemeMix": "Ushtrime të përzierë", + "puzzleThemeMixDescription": "Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.", "puzzleThemePlayerGames": "Lojëra të lojëtarit", "puzzleThemePuzzleDownloadInformation": "Këto ushtrime janë nën përkatësi publike dhe mund të shkarkohen nga {param}.", "searchSearch": "Kërko", @@ -492,7 +568,6 @@ "replayMode": "Mënyra përsëritje", "realtimeReplay": "Aty për aty", "byCPL": "nga CPL", - "openStudy": "Studim i hapur", "enable": "Aktivizoje", "bestMoveArrow": "Shigjetë e lëvizjes më të mirë", "showVariationArrows": "Shfaq shigjeta variacionesh", @@ -583,6 +658,7 @@ "rank": "Renditje", "rankX": "Renditja: {param}", "gamesPlayed": "Lojëra të luajtura", + "ok": "OK", "cancel": "Anuloje", "whiteTimeOut": "Të bardhit i mbaroi koha", "blackTimeOut": "Të ziut i mbaroi koha", @@ -699,7 +775,6 @@ "block": "Bllokoje", "blocked": "I bllokuar", "unblock": "Zhbllokoje", - "followsYou": "Ju ndjek juve", "xStartedFollowingY": "{param1} nisi të ndjekë {param2}", "more": "Më shumë", "memberSince": "Anëtar që prej", @@ -801,7 +876,9 @@ "cheat": "Hile", "troll": "Troll", "other": "Tjetër", - "reportDescriptionHelp": "Ngjitni lidhjen për te loja(ra) dhe shpjegoni çfarë nuk shkon me sjelljen e këtij përdoruesi. Mos shkruani thjesht “mashtrojnë”, por na tregoni si mbërritët në këtë përfundim. Raportimi juaj do të përpunohet më shpejt, nëse shkruhet në anglisht.", + "reportCheatBoostHelp": "Ngjitni lidhjen për te loja(rat) dhe shpjegoni se ç’nuk shkon me sjelljen e këtij përdoruesi. Mos thoni thjesht “bën me hile”, por na tregoni se si arritët në këtë konkluzion.", + "reportUsernameHelp": "Shpjegoni pse ky emër përdoruesi është fyes. Mos thoni thjesht “është fyes/i papërshtatshëm”, por na tregoni se si arritët në këtë konkluzion, veçanërisht nëse fyerja është e hollë, jo në anglisht, është në një slang, ose është një referencë historike/kulturore.", + "reportProcessedFasterInEnglish": "Raportimi juaj do të shqyrtohet më shpejt, nëse është shkruar në anglisht.", "error_provideOneCheatedGameLink": "Ju lutemi, jepni të paktën një lidhje te një lojë me hile.", "by": "nga {param}", "importedByX": "Importuar nga {param}", @@ -1190,6 +1267,7 @@ "showMeEverything": "Shfaqmë gjithçka", "lichessPatronInfo": "Lichess është një program bamirësie dhe krejtësisht falas/libre, me burim të hapët.\nKrejt kostot operative, zhvillimi dhe lënda financohen vetëm me dhurime nga përdoruesit.", "nothingToSeeHere": "S’ka ç’shihet këtu tani.", + "stats": "Statistika", "opponentLeftCounter": "{count, plural, =1{Kundërshtari juaj la lojën. Mund të kërkoni fitoren pas {count} sekondash.} other{Kundërshtari juaj la lojën. Mund të kërkoni fitoren pas {count} sekondash.}}", "mateInXHalfMoves": "{count, plural, =1{Mat në {count} gjysmë lëvizje} other{Mat në {count} gjysmë lëvizje}}", "nbBlunders": "{count, plural, =1{{count} gafë} other{{count} gafa}}", @@ -1286,6 +1364,178 @@ "stormXRuns": "{count, plural, =1{1 raund} other{{count} raunde}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Luajti një raund nga {param2}} other{Luajti {count} raunde nga {param2}}}", "streamerLichessStreamers": "Transmetues Lichess-i", + "studyPrivate": "Privat", + "studyMyStudies": "Mësimet e mia", + "studyStudiesIContributeTo": "Mësimet në të cilat kam kontribuar", + "studyMyPublicStudies": "Mësimet e mia publike", + "studyMyPrivateStudies": "Mësimet e mia private", + "studyMyFavoriteStudies": "Mësimet e mia të parapëlqyera", + "studyWhatAreStudies": "Ç’janë mësimet?", + "studyAllStudies": "Krejt mësimet", + "studyStudiesCreatedByX": "Mësime të krijuara nga {param}", + "studyNoneYet": "Ende asnjë.", + "studyHot": "Më aktualet", + "studyDateAddedNewest": "Data e krijimit (nga më e reja)", + "studyDateAddedOldest": "Data e krijimit (nga më e vjetra)", + "studyRecentlyUpdated": "E përditësuar së fundmi", + "studyMostPopular": "Më populloret", + "studyAlphabetical": "Alfabetik", + "studyAddNewChapter": "Shto një kapitull të ri", + "studyAddMembers": "Shto anëtarë", + "studyInviteToTheStudy": "Ftoni në mësim", + "studyPleaseOnlyInvitePeopleYouKnow": "Ju lutemi, ftoni vetëm njerëzit që i njihni dhe që duan vërtet të marrin pjesë në këtë mësim.", + "studySearchByUsername": "Kërkoni sipas emrit të përdoruesit", + "studySpectator": "Shikues", + "studyContributor": "Kontribues", + "studyKick": "Përjashtoje", + "studyLeaveTheStudy": "Braktisni mësimin", + "studyYouAreNowAContributor": "Tani jeni një kontribues", + "studyYouAreNowASpectator": "Tani jeni shikues", + "studyPgnTags": "Etiketa PGN", + "studyLike": "Pëlqejeni", + "studyUnlike": "Shpëlqejeni", + "studyNewTag": "Etiketë e re", + "studyCommentThisPosition": "Komentoni këtë pozicion", + "studyCommentThisMove": "Komentoni këtë lëvizje", + "studyAnnotateWithGlyphs": "Shenjoni me karaktere", + "studyTheChapterIsTooShortToBeAnalysed": "Ky kapitull është shumë i shkurtë për t’u analizuar.", + "studyOnlyContributorsCanRequestAnalysis": "Analizë kompjuterike mund të kërkohet vetëm nga kontribuesit e këtij mësimi.", + "studyGetAFullComputerAnalysis": "Merrni nga shërbyesi një analizë të plotë kompjuterike të variantit kryesor.", + "studyMakeSureTheChapterIsComplete": "Sigurohuni që kapitulli të jetë i plotë. Mund të kërkoni analizë vetëm një herë.", + "studyAllSyncMembersRemainOnTheSamePosition": "Krejt anëtarët SYNC mbesin në të njëjtin pozicion", + "studyShareChanges": "Ndani ndryshimet me shikuesit dhe ruajini ato në shërbyes", + "studyPlaying": "Po luhet", + "studyShowEvalBar": "Shtylla vlerësimi", + "studyFirst": "E para", + "studyPrevious": "E mëparshmja", + "studyNext": "Pasuesja", + "studyLast": "E fundit", "studyShareAndExport": "Ndajeni me të tjerë & eksportoni", - "studyStart": "Fillo" + "studyCloneStudy": "Klonoje", + "studyStudyPgn": "Studioni PGN", + "studyDownloadAllGames": "Shkarkoji krejt lojërat", + "studyChapterPgn": "PGN e kapitullit", + "studyCopyChapterPgn": "Kopjo PGN", + "studyDownloadGame": "Shkarko lojën", + "studyStudyUrl": "URL Mësimi", + "studyCurrentChapterUrl": "URL e Kapitullit Aktual", + "studyYouCanPasteThisInTheForumToEmbed": "Këtë mund ta ngjitni te forumi ose blogu juaj Lichess, për ta trupëzuar", + "studyStartAtInitialPosition": "Fillo në pozicionin fillestar", + "studyStartAtX": "Fillo tek {param}", + "studyEmbedInYourWebsite": "Trupëzojeni te sajti juaj", + "studyReadMoreAboutEmbedding": "Lexoni më tepër rreth trupëzimit", + "studyOnlyPublicStudiesCanBeEmbedded": "Mund të trupëzoni vetëm mësime publike!", + "studyOpen": "Hap", + "studyXBroughtToYouByY": "{param1}, sjellë për ju nga {param2}", + "studyStudyNotFound": "Mësimi s’u gjet", + "studyEditChapter": "Përpunoni kapitullin", + "studyNewChapter": "Kapitull i ri", + "studyImportFromChapterX": "Importo prej {param}", + "studyOrientation": "Drejtimi", + "studyAnalysisMode": "Mënyra Analizim", + "studyPinnedChapterComment": "Koment kapitulli i fiksuar", + "studySaveChapter": "Ruaje kapitullin", + "studyClearAnnotations": "Spastro shënimet", + "studyClearVariations": "Spastroji variantet", + "studyDeleteChapter": "Fshije kapitullin", + "studyDeleteThisChapter": "Të fshihet ky kapitull? S’ka kthim mbrapa!", + "studyClearAllCommentsInThisChapter": "Të spastrohen krejt komentet, glifet dhe format e vizatuara në këtë kapitull?", + "studyRightUnderTheBoard": "Mu nën fushë", + "studyNoPinnedComment": "Asnjë", + "studyNormalAnalysis": "Analizë normale", + "studyHideNextMoves": "Fshih lëvizjen e radhës", + "studyInteractiveLesson": "Mësim me ndërveprim", + "studyChapterX": "Kapitulli {param}", + "studyEmpty": "E zbrazët", + "studyStartFromInitialPosition": "Fillo nga pozicioni fillestar", + "studyEditor": "Përpunues", + "studyStartFromCustomPosition": "Fillo nga pozicion vetjak", + "studyLoadAGameByUrl": "Ngarko lojëra nga URL", + "studyLoadAPositionFromFen": "Ngarko pozicionin nga FEN", + "studyLoadAGameFromPgn": "Ngarko lojëra nga PGN", + "studyAutomatic": "Automatik", + "studyUrlOfTheGame": "URL e lojërave, një për rresht", + "studyLoadAGameFromXOrY": "Ngarko lojëra nga {param1} ose {param2}", + "studyCreateChapter": "Krijo kapitull", + "studyCreateStudy": "Krijoni mësim", + "studyEditStudy": "Përpunoni mësimin", + "studyVisibility": "Dukshmëri", + "studyPublic": "Publike", + "studyUnlisted": "Jo në listë", + "studyInviteOnly": "Vetëm me ftesa", + "studyAllowCloning": "Lejo klonimin", + "studyNobody": "Askush", + "studyOnlyMe": "Vetëm unë", + "studyContributors": "Kontribues", + "studyMembers": "Anëtarë", + "studyEveryone": "Cilido", + "studyEnableSync": "Lejo njëkohësim", + "studyYesKeepEveryoneOnTheSamePosition": "Po: mbaje këdo në të njëjtin pozicion", + "studyNoLetPeopleBrowseFreely": "Jo: lejoji njerëzit të shfletojnë lirisht", + "studyPinnedStudyComment": "Koment studimi i fiksuar", + "studyStart": "Fillo", + "studySave": "Ruaje", + "studyClearChat": "Spastroje bisedën", + "studyDeleteTheStudyChatHistory": "Të fshihet historiku i fjalosjeve të mësimit? S’ka kthim mbrapa!", + "studyDeleteStudy": "Fshije mësimin", + "studyConfirmDeleteStudy": "Të fshihet krejt mësimi? S’ka kthim mbrapa! Për ta ripohuar, shtypni emrin e mësimit: {param}", + "studyWhereDoYouWantToStudyThat": "Ku doni ta studioni atë?", + "studyGoodMove": "Lëvizje e mirë", + "studyMistake": "Gabim", + "studyBrilliantMove": "Lëvizje e shkëlqyer", + "studyBlunder": "Gafë", + "studyInterestingMove": "Lëvizje me interes", + "studyDubiousMove": "Lëvizje e dyshimtë", + "studyOnlyMove": "Lëvizja e vetme", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "është baras me", + "studyUnclearPosition": "Shenjë gishti e paqartë", + "studyWhiteIsSlightlyBetter": "I bardhi është pakëz më mirë", + "studyBlackIsSlightlyBetter": "I ziu është pakëz më mirë", + "studyWhiteIsBetter": "I bardhi është më mirë", + "studyBlackIsBetter": "I ziu është më mirë", + "studyWhiteIsWinning": "I bardhi po fiton", + "studyBlackIsWinning": "I ziu po fiton", + "studyNovelty": "Risi", + "studyDevelopment": "Zhvillim", + "studyInitiative": "Nismë", + "studyAttack": "Sulm", + "studyCounterplay": "Kundërsulm", + "studyTimeTrouble": "Probleme me këtë instalim?", + "studyWithCompensation": "Me kompesim", + "studyWithTheIdea": "Me idenë", + "studyNextChapter": "Kapitulli pasues", + "studyPrevChapter": "Kapitulli i mëparshëm", + "studyStudyActions": "Studioni veprimet", + "studyTopics": "Tema", + "studyMyTopics": "Temat e mia", + "studyPopularTopics": "Tema popullore", + "studyManageTopics": "Administroni tema", + "studyBack": "Mbrapsht", + "studyPlayAgain": "Riluaje", + "studyWhatWouldYouPlay": "Ç’lëvizje do të bënit në këtë pozicion?", + "studyYouCompletedThisLesson": "Përgëzime! E mbaruat këtë mësim.", + "studyPerPage": "{param} për faqe", + "studyNbChapters": "{count, plural, =1{{count} Kapitull} other{{count} Kapituj}}", + "studyNbGames": "{count, plural, =1{{count} Lojë} other{{count} Lojëra}}", + "studyNbMembers": "{count, plural, =1{{count} Anëtar} other{{count} Anëtarë}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojë} other{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojëra}}", + "timeagoJustNow": "tani", + "timeagoRightNow": "pikërisht tani", + "timeagoCompleted": "mbaroi", + "timeagoInNbSeconds": "{count, plural, =1{në {count} sekondë} other{pas {count} sekondave}}", + "timeagoInNbMinutes": "{count, plural, =1{në {count} minutë} other{pas {count} minutave}}", + "timeagoInNbHours": "{count, plural, =1{në {count} orë} other{në {count} orë}}", + "timeagoInNbDays": "{count, plural, =1{në {count} ditë} other{pas {count} ditëve}}", + "timeagoInNbWeeks": "{count, plural, =1{në {count} javë} other{pas {count} javë}}", + "timeagoInNbMonths": "{count, plural, =1{në {count} muaj} other{pas {count} muajve}}", + "timeagoInNbYears": "{count, plural, =1{në {count} vit} other{pas {count} viteve}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutë më parë} other{para {count} minutave}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} orë më parë} other{para {count} orëve}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} ditë më parë} other{para {count} ditëve}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} javë më parë} other{para {count} jave}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} muaj më parë} other{para {count} muajve}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} vit më parë} other{para {count} viteve}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Edhe {count} minutë} other{Edhe {count} minuta}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Edhe {count} orë} other{Edhe {count} orë}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sr.arb b/lib/l10n/lila_sr.arb index 8e1ba93d63..7047088736 100644 --- a/lib/l10n/lila_sr.arb +++ b/lib/l10n/lila_sr.arb @@ -21,6 +21,12 @@ "activityJoinedNbTeams": "{count, plural, =1{Ушли у {count} тим} few{Ушли у {count} тима} other{Ушли у {count} тимова}}", "broadcastBroadcasts": "Емитовања", "broadcastLiveBroadcasts": "Уживо емитовање турнира", + "broadcastNewBroadcast": "Нова ужива емитовања", + "broadcastOngoing": "Текућа", + "broadcastUpcoming": "Предстојећа", + "broadcastCompleted": "Завршена", + "broadcastRoundNumber": "Број рунде", + "broadcastFullDescription": "Цео опис догађаја", "challengeChallengeToPlay": "Изазови на партију", "challengeChallengeDeclined": "Изазов одбијен", "challengeChallengeAccepted": "Изазов прихваћен!", @@ -277,8 +283,8 @@ "puzzleThemeXRayAttackDescription": "Фигура напада или брани поље, захваљујући противничкој фигури.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Противник има ограничен избор потеза и сваким потезом погоршава своју позицију.", - "puzzleThemeHealthyMix": "Здрава мешавина", - "puzzleThemeHealthyMixDescription": "Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.", + "puzzleThemeMix": "Здрава мешавина", + "puzzleThemeMixDescription": "Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.", "puzzleThemePlayerGames": "Играчеве партије", "puzzleThemePlayerGamesDescription": "Потражи проблеме створене на основу твојих партија или партија других грача.", "puzzleThemePuzzleDownloadInformation": "Ови проблеми су у јавном власништву и могуће их је презузети са {param}.", @@ -396,7 +402,6 @@ "replayMode": "Понављање партије", "realtimeReplay": "Као уживо", "byCPL": "По рачунару", - "openStudy": "Отвори проуку", "enable": "Укључи", "bestMoveArrow": "Стрелица за најбољи потез", "showVariationArrows": "Прикажи стрелице за варијацију", @@ -406,7 +411,6 @@ "memory": "Меморија", "infiniteAnalysis": "Бесконачна анализа", "removesTheDepthLimit": "Уклања ограничење дубине и греје рачунар", - "engineManager": "Менаџер машине", "blunder": "Груба грешка", "mistake": "Грешка", "inaccuracy": "Непрецизност", @@ -603,7 +607,6 @@ "block": "Блокирај", "blocked": "Блокиран", "unblock": "Одблокирај", - "followsYou": "Прате тебе", "xStartedFollowingY": "{param1} је почео/ла пратити {param2}", "more": "Више", "memberSince": "Члан од", @@ -706,7 +709,6 @@ "cheat": "Варање", "troll": "Трол", "other": "Остало", - "reportDescriptionHelp": "Залијепите везу до игре и објасните шта није у реду са понашањем корисника. Немојте само рећи \"варао\", али реците како сте дошли до тог закључка. Ваша пријава ће бити обрађена брже ако је напишете на енглеском језику.", "error_provideOneCheatedGameLink": "Наведите барем једну везу игре у којој је играч варао.", "by": "од {param}", "importedByX": "Увезао {param}", @@ -1124,6 +1126,124 @@ "stormXRuns": "{count, plural, =1{1 рунда} few{{count} рунде} other{{count} рунди}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Одиграли једну рунду {param2}-а} few{Одиграли {count} рунде {param2}-а} other{Одиграли {count} рунди {param2}-а}}", "streamerLichessStreamers": "Личес стримери", + "studyPrivate": "Приватна", + "studyMyStudies": "Моје студије", + "studyStudiesIContributeTo": "Студије којима доприносим", + "studyMyPublicStudies": "Моје јавне студије", + "studyMyPrivateStudies": "Моје приватне студије", + "studyMyFavoriteStudies": "Моје омиљене студије", + "studyWhatAreStudies": "Шта су студије?", + "studyAllStudies": "Све студије", + "studyStudiesCreatedByX": "Студије које је {param} направио/ла", + "studyNoneYet": "Ниједна за сад.", + "studyHot": "У тренду", + "studyDateAddedNewest": "Датум додавања (најновије)", + "studyDateAddedOldest": "Датум додавања (најстарије)", + "studyRecentlyUpdated": "Недавно ажуриране", + "studyMostPopular": "Најпопуларније", + "studyAddNewChapter": "Додајте ново поглавље", + "studyAddMembers": "Додај чланове", + "studyInviteToTheStudy": "Позовите у студију", + "studyPleaseOnlyInvitePeopleYouKnow": "Молимо вас да само позивате људе које познајете и који активно желе да се придруже овој студији.", + "studySearchByUsername": "Претражујте по корисничком имену", + "studySpectator": "Посматрач", + "studyContributor": "Cарадник", + "studyKick": "Избаци", + "studyLeaveTheStudy": "Напусти студију", + "studyYouAreNowAContributor": "Сада сте сарадник", + "studyYouAreNowASpectator": "Сада сте посматрач", + "studyPgnTags": "PGN ознаке", + "studyLike": "Свиђа ми се", + "studyNewTag": "Нова ознака", + "studyCommentThisPosition": "Прокоментаришите ову позицију", + "studyCommentThisMove": "Прокоментаришите овај потез", + "studyAnnotateWithGlyphs": "Прибележите глифовима", + "studyTheChapterIsTooShortToBeAnalysed": "Поглавље је прекратко за анализу.", + "studyOnlyContributorsCanRequestAnalysis": "Само сарадници у студији могу захтевати рачунарску анализу.", + "studyGetAFullComputerAnalysis": "Добијте потпуну рачунарску анализу главне варијације од стране сервера.", + "studyMakeSureTheChapterIsComplete": "Побрините се да је поглавље завршено. Само једном можете захтевати анализу.", + "studyAllSyncMembersRemainOnTheSamePosition": "Сви SYNC чланови остају на истој позицији", + "studyShareChanges": "Делите измене са посматрачима и сачувајте их на сервер", + "studyPlaying": "У току", + "studyFirst": "Прва", + "studyPrevious": "Претходна", + "studyNext": "Следећа", + "studyLast": "Последња", "studyShareAndExport": "Подели и извези", - "studyStart": "Започни" + "studyCloneStudy": "Клонирај", + "studyStudyPgn": "PGN студије", + "studyDownloadAllGames": "Преузми све партије", + "studyChapterPgn": "PGN поглавља", + "studyDownloadGame": "Преузми партију", + "studyStudyUrl": "Линк студије", + "studyCurrentChapterUrl": "Линк тренутног поглавља", + "studyYouCanPasteThisInTheForumToEmbed": "Ово можете налепити у форум да уградите", + "studyStartAtInitialPosition": "Започни на иницијалној позицији", + "studyStartAtX": "Започни на {param}", + "studyEmbedInYourWebsite": "Угради у свој сајт или блог", + "studyReadMoreAboutEmbedding": "Прочитај више о уграђивању", + "studyOnlyPublicStudiesCanBeEmbedded": "Само јавне студије могу бити уграђене!", + "studyOpen": "Отворите", + "studyXBroughtToYouByY": "{param2} Вам доноси {param1}", + "studyStudyNotFound": "Студија није пронађена", + "studyEditChapter": "Измени поглавље", + "studyNewChapter": "Ново поглавље", + "studyOrientation": "Оријентација", + "studyAnalysisMode": "Врста анализе", + "studyPinnedChapterComment": "Закачен коментар поглавља", + "studySaveChapter": "Сачувај поглавље", + "studyClearAnnotations": "Избриши анотације", + "studyDeleteChapter": "Избриши поглавље", + "studyDeleteThisChapter": "Избриши ово поглавље? Нема повратка назад!", + "studyClearAllCommentsInThisChapter": "Избриши све коментаре, глифове и нацртане облике у овом поглављу?", + "studyRightUnderTheBoard": "Одмах испод табле", + "studyNoPinnedComment": "Ниједан", + "studyNormalAnalysis": "Нормална анализа", + "studyHideNextMoves": "Сакриј следеће потезе", + "studyInteractiveLesson": "Интерактивна лекција", + "studyChapterX": "Поглавље {param}", + "studyEmpty": "Празно", + "studyStartFromInitialPosition": "Започните од иницијалне позиције", + "studyEditor": "Уређивач", + "studyStartFromCustomPosition": "Започните од жељене позиције", + "studyLoadAGameByUrl": "Учитајте партије преко линкова", + "studyLoadAPositionFromFen": "Учитајте позицију из FEN-а", + "studyLoadAGameFromPgn": "Учитајте партију из PGN-а", + "studyAutomatic": "Аутоматски", + "studyUrlOfTheGame": "Линкови партија, једна по реду", + "studyLoadAGameFromXOrY": "Учитајте партије са {param1} или {param2}", + "studyCreateChapter": "Направи поглавље", + "studyCreateStudy": "Направи студију", + "studyEditStudy": "Измени студију", + "studyVisibility": "Видљивост", + "studyPublic": "Јавно", + "studyUnlisted": "Неприказано", + "studyInviteOnly": "Само по позиву", + "studyAllowCloning": "Дозволите клонирање", + "studyNobody": "Нико", + "studyOnlyMe": "Само ја", + "studyContributors": "Сарадници", + "studyMembers": "Чланови", + "studyEveryone": "Сви", + "studyEnableSync": "Омогући синхронизацију", + "studyYesKeepEveryoneOnTheSamePosition": "Да: задржи све на истој позицији", + "studyNoLetPeopleBrowseFreely": "Не: дозволи људима да слободно прегледају", + "studyPinnedStudyComment": "Закачен коментар студије", + "studyStart": "Започни", + "studySave": "Сачувај", + "studyClearChat": "Очисти ћаскање", + "studyDeleteTheStudyChatHistory": "Избриши историју ћаскања студије? Нема повратка назад!", + "studyDeleteStudy": "Избриши студију", + "studyWhereDoYouWantToStudyThat": "Где желите то проучити?", + "studyGoodMove": "Добар потез", + "studyMistake": "Грешка", + "studyBlunder": "Груба грешка", + "studyNbChapters": "{count, plural, =1{{count} Поглавље} few{{count} Поглављa} other{{count} Поглављa}}", + "studyNbGames": "{count, plural, =1{{count} Партија} few{{count} Партијe} other{{count} Партија}}", + "studyNbMembers": "{count, plural, =1{{count} Члан} few{{count} Чланa} other{{count} Чланова}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Налепите свој PGN текст овде, до {count} партије} few{Налепите свој PGN текст овде, до {count} партије} other{Налепите свој PGN текст овде, до {count} партија}}", + "timeagoJustNow": "управо сада", + "timeagoRightNow": "управо сад", + "timeagoCompleted": "завршено", + "timeagoInNbSeconds": "{count, plural, =1{за {count} секунди} few{за {count} сати} other{за {count} дана}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sv.arb b/lib/l10n/lila_sv.arb index 356c119feb..6cbf08b278 100644 --- a/lib/l10n/lila_sv.arb +++ b/lib/l10n/lila_sv.arb @@ -1,4 +1,30 @@ { + "mobileAllGames": "Alla spel", + "mobileAreYouSure": "Är du säker?", + "mobileBlindfoldMode": "I blindo", + "mobileClearButton": "Rensa", + "mobileCustomGameJoinAGame": "Gå med i spel", + "mobileGreeting": "Hej {param}", + "mobileGreetingWithoutName": "Hej", + "mobileHideVariation": "Dölj variationer", + "mobileHomeTab": "Hem", + "mobileNoSearchResults": "Inga resultat", + "mobileNotFollowingAnyUser": "Du följer inte någon användare.", + "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spelare med \"{param}\"", + "mobilePuzzlesTab": "Problem", + "mobileRecentSearches": "Senaste sökningar", + "mobileShareGamePGN": "Dela PGN", + "mobileShareGameURL": "Dela parti-URL", + "mobileSharePositionAsFEN": "Dela position som FEN", + "mobileSharePuzzle": "Dela detta schackproblem", + "mobileShowComments": "Visa kommentarer", + "mobileShowResult": "Visa resultat", + "mobileShowVariations": "Visa variationer", + "mobileSomethingWentWrong": "Något gick fel.", + "mobileSystemColors": "Systemets färger", + "mobileToolsTab": "Verktyg", + "mobileWatchTab": "Titta", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Var värd för en direktsänd videosändning", "activityRankedInSwissTournament": "Rankad #{param1} i {param2}", @@ -21,7 +47,35 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Tävlade i {count} swissturnering} other{Tävlade i {count} swissturneringar}}", "activityJoinedNbTeams": "{count, plural, =1{Gick med i {count} lag} other{Gick med i {count} lag}}", "broadcastBroadcasts": "Sändningar", + "broadcastMyBroadcasts": "Mina sändningar", "broadcastLiveBroadcasts": "Direktsända turneringar", + "broadcastNewBroadcast": "Ny direktsändning", + "broadcastAboutBroadcasts": "Om sändningar", + "broadcastHowToUseLichessBroadcasts": "Hur man använder Lichess-Sändningar.", + "broadcastTheNewRoundHelp": "Den nya rundan kommer att ha samma medlemmar och bidragsgivare som den föregående.", + "broadcastAddRound": "Lägg till en omgång", + "broadcastOngoing": "Pågående", + "broadcastUpcoming": "Kommande", + "broadcastCompleted": "Slutförda", + "broadcastCompletedHelp": "Lichess upptäcker slutförandet av rundor baserat på källspelen. Använd detta alternativ om det inte finns någon källa.", + "broadcastRoundName": "Omgångens namn", + "broadcastRoundNumber": "Omgångens nummer", + "broadcastTournamentName": "Turneringens namn", + "broadcastTournamentDescription": "Kort beskrivning av turneringen", + "broadcastFullDescription": "Fullständig beskrivning", + "broadcastFullDescriptionHelp": "Valfri längre beskrivning av sändningen. {param1} är tillgänglig. Längden måste vara mindre än {param2} tecken.", + "broadcastSourceUrlHelp": "URL som Lichess kan använda för att få PGN-uppdateringar. Den måste vara publikt tillgänglig från Internet.", + "broadcastStartDateHelp": "Valfritt, om du vet när händelsen startar", + "broadcastCurrentGameUrl": "Länk till aktuellt parti (URL)", + "broadcastDownloadAllRounds": "Ladda ner alla omgångar", + "broadcastResetRound": "Återställ den här omgången", + "broadcastDeleteRound": "Ta bort den här omgången", + "broadcastDefinitivelyDeleteRound": "Ta bort denna runda och dess partier definitivt.", + "broadcastDeleteAllGamesOfThisRound": "Radera alla partier i denna runda. Källan kommer behöva vara aktiv för att återskapa dem.", + "broadcastEditRoundStudy": "Redigera studie för ronden", + "broadcastDeleteTournament": "Radera turnering", + "broadcastDefinitivelyDeleteTournament": "Definitivt radera turnering.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} sändning} other{{count} sändningar}}", "challengeChallengesX": "Utmaningar: {param1}", "challengeChallengeToPlay": "Utmana till ett parti", "challengeChallengeDeclined": "Utmaning avböjd", @@ -145,6 +199,7 @@ "preferencesNotifyWeb": "Webbläsare", "preferencesNotifyDevice": "Enhet", "preferencesBellNotificationSound": "Klock-notisljud", + "preferencesBlindfold": "I blindo", "puzzlePuzzles": "Problem", "puzzlePuzzleThemes": "Teman för schackproblem", "puzzleRecommended": "Rekommenderad", @@ -340,8 +395,8 @@ "puzzleThemeXRayAttackDescription": "En pjäs attackerar eller försvarar en ruta, genom en motståndarpjäs.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Motspelaren har begränsat antal möjliga drag, och alla möjliga drag förvärrar motspelarens position.", - "puzzleThemeHealthyMix": "Blandad kompott", - "puzzleThemeHealthyMixDescription": "Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.", + "puzzleThemeMix": "Blandad kompott", + "puzzleThemeMixDescription": "Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.", "puzzleThemePlayerGames": "Spelarspel", "puzzleThemePlayerGamesDescription": "Hitta pussel genererade från dina egna parti, eller från andra spelares parti.", "puzzleThemePuzzleDownloadInformation": "Dessa pussel tillhör den allmänna egendomen, och kan laddas ner från {param}.", @@ -460,7 +515,6 @@ "replayMode": "Uppspelningsläge", "realtimeReplay": "Realtid", "byCPL": "CPL", - "openStudy": "Öppna studie", "enable": "Aktivera", "bestMoveArrow": "Pil som anger bästa drag", "showVariationArrows": "Visa variationspilar", @@ -470,7 +524,6 @@ "memory": "Minne", "infiniteAnalysis": "Oändlig analys", "removesTheDepthLimit": "Tar bort sökdjupsbegränsningen och håller datorn varm", - "engineManager": "Hantera analysmotor", "blunder": "Blunder", "mistake": "Misstag", "inaccuracy": "Felaktighet", @@ -666,7 +719,6 @@ "block": "Blockera", "blocked": "Blockerad", "unblock": "Avblockera", - "followsYou": "Följer dig", "xStartedFollowingY": "{param1} började följa {param2}", "more": "Visa mer", "memberSince": "Medlem sedan", @@ -770,7 +822,6 @@ "cheat": "Fusk", "troll": "Troll", "other": "Annat", - "reportDescriptionHelp": "Klistra in länken till partiet och förklara vad som är fel med den här användarens beteende. Säg inte bara \"de fuskar\", utan förklara hur du dragit denna slutsats. Din rapport kommer att behandlas fortare om den är skriven på engelska.", "error_provideOneCheatedGameLink": "Ange minst en länk till ett spel där användaren fuskade.", "by": "av {param}", "importedByX": "Importerad av {param}", @@ -1260,6 +1311,177 @@ "stormXRuns": "{count, plural, =1{1 försök} other{{count} försök}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Spelade en omgång {param2}} other{Spelade {count} omgångar {param2}}}", "streamerLichessStreamers": "Videokanaler från Lichess", + "studyPrivate": "Privat", + "studyMyStudies": "Mina studier", + "studyStudiesIContributeTo": "Studier som jag bidrar till", + "studyMyPublicStudies": "Mina offentliga studier", + "studyMyPrivateStudies": "Mina privata studier", + "studyMyFavoriteStudies": "Mina favoritstudier", + "studyWhatAreStudies": "Vad är studier?", + "studyAllStudies": "Alla studier", + "studyStudiesCreatedByX": "Studier skapade av {param}", + "studyNoneYet": "Inga ännu.", + "studyHot": "Populära", + "studyDateAddedNewest": "Datum tillagd (nyaste)", + "studyDateAddedOldest": "Datum tillagd (nyaste)", + "studyRecentlyUpdated": "Nyligen uppdaterade", + "studyMostPopular": "Mest populära", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Lägg till ett nytt kapitel", + "studyAddMembers": "Lägg till medlemmar", + "studyInviteToTheStudy": "Bjud in till studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Viktigt: bjud bara in människor du känner och som aktivt vill gå med i studien.", + "studySearchByUsername": "Sök efter användarnamn", + "studySpectator": "Åskådare", + "studyContributor": "Bidragsgivare", + "studyKick": "Sparka", + "studyLeaveTheStudy": "Lämna studien", + "studyYouAreNowAContributor": "Du är nu bidragsgivare", + "studyYouAreNowASpectator": "Du är nu en åskådare", + "studyPgnTags": "PGN taggar", + "studyLike": "Gilla", + "studyUnlike": "Sluta gilla", + "studyNewTag": "Ny tag", + "studyCommentThisPosition": "Kommentera denna position", + "studyCommentThisMove": "Kommentera detta drag", + "studyAnnotateWithGlyphs": "Kommentera med glyfer", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitlet är för kort för att analyseras.", + "studyOnlyContributorsCanRequestAnalysis": "Endast studiens bidragsgivare kan begära en datoranalys.", + "studyGetAFullComputerAnalysis": "Hämta en fullständig serveranalys av huvudlinjen.", + "studyMakeSureTheChapterIsComplete": "Försäkra dig om att kapitlet är färdigt. Du kan bara begära analysen en gång.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alla SYNC-medlemmar är kvar på samma position", + "studyShareChanges": "Dela ändringar med åskådare och spara dem på servern", + "studyPlaying": "Spelar", + "studyShowEvalBar": "Värderingsfält", + "studyFirst": "Första", + "studyPrevious": "Föregående", + "studyNext": "Nästa", + "studyLast": "Sista", "studyShareAndExport": "Dela & exportera", - "studyStart": "Starta" + "studyCloneStudy": "Klona", + "studyStudyPgn": "Studiens PGN", + "studyDownloadAllGames": "Ladda ner alla partier", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "Kopiera PGN", + "studyDownloadGame": "Ladda ner parti", + "studyStudyUrl": "Studiens URL", + "studyCurrentChapterUrl": "Aktuell kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan klistra in detta i forumet för att infoga", + "studyStartAtInitialPosition": "Start vid ursprunglig position", + "studyStartAtX": "Börja på {param}", + "studyEmbedInYourWebsite": "Infoga på din hemsida eller blogg", + "studyReadMoreAboutEmbedding": "Läs mer om att infoga", + "studyOnlyPublicStudiesCanBeEmbedded": "Endast offentliga studier kan läggas till!", + "studyOpen": "Öppna", + "studyXBroughtToYouByY": "{param1} gjord av {param2}", + "studyStudyNotFound": "Studien kan inte hittas", + "studyEditChapter": "Redigera kapitel", + "studyNewChapter": "Nytt kapitel", + "studyImportFromChapterX": "Importera från {param}", + "studyOrientation": "Orientering", + "studyAnalysisMode": "Analysläge", + "studyPinnedChapterComment": "Fastnålad kommentar till kapitlet", + "studySaveChapter": "Spara kapitlet", + "studyClearAnnotations": "Rensa kommentarer", + "studyClearVariations": "Rensa variationer", + "studyDeleteChapter": "Ta bort kapitel", + "studyDeleteThisChapter": "Ta bort detta kapitel. Det går inte att ångra!", + "studyClearAllCommentsInThisChapter": "Rensa alla kommentarer, symboler och former i detta kapitel?", + "studyRightUnderTheBoard": "Direkt under brädet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analys", + "studyHideNextMoves": "Dölj nästa drag", + "studyInteractiveLesson": "Interaktiv lektion", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Starta från ursprunglig position", + "studyEditor": "Redigeringsverktyg", + "studyStartFromCustomPosition": "Starta från anpassad position", + "studyLoadAGameByUrl": "Importera ett spel med URL", + "studyLoadAPositionFromFen": "Importera en position med FEN-kod", + "studyLoadAGameFromPgn": "Importera ett spel med PGN-kod", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL till partiet", + "studyLoadAGameFromXOrY": "Importera ett parti från {param1} eller {param2}", + "studyCreateChapter": "Skapa kapitel", + "studyCreateStudy": "Skapa en studie", + "studyEditStudy": "Redigera studie", + "studyVisibility": "Synlighet", + "studyPublic": "Offentlig", + "studyUnlisted": "Ej listad", + "studyInviteOnly": "Endast inbjudna", + "studyAllowCloning": "Tillåt kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Bara mig", + "studyContributors": "Medhjälpare", + "studyMembers": "Medlemmar", + "studyEveryone": "Alla", + "studyEnableSync": "Aktivera synkronisering", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: håll alla på samma position", + "studyNoLetPeopleBrowseFreely": "Nej: låt alla bläddra fritt", + "studyPinnedStudyComment": "Ständigt synlig studiekommentar", + "studyStart": "Starta", + "studySave": "Spara", + "studyClearChat": "Rensa Chatten", + "studyDeleteTheStudyChatHistory": "Radera studiens chatthistorik? Detta går inte att ångra!", + "studyDeleteStudy": "Ta bort studie", + "studyConfirmDeleteStudy": "Radera hela studien? Detta går inte att ångra! Skriv namnet på studien för att bekräfta: {param}", + "studyWhereDoYouWantToStudyThat": "Var vill du studera detta?", + "studyGoodMove": "Bra drag", + "studyMistake": "Misstag", + "studyBrilliantMove": "Lysande drag", + "studyBlunder": "Blunder", + "studyInterestingMove": "Intressant drag", + "studyDubiousMove": "Tvivelaktigt drag", + "studyOnlyMove": "Enda draget", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Likvärdig position", + "studyUnclearPosition": "Oklar position", + "studyWhiteIsSlightlyBetter": "Vit är något bättre", + "studyBlackIsSlightlyBetter": "Svart är något bättre", + "studyWhiteIsBetter": "Vit är bättre", + "studyBlackIsBetter": "Svart är bättre", + "studyWhiteIsWinning": "Vit vinner", + "studyBlackIsWinning": "Svart vinner", + "studyNovelty": "Ny variant", + "studyDevelopment": "Utveckling", + "studyInitiative": "Initiativ", + "studyAttack": "Attack", + "studyCounterplay": "Motspel", + "studyTimeTrouble": "Tidsproblem", + "studyWithCompensation": "Med kompensation", + "studyWithTheIdea": "Med idén", + "studyNextChapter": "Nästa kapitel", + "studyPrevChapter": "Föregående kapitel", + "studyStudyActions": "Studie-alternativ", + "studyTopics": "Ämnen", + "studyMyTopics": "Mina ämnen", + "studyPopularTopics": "Populära ämnen", + "studyManageTopics": "Hantera ämnen", + "studyBack": "Tillbaka", + "studyPlayAgain": "Spela igen", + "studyWhatWouldYouPlay": "Vad skulle du spela i denna position?", + "studyYouCompletedThisLesson": "Grattis! Du har slutfört denna lektionen.", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} partier} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmar}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Klistra in din PGN-kod här, upp till {count} parti} other{Klistra in din PGN-kod här, upp till {count} partier}}", + "timeagoJustNow": "just nu", + "timeagoRightNow": "just nu", + "timeagoCompleted": "slutfört", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minut} other{om {count} minuter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} timme} other{om {count} timmar}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dagar}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} vecka} other{om {count} veckor}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} månad} other{om {count} månader}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut sedan} other{{count} minuter sedan}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} timme sedan} other{{count} timmar sedan}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag sedan} other{{count} dagar sedan}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} vecka sedan} other{{count} veckor sedan}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} månad sedan} other{{count} månader sedan}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år sedan} other{{count} år sedan}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut återstår} other{{count} minuter återstår}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} timme återstår} other{{count} timmar återstår}}" } \ No newline at end of file diff --git a/lib/l10n/lila_tr.arb b/lib/l10n/lila_tr.arb index d59f54004e..afabc8dd64 100644 --- a/lib/l10n/lila_tr.arb +++ b/lib/l10n/lila_tr.arb @@ -1,33 +1,47 @@ { + "mobileAllGames": "Tüm oyunlar", + "mobileAreYouSure": "Emin misiniz?", + "mobileBlindfoldMode": "Körleme modu", + "mobileCancelTakebackOffer": "Geri alma teklifini iptal et", + "mobileClearButton": "Temizle", + "mobileCorrespondenceClearSavedMove": "Kayıtlı hamleyi sil", + "mobileCustomGameJoinAGame": "Bir oyuna katıl", + "mobileFeedbackButton": "Geri bildirimde bulun", + "mobileGreeting": "Merhaba, {param}", + "mobileGreetingWithoutName": "Merhaba", + "mobileHideVariation": "Varyasyonu gizle", "mobileHomeTab": "Ana sayfa", - "mobilePuzzlesTab": "Bulmacalar", - "mobileToolsTab": "Araçlar", - "mobileWatchTab": "İzle", - "mobileSettingsTab": "Ayarlar", + "mobileLiveStreamers": "Canlı yayıncılar", "mobileMustBeLoggedIn": "Bu sayfayı görüntülemek için giriş yapmalısınız.", - "mobileSystemColors": "Sistem renkleri", - "mobileFeedbackButton": "Geri bildirimde bulun", - "mobileOkButton": "Tamam", - "mobileSettingsHapticFeedback": "Titreşimli geri bildirim", + "mobileNoSearchResults": "Sonuç bulunamadı", "mobileNotFollowingAnyUser": "Hiçbir kullanıcıyı takip etmiyorsunuz.", - "mobileAllGames": "Tüm oyunlar", + "mobileOkButton": "Tamam", + "mobilePlayersMatchingSearchTerm": "\"{param}\" ile başlayan oyuncularla", + "mobilePrefMagnifyDraggedPiece": "Sürüklenen parçayı büyüt", + "mobilePuzzleStormConfirmEndRun": "Bu oyunu bitirmek istiyor musun?", + "mobilePuzzleStormFilterNothingToShow": "Gösterilecek bir şey yok, lütfen filtreleri değiştirin", + "mobilePuzzleStormNothingToShow": "Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.", + "mobilePuzzleStormSubtitle": "3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.", + "mobilePuzzleStreakAbortWarning": "Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.", + "mobilePuzzleThemesSubtitle": "En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.", + "mobilePuzzlesTab": "Bulmacalar", "mobileRecentSearches": "Son aramalar", - "mobileClearButton": "Temizle", - "mobileNoSearchResults": "Sonuç bulunamadı", - "mobileAreYouSure": "Emin misiniz?", - "mobileSharePuzzle": "Bulmacayı paylaş", - "mobileShareGameURL": "Oyun linkini paylaş", + "mobileSettingsHapticFeedback": "Titreşimli geri bildirim", + "mobileSettingsImmersiveMode": "Sürükleyici mod", + "mobileSettingsImmersiveModeSubtitle": "Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.", + "mobileSettingsTab": "Ayarlar", "mobileShareGamePGN": "PGN'yi paylaş", + "mobileShareGameURL": "Oyun linkini paylaş", "mobileSharePositionAsFEN": "Konumu FEN olarak paylaş", - "mobileShowVariations": "Varyasyonları göster", - "mobileHideVariation": "Varyasyonu gizle", + "mobileSharePuzzle": "Bulmacayı paylaş", "mobileShowComments": "Yorumları göster", - "mobileCancelTakebackOffer": "Geri alma teklifini iptal et", - "mobileCancelDrawOffer": "Berabere teklifini iptal et", - "mobileWaitingForOpponentToJoin": "Rakip bekleniyor...", - "mobileBlindfoldMode": "Körleme modu", - "mobileCustomGameJoinAGame": "Bir oyuna katıl", + "mobileShowResult": "Sonucu göster", + "mobileShowVariations": "Varyasyonları göster", "mobileSomethingWentWrong": "Birşeyler ters gitti.", + "mobileSystemColors": "Sistem renkleri", + "mobileToolsTab": "Araçlar", + "mobileWaitingForOpponentToJoin": "Rakip bekleniyor...", + "mobileWatchTab": "İzle", "activityActivity": "Son Etkinlikler", "activityHostedALiveStream": "Canlı yayın yaptı", "activityRankedInSwissTournament": "{param2} katılımcıları arasında #{param1}. oldu", @@ -40,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{{count} hamle yaptı} other{{count} hamle yaptı}}", "activityInNbCorrespondenceGames": "{count, plural, =1{({count} yazışmalı oyunda)} other{({count} yazışmalı oyunda)}}", "activityCompletedNbGames": "{count, plural, =1{{count} adet yazışmalı oyun tamamladı} other{{count} adet yazışmalı oyun tamamladı}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} {param2} yazışmalı oyunu tamamladı} other{{count} {param2} yazışmalı oyunu tamamladı}}", "activityFollowedNbPlayers": "{count, plural, =1{{count} oyuncuyu takip etmeye başladı} other{{count} oyuncuyu takip etmeye başladı}}", "activityGainedNbFollowers": "{count, plural, =1{{count} yeni takipçi kazandı} other{{count} yeni takipçi kazandı}}", "activityHostedNbSimuls": "{count, plural, =1{{count} simultaneye ev sahipliği yaptı} other{{count} eş zamanlı gösteriye ev sahipliği yaptı}}", @@ -50,7 +65,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} İsviçre Sistemi turnuvasına katıldı} other{{count} İsviçre Sistemi turnuvasına katıldı}}", "activityJoinedNbTeams": "{count, plural, =1{{count} takıma katıldı} other{{count} takıma katıldı}}", "broadcastBroadcasts": "Canlı Turnuvalar", + "broadcastMyBroadcasts": "Canlı Turnuvalarım", "broadcastLiveBroadcasts": "Canlı Turnuva Yayınları", + "broadcastBroadcastCalendar": "Turnuva takvimi", + "broadcastNewBroadcast": "Canlı Turnuva Ekle", + "broadcastSubscribedBroadcasts": "Abone olduğunuz yayınlar", + "broadcastAboutBroadcasts": "Canlı Turnuvalar hakkında", + "broadcastHowToUseLichessBroadcasts": "Lichess Canlı Turnuvaları nasıl kullanılır.", + "broadcastTheNewRoundHelp": "Yeni tur, önceki turdaki üyeler ve katkıda bulunanlarla aynı olacak.", + "broadcastAddRound": "Bir tur ekle", + "broadcastOngoing": "Devam eden turnuvalar", + "broadcastUpcoming": "Yaklaşan turnuvalar", + "broadcastCompleted": "Tamamlanan turnuvalar", + "broadcastCompletedHelp": "Lichess, tur tamamlanmasını kaynak oyunlara dayanarak algılar. Kaynak yoksa bu anahtarı kullanın.", + "broadcastRoundName": "Tur ismi", + "broadcastRoundNumber": "Tur sayısı", + "broadcastTournamentName": "Turnuva ismi", + "broadcastTournamentDescription": "Turnuvanın kısa tanımı", + "broadcastFullDescription": "Etkinliğin detaylıca açıklaması", + "broadcastFullDescriptionHelp": "Etkinliğin isteğe bağlı detaylı açıklaması. {param1} seçeneği mevcuttur. Metnin uzunluğu azami {param2} karakter olmalıdır.", + "broadcastSourceSingleUrl": "PGN Kaynak URL'si", + "broadcastSourceUrlHelp": "Lichess, sağladığınız URL yardımıyla PGN'yi güncelleyecektir. İnternet üzerinden herkese açık bir URL yazmalısınız.", + "broadcastSourceGameIds": "Boşluklarla ayrılmış 64 adede kadar Lichess oyun ID'si.", + "broadcastStartDateTimeZone": "Turnuva yerel saati ile başlama zamanı: {param}", + "broadcastStartDateHelp": "İsteğe bağlı, etkinliğin ne zaman başladığını biliyorsanız ekleyebilirsiniz.", + "broadcastCurrentGameUrl": "Şu anki oyunun linki", + "broadcastDownloadAllRounds": "Bütün maçları indir", + "broadcastResetRound": "Bu turu sıfırla", + "broadcastDeleteRound": "Bu turu sil", + "broadcastDefinitivelyDeleteRound": "Turu ve oyunlarını tamamen sil.", + "broadcastDeleteAllGamesOfThisRound": "Bu turdaki tüm oyunları sil. Tekrardan yapılabilmesi için kaynağın aktif olması gerekir.", + "broadcastEditRoundStudy": "Tur çalışmasını düzenle", + "broadcastDeleteTournament": "Bu turnuvayı sil", + "broadcastDefinitivelyDeleteTournament": "Bütün turnuvayı, turlarını ve oyunlarını kalıcı olarak sil.", + "broadcastShowScores": "Oyuncuların puanlarını oyun sonuçlarına göre göster", + "broadcastReplacePlayerTags": "İsteğe bağlı: Oyuncu adlarını, derecelendirmelerini ve unvanlarını değiştirin", + "broadcastFideFederations": "FIDE federasyonları", + "broadcastTop10Rating": "İlk 10 rating", + "broadcastFidePlayers": "FIDE oyuncuları", + "broadcastFidePlayerNotFound": "FIDE oyuncusu bulunamadı", + "broadcastFideProfile": "FIDE profili", + "broadcastFederation": "Federasyon", + "broadcastAgeThisYear": "Bu yılki yaşı", + "broadcastUnrated": "Derecelendirilmemiş", + "broadcastRecentTournaments": "Son Turnuvalar", + "broadcastOpenLichess": "Lichess'te aç", + "broadcastTeams": "Takımlar", + "broadcastBoards": "Tahtalar", + "broadcastOverview": "Genel Bakış", + "broadcastSubscribeTitle": "Tur başladığında bildirim almak için abone olun. Hesap tercihlerinizden anlık ya da çan bildirimi tercihinizi hesap tercihlerinizden belirleyebilirsiniz.", + "broadcastUploadImage": "Turnuva görseli yükleyin", + "broadcastNoBoardsYet": "Henüz tahta bulunmamaktadır. Oyunlar yüklendikçe tahtalar ortaya çıkacaktır.", + "broadcastBoardsCanBeLoaded": "Tahtalar bir kaynaktan ya da {param}ndan yüklenebilir", + "broadcastStartsAfter": "{param}'ten sonra başlar", + "broadcastStartVerySoon": "Yayın az sonra başlayacak.", + "broadcastNotYetStarted": "Yayın henüz başlamadı.", + "broadcastOfficialWebsite": "Resmî site", + "broadcastStandings": "Sıralamalar", + "broadcastOfficialStandings": "Resmi Sıralamalar", + "broadcastIframeHelp": "{param}nda daha fazla seçenek", + "broadcastWebmastersPage": "ağ yöneticileri sayfası", + "broadcastPgnSourceHelp": "Bu turun açık, gerçek zamanlı PGN kaynağı. Daha hızlı ve verimli senkronizasyon için {param}'ımız da bulunmaktadır.", + "broadcastEmbedThisBroadcast": "İnternet sitenizde bu yayını gömülü paylaşın", + "broadcastEmbedThisRound": "{param}u İnternet sitenizde gömülü paylaşın", + "broadcastRatingDiff": "Puan farkı", + "broadcastGamesThisTournament": "Bu turnuvadaki maçlar", + "broadcastScore": "Skor", + "broadcastAllTeams": "Tüm takımlar", + "broadcastTournamentFormat": "Turnuva biçimi", + "broadcastTournamentLocation": "Turnuva Konumu", + "broadcastTopPlayers": "En iyi oyuncular", + "broadcastTimezone": "Zaman dilimi", + "broadcastFideRatingCategory": "FIDE derecelendirme kategorisi", + "broadcastOptionalDetails": "İsteğe bağlı ayrıntılar", + "broadcastPastBroadcasts": "Geçmiş yayınlar", + "broadcastAllBroadcastsByMonth": "Tüm yayınları aylara göre görüntüleyin", + "broadcastNbBroadcasts": "{count, plural, =1{{count} canlı turnuva} other{{count} canlı turnuva}}", "challengeChallengesX": "{param1} karşılaşmaları", "challengeChallengeToPlay": "Oyun teklif et", "challengeChallengeDeclined": "Oyun teklifi reddedildi", @@ -174,6 +264,7 @@ "preferencesNotifyWeb": "Tarayıcı", "preferencesNotifyDevice": "Cihaz", "preferencesBellNotificationSound": "Çan bildirimi sesi", + "preferencesBlindfold": "Körleme modu", "puzzlePuzzles": "Bulmacalar", "puzzlePuzzleThemes": "Bulmaca temaları", "puzzleRecommended": "Önerilen", @@ -369,8 +460,8 @@ "puzzleThemeXRayAttackDescription": "Rakibin taşı üzerinden bir kareyi koruyan veya bir kareye saldıran taşları içeren taktikler.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Rakibin iyi bir hamlesinin kalmadığı, yapabileceği bütün hamlelerin kendisine zarar vereceği taktikler.", - "puzzleThemeHealthyMix": "Bir ondan bir bundan", - "puzzleThemeHealthyMixDescription": "Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.", + "puzzleThemeMix": "Bir ondan bir bundan", + "puzzleThemeMixDescription": "Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.", "puzzleThemePlayerGames": "Bireysel oyunlar", "puzzleThemePlayerGamesDescription": "Sizin veya başka bir oyuncunun maçlarından üretilen bulmacalar.", "puzzleThemePuzzleDownloadInformation": "Bütün bulmacalar kamuya açıktır ve {param} adresinden indirilebilir.", @@ -450,7 +541,7 @@ "makeMainLine": "Ana devam yolu yap", "deleteFromHere": "Bu hamleden sonrasını sil", "collapseVariations": "Varyasyonları daralt", - "expandVariations": "Varyasyonları genişler", + "expandVariations": "Varyasyonları genişlet", "forceVariation": "Varyant olarak göster", "copyVariationPgn": "Varyasyon PGN'sini kopyala", "move": "Hamle", @@ -491,7 +582,6 @@ "replayMode": "Tekrar modu", "realtimeReplay": "Gerçek Zamanlı", "byCPL": "CPL ile", - "openStudy": "Çalışma oluştur", "enable": "Etkinleştir", "bestMoveArrow": "En iyi hamle imleci", "showVariationArrows": "Varyasyon oklarını göster", @@ -501,7 +591,6 @@ "memory": "Hafıza", "infiniteAnalysis": "Sonsuz analiz", "removesTheDepthLimit": "Derinlik sınırını kaldırır ve bilgisayarınızı sıcacık tutar", - "engineManager": "Motor yöneticisi", "blunder": "Vahim hata", "mistake": "Hata", "inaccuracy": "Kusurlu hamle", @@ -527,6 +616,7 @@ "latestForumPosts": "En son forum gönderileri", "players": "Oyuncular", "friends": "Arkadaşlar", + "otherPlayers": "diğer oyuncular", "discussions": "Sohbetler", "today": "Bugün", "yesterday": "Dün", @@ -582,6 +672,7 @@ "rank": "Sıralama", "rankX": "Sıralama: {param}", "gamesPlayed": "Oynanmış oyunlar", + "ok": "Tamam", "cancel": "İptal et", "whiteTimeOut": "Beyazın zamanı tükendi", "blackTimeOut": "Siyahın zamanı tükendi", @@ -698,7 +789,6 @@ "block": "Engelle", "blocked": "Engellendi", "unblock": "Engeli kaldır", - "followsYou": "Sizi takip ediyor", "xStartedFollowingY": "{param1}, {param2} isimli oyuncuyu takip etmeye başladı", "more": "Daha fazla", "memberSince": "Üyelik tarihi", @@ -784,6 +874,8 @@ "descPrivateHelp": "Sadece takım üyelerinin görebileceği metindir. Kullanılması halinde, takım üyeleri genel açıklama yerine bunu görür.", "no": "Hayır", "yes": "Evet", + "website": "Web sitesi", + "mobile": "Mobil", "help": "Yardım:", "createANewTopic": "Yeni bir bildiri oluştur.", "topics": "Konular", @@ -802,7 +894,9 @@ "cheat": "Hile", "troll": "Trol", "other": "Diğer", - "reportDescriptionHelp": "Raporlamak istediğiniz oyunun linkini yapıştırın ve sorununuzu açıklayın. Lütfen sadece \"hile yapıyor\" gibisinden açıklama yazmayın, hile olduğunu nasıl anladığınızı açıklayın. Rapor edeceğiniz kişiyi ya da oyunu \"İngilizce\" açıklarsanız, daha hızlı sonuca ulaşırsınız.", + "reportCheatBoostHelp": "Oyunun (oyunların) bağlantısını yapıştırın ve kullanıcının hangi davranışı yanlış açıklayın. Sadece \"bu hile\" demeyin, bize bu sonuca nasıl vardığınızı söyleyin.", + "reportUsernameHelp": "Bu kullanıcı adı hakkında neyin saldırganca olduğunu açıklayın. Sadece \"bu saldırganca/uygunsuz\" demeyin, bu sonuca nasıl vardığınızı söyleyin bize, özellikle de hakaret gizlenmiş, ingilizce olmayan, argo, veya tarihsel/kültürel referansalar varsa.", + "reportProcessedFasterInEnglish": "Eğer İngilizce yazarsanız raporunuz daha hızlı işleme alınır.", "error_provideOneCheatedGameLink": "Lütfen hileli gördüğünüz en az 1 adet oyun linki verin.", "by": "{param} oluşturdu", "importedByX": "{param} tarafından yüklendi", @@ -835,6 +929,7 @@ "slow": "Yavaş", "insideTheBoard": "Karelerin üzerinde", "outsideTheBoard": "Karelerin dışında", + "allSquaresOfTheBoard": "Tahtadaki tüm karelerde", "onSlowGames": "Yavaş oyunlarda", "always": "Her zaman", "never": "Asla", @@ -1199,6 +1294,7 @@ "showMeEverything": "Bana her şeyi göster", "lichessPatronInfo": "Lichess bir yardım kuruluşudur ve tamamen özgür/açık kaynak kodlu bir yazılımdır. Tüm işletme maliyetleri, geliştirmeler ve içerikler yalnızca kullanıcı bağışları ile finanse edilmektedir.", "nothingToSeeHere": "Şu anda görülebilecek bir şey yok.", + "stats": "İstatistikler", "opponentLeftCounter": "{count, plural, =1{Rakibiniz oyundan ayrıldı. {count} saniye sonra galibiyet talep edebilirsiniz.} other{Rakibiniz oyundan ayrıldı. {count} saniye sonra galibiyet talep edebilirsiniz.}}", "mateInXHalfMoves": "{count, plural, =1{{count} yarım hamle sonra mat} other{{count} yarım hamlede mat}}", "nbBlunders": "{count, plural, =1{{count} vahim hata} other{{count} vahim hata}}", @@ -1295,6 +1391,178 @@ "stormXRuns": "{count, plural, =1{1 tur} other{{count} tur}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Bir kez {param2} oynadı} other{{count} kez {param2} oynadı}}", "streamerLichessStreamers": "Lichess yayıncıları", + "studyPrivate": "Gizli", + "studyMyStudies": "Çalışmalarım", + "studyStudiesIContributeTo": "Katkıda bulunduğum çalışmalar", + "studyMyPublicStudies": "Herkese açık çalışmalarım", + "studyMyPrivateStudies": "Gizli çalışmalarım", + "studyMyFavoriteStudies": "En sevdiğim çalışmalar", + "studyWhatAreStudies": "Çalışmalar nedir?", + "studyAllStudies": "Bütün çalışmalar", + "studyStudiesCreatedByX": "Çalışmalar {param} tarafından oluşturulmuştur", + "studyNoneYet": "Henüz yok.", + "studyHot": "Popüler", + "studyDateAddedNewest": "Eklenme tarihi (en yeni)", + "studyDateAddedOldest": "Eklenme tarihi (en eski)", + "studyRecentlyUpdated": "Yeni güncellenmiş", + "studyMostPopular": "En popüler", + "studyAlphabetical": "Alfabetik", + "studyAddNewChapter": "Yeni bir bölüm ekle", + "studyAddMembers": "Üye ekle", + "studyInviteToTheStudy": "Bu çalışmaya davet edin", + "studyPleaseOnlyInvitePeopleYouKnow": "Lütfen sadece tanıdığınız ve katkı sağlayacağını düşündüğünüz kişileri davet ediniz.", + "studySearchByUsername": "Kullanıcı adına göre ara", + "studySpectator": "İzleyici", + "studyContributor": "Katılımcı", + "studyKick": "Çıkar", + "studyLeaveTheStudy": "Çalışmadan ayrıl", + "studyYouAreNowAContributor": "Artık bir katılımcısınız", + "studyYouAreNowASpectator": "Artık bir izleyicisiniz", + "studyPgnTags": "PGN etiketleri", + "studyLike": "Beğen", + "studyUnlike": "Beğenmekten Vazgeç", + "studyNewTag": "Yeni etiket", + "studyCommentThisPosition": "Bu pozisyon için yorum yap", + "studyCommentThisMove": "Bu hamle için yorum yap", + "studyAnnotateWithGlyphs": "Glifler ile açıkla", + "studyTheChapterIsTooShortToBeAnalysed": "Bu bölüm analiz için çok kısa.", + "studyOnlyContributorsCanRequestAnalysis": "Sadece katılımcılar bilgisayar analizi isteyebilir.", + "studyGetAFullComputerAnalysis": "Bu varyant için ayrıntılı bir sunucu analizi yapın.", + "studyMakeSureTheChapterIsComplete": "Bu bölümü tamamladığınızdan emin olun. Sadece bir kez bilgisayar analizi talep edebilirsiniz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Senkronize edilen bütün üyeler aynı pozisyonda kalır", + "studyShareChanges": "Değişiklikleri izleyiciler ile paylaşın ve sunucuya kaydedin", + "studyPlaying": "Oynanıyor", + "studyShowEvalBar": "Değerlendirme çubuğu", + "studyFirst": "İlk", + "studyPrevious": "Önceki", + "studyNext": "Sonraki", + "studyLast": "Son", "studyShareAndExport": "Paylaş ve dışa aktar", - "studyStart": "Başlat" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Çalışma PGN'si", + "studyDownloadAllGames": "Bütün oyunları indir", + "studyChapterPgn": "Bölüm PGN'si", + "studyCopyChapterPgn": "PGN 'yi kopyala", + "studyDownloadGame": "Oyunu indir", + "studyStudyUrl": "Çalışma Adresi", + "studyCurrentChapterUrl": "Mevcut Bölümün Adresi", + "studyYouCanPasteThisInTheForumToEmbed": "Forumda gömülü olarak paylaşmak için yukarıdaki bağlantıyı kullanabilirsiniz", + "studyStartAtInitialPosition": "İlk pozisyondan başlasın", + "studyStartAtX": "{param} pozisyonundan başlasın", + "studyEmbedInYourWebsite": "İnternet sitenizde ya da blogunuzda gömülü olarak paylaşın", + "studyReadMoreAboutEmbedding": "Gömülü paylaşma hakkında", + "studyOnlyPublicStudiesCanBeEmbedded": "Yalnızca herkese açık çalışmalar gömülü paylaşılabilir!", + "studyOpen": "Aç", + "studyXBroughtToYouByY": "{param2} sana {param1} getirdi", + "studyStudyNotFound": "Böyle bir çalışma bulunamadı", + "studyEditChapter": "Bölümü düzenle", + "studyNewChapter": "Yeni bölüm", + "studyImportFromChapterX": "{param} çalışmasından içe aktar", + "studyOrientation": "Tahta yönü", + "studyAnalysisMode": "Analiz modu", + "studyPinnedChapterComment": "Bölüm üzerine yorumlar", + "studySaveChapter": "Bölümü kaydet", + "studyClearAnnotations": "Açıklamaları sil", + "studyClearVariations": "Varyasyonları sil", + "studyDeleteChapter": "Bölümü sil", + "studyDeleteThisChapter": "Bu bölüm silinsin mi? Bunun geri dönüşü yok!", + "studyClearAllCommentsInThisChapter": "Bu bölümdeki bütün yorumlar ve işaretler temizlensin mi?", + "studyRightUnderTheBoard": "Tahtanın hemen altında görünsün", + "studyNoPinnedComment": "Yok", + "studyNormalAnalysis": "Normal analiz", + "studyHideNextMoves": "Sonraki hamleleri gizle", + "studyInteractiveLesson": "Etkileşimli ders", + "studyChapterX": "Bölüm {param}", + "studyEmpty": "Boş", + "studyStartFromInitialPosition": "İlk pozisyondan başlasın", + "studyEditor": "Editör", + "studyStartFromCustomPosition": "Özel bir pozisyondan başlasın", + "studyLoadAGameByUrl": "URL ile oyun yükle", + "studyLoadAPositionFromFen": "FEN kullanarak pozisyon yükle", + "studyLoadAGameFromPgn": "PGN ile oyun yükle", + "studyAutomatic": "Otomatik", + "studyUrlOfTheGame": "Oyunun bağlantısı", + "studyLoadAGameFromXOrY": "{param1} veya {param2} kullanarak oyun yükle", + "studyCreateChapter": "Bölüm oluştur", + "studyCreateStudy": "Çalışma oluştur", + "studyEditStudy": "Çalışmayı düzenle", + "studyVisibility": "Görünürlük", + "studyPublic": "Herkese açık", + "studyUnlisted": "Liste dışı", + "studyInviteOnly": "Sadece davet edilenler", + "studyAllowCloning": "Klonlamaya izni olanlar", + "studyNobody": "Hiç kimse", + "studyOnlyMe": "Sadece ben", + "studyContributors": "Katkıda Bulunanlar", + "studyMembers": "Üyeler", + "studyEveryone": "Herkes", + "studyEnableSync": "Senkronizasyonu etkinleştir", + "studyYesKeepEveryoneOnTheSamePosition": "Evet: herkes aynı pozisyonda kalsın", + "studyNoLetPeopleBrowseFreely": "Hayır: herkes dilediğince gezinebilsin", + "studyPinnedStudyComment": "Çalışma üzerine yorumlar", + "studyStart": "Başlat", + "studySave": "Kaydet", + "studyClearChat": "Sohbeti temizle", + "studyDeleteTheStudyChatHistory": "Çalışmanın sohbet geçmişi silinsin mi? Bunun geri dönüşü yok!", + "studyDeleteStudy": "Çalışmayı sil", + "studyConfirmDeleteStudy": "Tüm çalışma silinsin mi? Bunun geri dönüşü yok! İşlemi onaylamak için çalışmanın adını yazın: {param}", + "studyWhereDoYouWantToStudyThat": "Bu çalışmayı nerede sürdürmek istersiniz?", + "studyGoodMove": "İyi hamle", + "studyMistake": "Hata", + "studyBrilliantMove": "Muhteşem hamle", + "studyBlunder": "Vahim hata", + "studyInterestingMove": "İlginç hamle", + "studyDubiousMove": "Şüpheli hamle", + "studyOnlyMove": "Tek hamle", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Eşit pozisyon", + "studyUnclearPosition": "Belirsiz pozisyon", + "studyWhiteIsSlightlyBetter": "Beyaz biraz önde", + "studyBlackIsSlightlyBetter": "Siyah biraz önde", + "studyWhiteIsBetter": "Beyaz daha üstün", + "studyBlackIsBetter": "Siyah daha üstün", + "studyWhiteIsWinning": "Beyaz kazanıyor", + "studyBlackIsWinning": "Siyah kazanıyor", + "studyNovelty": "Farklı bir açılış", + "studyDevelopment": "Gelişim hamlesi", + "studyInitiative": "Girişim", + "studyAttack": "Saldırı", + "studyCounterplay": "Karşı saldırı", + "studyTimeTrouble": "Vakit sıkıntısı", + "studyWithCompensation": "Pozisyon avantajı ile", + "studyWithTheIdea": "Plan doğrultusunda", + "studyNextChapter": "Sonraki bölüm", + "studyPrevChapter": "Önceki bölüm", + "studyStudyActions": "Çalışma seçenekleri", + "studyTopics": "Konular", + "studyMyTopics": "Konularım", + "studyPopularTopics": "Popüler konular", + "studyManageTopics": "Konuları yönet", + "studyBack": "Baştan başlat", + "studyPlayAgain": "Tekrar oyna", + "studyWhatWouldYouPlay": "Burada hangi hamleyi yapardınız?", + "studyYouCompletedThisLesson": "Tebrikler! Bu dersi tamamlandınız.", + "studyPerPage": "Sayfa başına {param}", + "studyNbChapters": "{count, plural, =1{{count} Bölüm} other{{count} Bölüm}}", + "studyNbGames": "{count, plural, =1{{count} oyun} other{{count} Oyun}}", + "studyNbMembers": "{count, plural, =1{{count} Üye} other{{count} Üye}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar} other{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar}}", + "timeagoJustNow": "şu anda", + "timeagoRightNow": "hemen şimdi", + "timeagoCompleted": "tamamlanmış", + "timeagoInNbSeconds": "{count, plural, =1{{count} saniyede} other{{count} saniyede}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} dakikada} other{{count} dakikada}}", + "timeagoInNbHours": "{count, plural, =1{{count} saatte} other{{count} saatte}}", + "timeagoInNbDays": "{count, plural, =1{{count} günde} other{{count} günde}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} haftada} other{{count} haftada}}", + "timeagoInNbMonths": "{count, plural, =1{{count} ayda} other{{count} ayda}}", + "timeagoInNbYears": "{count, plural, =1{{count} yılda} other{{count} yılda}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} dakika önce} other{{count} dakika önce}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} saat önce} other{{count} saat önce}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} gün önce} other{{count} gün önce}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} hafta önce} other{{count} hafta önce}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ay önce} other{{count} ay önce}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} yıl önce} other{{count} yıl önce}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} dakika kaldı} other{{count} dakika kaldı}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} saat kaldı} other{{count} saat kaldı}}" } \ No newline at end of file diff --git a/lib/l10n/lila_uk.arb b/lib/l10n/lila_uk.arb index d027f7aa1b..857036123a 100644 --- a/lib/l10n/lila_uk.arb +++ b/lib/l10n/lila_uk.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Усі ігри", + "mobileAreYouSure": "Ви впевнені?", + "mobileBlindfoldMode": "Наосліп", + "mobileCancelTakebackOffer": "Скасувати пропозицію повернення ходу", + "mobileClearButton": "Очистити", + "mobileCorrespondenceClearSavedMove": "Очистити збережений хід", + "mobileCustomGameJoinAGame": "Приєднатися до гри", + "mobileFeedbackButton": "Відгук", + "mobileGreeting": "Привіт, {param}", + "mobileGreetingWithoutName": "Привіт", + "mobileHideVariation": "Сховати варіанти", "mobileHomeTab": "Головна", - "mobilePuzzlesTab": "Задачі", - "mobileToolsTab": "Інструм.", - "mobileWatchTab": "Дивитися", - "mobileSettingsTab": "Налашт.", + "mobileLiveStreamers": "Стримери в прямому етері", "mobileMustBeLoggedIn": "Ви повинні ввійти, аби переглянути цю сторінку.", - "mobileSystemColors": "Системні кольори", - "mobileFeedbackButton": "Відгук", + "mobileNoSearchResults": "Немає результатів ", + "mobileNotFollowingAnyUser": "Ви ні на кого не підписані.", "mobileOkButton": "Гаразд", + "mobilePlayersMatchingSearchTerm": "Гравці з «{param}»", + "mobilePrefMagnifyDraggedPiece": "Збільшувати розмір фігури при перетягуванні", + "mobilePuzzleStormConfirmEndRun": "Ви хочете закінчити цю серію?", + "mobilePuzzleStormFilterNothingToShow": "Нічого не знайдено, будь ласка, змініть фільтри", + "mobilePuzzleStormNothingToShow": "Нічого показати. Зіграйте в гру Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Розв'яжіть якомога більше задач за 3 хвилини.", + "mobilePuzzleStreakAbortWarning": "Ви втратите поточну серію, і ваш рахунок буде збережено.", + "mobilePuzzleThemesSubtitle": "Розв'язуйте задачі з улюбленими дебютами або обирайте тему.", + "mobilePuzzlesTab": "Задачі", + "mobileRecentSearches": "Недавні пошуки", "mobileSettingsHapticFeedback": "Вібрація при ході", "mobileSettingsImmersiveMode": "Повноекранний режим", "mobileSettingsImmersiveModeSubtitle": "Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.", - "mobileNotFollowingAnyUser": "Ви ні на кого не підписані.", - "mobileAllGames": "Усі ігри", - "mobileRecentSearches": "Недавні пошуки", - "mobileClearButton": "Очистити", - "mobilePlayersMatchingSearchTerm": "Гравці з «{param}»", - "mobileNoSearchResults": "Немає результатів ", - "mobileAreYouSure": "Ви впевнені?", - "mobilePuzzleStreakAbortWarning": "Ви втратите поточну серію, і ваш рахунок буде збережено.", - "mobilePuzzleStormNothingToShow": "Нічого показати. Зіграйте в гру Puzzle Storm.", - "mobileSharePuzzle": "Поділитися задачею", - "mobileShareGameURL": "Поділитися посиланням на гру", + "mobileSettingsTab": "Налашт.", "mobileShareGamePGN": "Поділитися PGN", + "mobileShareGameURL": "Поділитися посиланням на гру", "mobileSharePositionAsFEN": "Поділитися FEN", - "mobileShowVariations": "Показати варіанти", - "mobileHideVariation": "Сховати варіанти", + "mobileSharePuzzle": "Поділитися задачею", "mobileShowComments": "Показати коментарі", - "mobilePuzzleStormConfirmEndRun": "Ви хочете закінчити цю серію?", - "mobilePuzzleStormFilterNothingToShow": "Нічого не знайдено, будь ласка, змініть фільтри", - "mobileCancelTakebackOffer": "Скасувати пропозицію повернення ходу", - "mobileCancelDrawOffer": "Скасувати пропозицію нічиєї", - "mobileWaitingForOpponentToJoin": "Очікування на суперника...", - "mobileBlindfoldMode": "Наосліп", - "mobileLiveStreamers": "Стримери в прямому етері", - "mobileCustomGameJoinAGame": "Приєднатися до гри", - "mobileCorrespondenceClearSavedMove": "Очистити збережений хід", - "mobileSomethingWentWrong": "Щось пішло не так.", "mobileShowResult": "Показати результат", - "mobilePuzzleThemesSubtitle": "Розв'язуйте задачі з улюбленими дебютами або обирайте тему.", - "mobilePuzzleStormSubtitle": "Розв'яжіть якомога більше задач за 3 хвилини.", - "mobileGreeting": "Привіт, {param}", - "mobileGreetingWithoutName": "Привіт", + "mobileShowVariations": "Показати варіанти", + "mobileSomethingWentWrong": "Щось пішло не так.", + "mobileSystemColors": "Системні кольори", + "mobileTheme": "Тема", + "mobileToolsTab": "Інструм.", + "mobileWaitingForOpponentToJoin": "Очікування на суперника...", + "mobileWatchTab": "Дивитися", "activityActivity": "Активність", "activityHostedALiveStream": "Проведено пряму трансляцію", "activityRankedInSwissTournament": "Зайняв #{param1} місце в {param2}", @@ -54,6 +55,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Зроблено {count} хід} few{Зроблено {count} ходи} many{Зроблено {count} ходів} other{Зроблено {count} ходів}}", "activityInNbCorrespondenceGames": "{count, plural, =1{у {count} заочній грі} few{у {count} заочних іграх} many{у {count} заочних іграх} other{у {count} заочних ігор}}", "activityCompletedNbGames": "{count, plural, =1{Зіграно {count} заочну гру} few{Зіграно {count} заочні гри} many{Зіграно {count} заочних ігор} other{Зіграно {count} заочних ігор}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Зіграно {count} {param2} заочну гру} few{Зіграно {count} {param2} заочні гри} many{Зіграно {count} {param2} заочних ігор} other{Зіграно {count} {param2} заочних ігор}}", "activityFollowedNbPlayers": "{count, plural, =1{Підписався на {count} гравця} few{Почав спостерігати за {count} гравцями} many{Почав спостерігати за {count} гравцями} other{Почав спостерігати за {count} гравцями}}", "activityGainedNbFollowers": "{count, plural, =1{Отримав {count} нового підписника} few{Отримав {count} нових підписників} many{Отримав {count} нових підписників} other{Отримав {count} нових підписників}}", "activityHostedNbSimuls": "{count, plural, =1{Провів {count} сеанс одночасної гри} few{Провів {count} сеанси одночасної гри} many{Провів {count} сеансів одночасної гри} other{Провів {count} сеансів одночасної гри}}", @@ -64,7 +66,75 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Завершив {count} турнір за швейцарською системою} few{Завершив {count} турніри за швейцарською системою} many{Завершив {count} турнірів за швейцарською системою} other{Завершив {count} турніри за швейцарською системою}}", "activityJoinedNbTeams": "{count, plural, =1{Приєднався до {count} команди} few{Приєднався до {count} команд} many{Приєднався до {count} команд} other{Приєднався до {count} команд}}", "broadcastBroadcasts": "Трансляції", + "broadcastMyBroadcasts": "Мої трансляції", "broadcastLiveBroadcasts": "Онлайн трансляції турнірів", + "broadcastBroadcastCalendar": "Календар трансляцій", + "broadcastNewBroadcast": "Нова трансляція", + "broadcastSubscribedBroadcasts": "Обрані трансляції", + "broadcastAboutBroadcasts": "Про трансляцію", + "broadcastHowToUseLichessBroadcasts": "Як користуватися Lichess трансляціями.", + "broadcastTheNewRoundHelp": "У новому раунді будуть ті самі учасники та редактори, що й у попередньому.", + "broadcastAddRound": "Додати раунд", + "broadcastOngoing": "Поточні", + "broadcastUpcoming": "Майбутні", + "broadcastCompleted": "Завершені", + "broadcastCompletedHelp": "Lichess виявляє завершення раунду на основі ігор. Використовуйте цей перемикач якщо немає джерела.", + "broadcastRoundName": "Назва раунду", + "broadcastRoundNumber": "Номер раунду", + "broadcastTournamentName": "Назва турніру", + "broadcastTournamentDescription": "Короткий опис турніру", + "broadcastFullDescription": "Повний опис події", + "broadcastFullDescriptionHelp": "Необов'язковий довгий опис трансляції. Наявна розмітка {param1}. Довжина має бути менша ніж {param2} символів.", + "broadcastSourceSingleUrl": "Адреса джерела PGN", + "broadcastSourceUrlHelp": "Посилання, яке Lichess перевірятиме, щоб отримати оновлення PGN. Воно має бути загальнодоступним в Інтернеті.", + "broadcastSourceGameIds": "До 64 ігрових ID Lichess, відокремлені пробілами.", + "broadcastStartDateHelp": "За бажанням, якщо ви знаєте, коли починається подія", + "broadcastCurrentGameUrl": "Посилання на поточну гру", + "broadcastDownloadAllRounds": "Завантажити всі тури", + "broadcastResetRound": "Скинути цей раунд", + "broadcastDeleteRound": "Видалити цей раунд", + "broadcastDefinitivelyDeleteRound": "Видалити всі ігри цього раунду.", + "broadcastDeleteAllGamesOfThisRound": "Видалити всі ігри цього раунду. Джерело має бути активним для того, щоб повторно відтворити його.", + "broadcastEditRoundStudy": "Редагувати дослідження раунду", + "broadcastDeleteTournament": "Видалити турнір", + "broadcastDefinitivelyDeleteTournament": "Остаточно видалити весь турнір, всі його раунди та всі його ігри.", + "broadcastShowScores": "Показувати результати гравців за результатами гри", + "broadcastReplacePlayerTags": "За бажанням: замінити імена, рейтинги та титули гравців", + "broadcastFideFederations": "Федерації FIDE", + "broadcastTop10Rating": "Топ 10 рейтингу", + "broadcastFidePlayers": "Гравці FIDE", + "broadcastFidePlayerNotFound": "Гравця FIDE не знайдено", + "broadcastFideProfile": "Профіль FIDE", + "broadcastFederation": "Федерація", + "broadcastAgeThisYear": "Вік цього року", + "broadcastUnrated": "Без рейтингу", + "broadcastRecentTournaments": "Нещодавні турніри", + "broadcastOpenLichess": "Відкрити в Lichess", + "broadcastTeams": "Команди", + "broadcastBoards": "Дошки", + "broadcastOverview": "Огляд", + "broadcastUploadImage": "Завантажити зображення турніру", + "broadcastNoBoardsYet": "Ще немає дощок. Вони з'являться, коли ігри будуть завантажені.", + "broadcastStartVerySoon": "Трансляція розпочнеться дуже скоро.", + "broadcastNotYetStarted": "Трансляція ще не розпочалася.", + "broadcastOfficialWebsite": "Офіційний вебсайт", + "broadcastStandings": "Турнірна таблиця", + "broadcastOfficialStandings": "Офіційна турнірна таблиця", + "broadcastIframeHelp": "Більше опцій на {param}", + "broadcastEmbedThisBroadcast": "Вбудувати цю трансляцію на своєму сайті", + "broadcastEmbedThisRound": "Вбудувати {param} на своєму сайті", + "broadcastRatingDiff": "Різниця у рейтингу", + "broadcastGamesThisTournament": "Ігри в цьому турнірі", + "broadcastScore": "Очки", + "broadcastAllTeams": "Усі команди", + "broadcastTournamentFormat": "Формат турніру", + "broadcastTournamentLocation": "Місце турніру", + "broadcastTopPlayers": "Найкращі гравці", + "broadcastTimezone": "Часовий пояс", + "broadcastFideRatingCategory": "Категорія рейтингу FIDE", + "broadcastOptionalDetails": "Додаткові деталі", + "broadcastPastBroadcasts": "Минулі трансляції", + "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляція} few{{count} трансляції} many{{count} трансляцій} other{{count} трансляцій}}", "challengeChallengesX": "Виклики: {param1}", "challengeChallengeToPlay": "Виклик на гру", "challengeChallengeDeclined": "Виклик відхилено", @@ -163,7 +233,7 @@ "preferencesMoveConfirmation": "Підтвердження ходу", "preferencesExplainCanThenBeTemporarilyDisabled": "Можна вимкнути під час гри в меню дошки", "preferencesInCorrespondenceGames": "У заочних партіях", - "preferencesCorrespondenceAndUnlimited": "За листуванням та необмежені", + "preferencesCorrespondenceAndUnlimited": "Заочні та необмежені", "preferencesConfirmResignationAndDrawOffers": "Підтверджувати повернення ходу та пропозиції нічий", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "Спосіб рокіровки", "preferencesCastleByMovingTwoSquares": "Перемістити короля на два поля", @@ -188,6 +258,7 @@ "preferencesNotifyWeb": "Браузер", "preferencesNotifyDevice": "Пристрій", "preferencesBellNotificationSound": "Звук сповіщення", + "preferencesBlindfold": "Наосліп", "puzzlePuzzles": "Задачі", "puzzlePuzzleThemes": "Теми задач", "puzzleRecommended": "Рекомендовані", @@ -383,8 +454,8 @@ "puzzleThemeXRayAttackDescription": "Фігура, що атакує чи захищає поле, що знаходиться за фігурою суперника.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Суперник обмежений в своїх ходах, а кожен хід погіршує його позицію.", - "puzzleThemeHealthyMix": "Здорова суміш", - "puzzleThemeHealthyMixDescription": "Всього потроху. Ви не знаєте, чого очікувати, тому готуйтесь до всього! Як у справжніх партіях.", + "puzzleThemeMix": "Усього потрохи", + "puzzleThemeMixDescription": "Усього потрохи. Ви не знатимете, чого очікувати, тому готуйтеся до всього! Як у справжніх партіях.", "puzzleThemePlayerGames": "Ігри гравця", "puzzleThemePlayerGamesDescription": "Пошук задач, згенерованих з ваших ігор або з ігор інших гравців.", "puzzleThemePuzzleDownloadInformation": "Ці задачі є у публічному доступі та можуть бути завантажені з {param}.", @@ -505,7 +576,6 @@ "replayMode": "Режим повтору", "realtimeReplay": "У реальному часі", "byCPL": "Цікаве", - "openStudy": "Почати дослідження", "enable": "Увімкнути", "bestMoveArrow": "Стрілка «Найкращий хід»", "showVariationArrows": "Показати стрілки для варіантів", @@ -515,7 +585,6 @@ "memory": "Пам'ять", "infiniteAnalysis": "Нескінченний аналіз", "removesTheDepthLimit": "Знімає обмеження на глибину аналізу - ваш комп’ютер стане теплішим", - "engineManager": "Менеджер рушія", "blunder": "Груба помилка", "mistake": "Помилка", "inaccuracy": "Неточність", @@ -597,6 +666,7 @@ "rank": "Місце", "rankX": "Місце: {param}", "gamesPlayed": "Ігор зіграно", + "ok": "Гаразд", "cancel": "Скасувати", "whiteTimeOut": "Час білих вийшов", "blackTimeOut": "Час чорних вийшов", @@ -713,7 +783,6 @@ "block": "Заблокувати", "blocked": "Заблоковано", "unblock": "Розблокувати", - "followsYou": "Спостерігає за вами", "xStartedFollowingY": "{param1} починає спостерігати за {param2}", "more": "Більше", "memberSince": "Зареєстрований з", @@ -819,7 +888,9 @@ "cheat": "Нечесна гра", "troll": "Тролінг", "other": "Інше", - "reportDescriptionHelp": "Вставте посилання на гру (ігри) та поясніть, що не так із поведінкою цього користувача. Не пишіть просто \"він шахраює\", а розкажіть, як ви дійшли до такого висновку. Вашу скаргу розглянуть швидше, якщо ви напишете її англійською.", + "reportCheatBoostHelp": "Вставте посилання на гру або ігри та поясніть, що не так з поведінкою цього користувача. Не кажіть «він нечесно грав», а поясніть, як ви прийшли до такого висновку.", + "reportUsernameHelp": "Поясніть, що саме в цьому імені користувача є образливе. Не кажіть «воно образливе/неприйнятне», а поясніть, чому ви так вважаєте, особливо коли образа заплутана, не англійською, на сленгу, чи є посиланням на щось історичне/культурне.", + "reportProcessedFasterInEnglish": "Ваша скарга оброблятиметься швидше, якщо буде написана англійською.", "error_provideOneCheatedGameLink": "Будь ласка, додайте посилання на хоча б одну нечесну гру.", "by": "від {param}", "importedByX": "Завантажено гравцем - {param}", @@ -1217,6 +1288,7 @@ "showMeEverything": "Показати все", "lichessPatronInfo": "Lichess — це благодійне й абсолютно безкоштовне програмне забезпечення з відкритим кодом.\nУсі витрати на обслуговування, розробку й контент фінансуються виключно пожертвуваннями користувачів.", "nothingToSeeHere": "Поки тут нічого немає.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.} few{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунди.} many{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.} other{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} напівхід} few{Мат в {count} напівходи} many{Мат в {count} напівходів} other{Мат у {count} напівходів}}", "nbBlunders": "{count, plural, =1{{count} груба помилка} few{{count} грубі помилки} many{{count} грубих помилок} other{{count} грубих помилок}}", @@ -1313,6 +1385,178 @@ "stormXRuns": "{count, plural, =1{1 серія} few{{count} серії} many{{count} серій} other{{count} серій}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Зіграна одна серія в {param2}} few{Зіграно {count} серії в {param2}} many{Зіграно {count} серій в {param2}} other{Зіграно {count} серій в {param2}}}", "streamerLichessStreamers": "Стримери Lichess", + "studyPrivate": "Приватне", + "studyMyStudies": "Мої дослідження", + "studyStudiesIContributeTo": "Дослідження, яким я сприяю", + "studyMyPublicStudies": "Мої публічні дослідження", + "studyMyPrivateStudies": "Мої приватні дослідження", + "studyMyFavoriteStudies": "Мої улюблені дослідження", + "studyWhatAreStudies": "Що таке дослідження?", + "studyAllStudies": "Усі дослідження", + "studyStudiesCreatedByX": "Дослідження, створені {param}", + "studyNoneYet": "Ще немає.", + "studyHot": "Активні", + "studyDateAddedNewest": "Дата додавання (старіші)", + "studyDateAddedOldest": "Дата додавання (старіші)", + "studyRecentlyUpdated": "Нещодавно оновлені", + "studyMostPopular": "Найпопулярніші", + "studyAlphabetical": "За алфавітом", + "studyAddNewChapter": "Додати новий розділ", + "studyAddMembers": "Додати учасників", + "studyInviteToTheStudy": "Запросити до дослідження", + "studyPleaseOnlyInvitePeopleYouKnow": "Будь ласка запрошуйте лише людей, яких ви знаєте, і які хочуть активно долучитися до цього дослідження.", + "studySearchByUsername": "Пошук за іменем користувача", + "studySpectator": "Глядач", + "studyContributor": "Співавтор", + "studyKick": "Вигнати", + "studyLeaveTheStudy": "Покинути дослідження", + "studyYouAreNowAContributor": "Тепер ви співавтор", + "studyYouAreNowASpectator": "Тепер ви глядач", + "studyPgnTags": "Теги PGN", + "studyLike": "Подобається", + "studyUnlike": "Не подобається", + "studyNewTag": "Новий тег", + "studyCommentThisPosition": "Коментувати цю позицію", + "studyCommentThisMove": "Коментувати цей хід", + "studyAnnotateWithGlyphs": "Додати символьну анотацію", + "studyTheChapterIsTooShortToBeAnalysed": "Розділ занадто короткий для аналізу.", + "studyOnlyContributorsCanRequestAnalysis": "Лише співавтори дослідження можуть дати запит на комп'ютерний аналіз.", + "studyGetAFullComputerAnalysis": "Отримати повний серверний комп'ютерний аналіз головної лінії.", + "studyMakeSureTheChapterIsComplete": "Переконайтесь, що розділ завершено. Ви можете дати запит на аналіз лише один раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Усі синхронізовані учасники залишаються на тій же позиції", + "studyShareChanges": "Поділитися змінами з глядачами та зберегти їх на сервері", + "studyPlaying": "Активні", + "studyShowEvalBar": "Шкала оцінки", + "studyFirst": "Перша", + "studyPrevious": "Попередня", + "studyNext": "Наступна", + "studyLast": "Остання", "studyShareAndExport": "Надсилання та експорт", - "studyStart": "Почати" + "studyCloneStudy": "Клонувати", + "studyStudyPgn": "PGN дослідження", + "studyDownloadAllGames": "Завантажити всі партії", + "studyChapterPgn": "PGN розділу", + "studyCopyChapterPgn": "Скопіювати PGN", + "studyDownloadGame": "Завантажити гру", + "studyStudyUrl": "Посилання на дослідження", + "studyCurrentChapterUrl": "Посилання на цей розділ", + "studyYouCanPasteThisInTheForumToEmbed": "Ви можете вставити цей код на форумі для вбудування", + "studyStartAtInitialPosition": "Старт з початкової позиції", + "studyStartAtX": "Почати з {param}", + "studyEmbedInYourWebsite": "Вбудувати на своєму сайті", + "studyReadMoreAboutEmbedding": "Докладніше про вбудовування", + "studyOnlyPublicStudiesCanBeEmbedded": "Лише публічні дослідження можна вбудовувати!", + "studyOpen": "Відкрити", + "studyXBroughtToYouByY": "{param1} надано вам {param2}", + "studyStudyNotFound": "Дослідження не знайдено", + "studyEditChapter": "Редагувати розділ", + "studyNewChapter": "Новий розділ", + "studyImportFromChapterX": "Імпортувати з {param}", + "studyOrientation": "Орієнтація", + "studyAnalysisMode": "Режим аналізу", + "studyPinnedChapterComment": "Закріплений коментар розділу", + "studySaveChapter": "Зберегти розділ", + "studyClearAnnotations": "Очистити анотацію", + "studyClearVariations": "Очистити анотацію", + "studyDeleteChapter": "Видалити розділ", + "studyDeleteThisChapter": "Видалити цей розділ? Відновити буде неможливо!", + "studyClearAllCommentsInThisChapter": "Очистити всі коментарі та позначки з цього розділу?", + "studyRightUnderTheBoard": "Відразу під шахівницею", + "studyNoPinnedComment": "Немає", + "studyNormalAnalysis": "Звичайний аналіз", + "studyHideNextMoves": "Приховати наступні ходи", + "studyInteractiveLesson": "Інтерактивний урок", + "studyChapterX": "Розділ {param}", + "studyEmpty": "Порожній", + "studyStartFromInitialPosition": "Старт з початкової позиції", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Почати з обраної позиції", + "studyLoadAGameByUrl": "Завантажте гру за посиланням", + "studyLoadAPositionFromFen": "Завантажити позицію з FEN", + "studyLoadAGameFromPgn": "Завантажити гру з PGN", + "studyAutomatic": "Автоматично", + "studyUrlOfTheGame": "Посилання на гру", + "studyLoadAGameFromXOrY": "Завантажити гру з {param1} або {param2}", + "studyCreateChapter": "Створити розділ", + "studyCreateStudy": "Створити дослідження", + "studyEditStudy": "Редагування дослідження", + "studyVisibility": "Видимість", + "studyPublic": "Публічне", + "studyUnlisted": "Поза списком", + "studyInviteOnly": "Лише за запрошенням", + "studyAllowCloning": "Дозволити копіювання", + "studyNobody": "Ніхто", + "studyOnlyMe": "Лише я", + "studyContributors": "Співавтори", + "studyMembers": "Учасники", + "studyEveryone": "Всі", + "studyEnableSync": "Увімкнути синхронізацію", + "studyYesKeepEveryoneOnTheSamePosition": "Так: однакова позиція для всіх", + "studyNoLetPeopleBrowseFreely": "Ні: дозволити вільний перегляд", + "studyPinnedStudyComment": "Закріплений коментар дослідження", + "studyStart": "Почати", + "studySave": "Зберегти", + "studyClearChat": "Очистити чат", + "studyDeleteTheStudyChatHistory": "Видалити історію чату дослідження? Відновити буде неможливо!", + "studyDeleteStudy": "Видалити дослідження", + "studyConfirmDeleteStudy": "Ви дійсно бажаєте видалити все дослідження? Назад дороги немає! Введіть назву дослідження для підтвердження: {param}", + "studyWhereDoYouWantToStudyThat": "Де ви хочете це дослідити?", + "studyGoodMove": "Хороший хід", + "studyMistake": "Помилка", + "studyBrilliantMove": "Блискучий хід", + "studyBlunder": "Груба помилка", + "studyInterestingMove": "Цікавий хід", + "studyDubiousMove": "Сумнівний хід", + "studyOnlyMove": "Єдиний хід", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Рівна позиція", + "studyUnclearPosition": "Незрозуміла позиція", + "studyWhiteIsSlightlyBetter": "Позиція білих трохи краще", + "studyBlackIsSlightlyBetter": "Позиція чорних трохи краще", + "studyWhiteIsBetter": "Позиція білих краще", + "studyBlackIsBetter": "Позиція чорних краще", + "studyWhiteIsWinning": "Білі перемагають", + "studyBlackIsWinning": "Чорні перемагають", + "studyNovelty": "Новинка", + "studyDevelopment": "Розвиток", + "studyInitiative": "Ініціатива", + "studyAttack": "Атака", + "studyCounterplay": "Контргра", + "studyTimeTrouble": "Цейтнот", + "studyWithCompensation": "З компенсацією", + "studyWithTheIdea": "З ідеєю", + "studyNextChapter": "Наступний розділ", + "studyPrevChapter": "Попередній розділ", + "studyStudyActions": "Команди дослідження", + "studyTopics": "Теми", + "studyMyTopics": "Мої теми", + "studyPopularTopics": "Популярні теми", + "studyManageTopics": "Управління темами", + "studyBack": "Назад", + "studyPlayAgain": "Грати знову", + "studyWhatWouldYouPlay": "Що б ви грали в цій позиції?", + "studyYouCompletedThisLesson": "Вітаємо! Ви завершили цей урок.", + "studyPerPage": "{param} на сторінку", + "studyNbChapters": "{count, plural, =1{{count} розділ} few{{count} розділи} many{{count} розділів} other{{count} розділи}}", + "studyNbGames": "{count, plural, =1{{count} Партія} few{{count} Партії} many{{count} Партій} other{{count} Партій}}", + "studyNbMembers": "{count, plural, =1{{count} учасник} few{{count} учасники} many{{count} учасників} other{{count} учасників}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставте ваш PGN текст тут, до {count} гри} few{Вставте ваш PGN текст тут, до {count} ігор} many{Вставте ваш PGN текст тут, до {count} ігор} other{Вставте ваш PGN текст тут, до {count} ігор}}", + "timeagoJustNow": "щойно", + "timeagoRightNow": "зараз", + "timeagoCompleted": "завершено", + "timeagoInNbSeconds": "{count, plural, =1{за {count} секунду} few{за {count} секунди} many{за {count} секунд} other{за {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{за {count} хвилину} few{за {count} хвилини} many{за {count} хвилин} other{за {count} хвилини}}", + "timeagoInNbHours": "{count, plural, =1{за {count} годину} few{за {count} години} many{за {count} годин} other{за {count} години}}", + "timeagoInNbDays": "{count, plural, =1{за {count} день} few{за {count} дні} many{за {count} днів} other{за {count} дня}}", + "timeagoInNbWeeks": "{count, plural, =1{за {count} тиждень} few{за {count} тижні} many{за {count} тижнів} other{за {count} тижня}}", + "timeagoInNbMonths": "{count, plural, =1{за {count} місяць} few{за {count} місяці} many{за {count} місяців} other{за {count} місяця}}", + "timeagoInNbYears": "{count, plural, =1{за {count} рік} few{за {count} роки} many{за {count} років} other{за {count} року}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} хвилину тому} few{{count} хвилини тому} many{{count} хвилин тому} other{{count} хвилини тому}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} годину тому} few{{count} години тому} many{{count} годин тому} other{{count} години тому}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} день тому} few{{count} дні тому} many{{count} днів тому} other{{count} дня тому}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} тиждень тому} few{{count} тижні тому} many{{count} тижнів тому} other{{count} тижня тому}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} місяць тому} few{{count} місяці тому} many{{count} місяців тому} other{{count} місяця тому}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} рік тому} few{{count} роки тому} many{{count} років тому} other{{count} року тому}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{залишилася {count} хвилина} few{залишилося {count} хвилини} many{залишилося {count} хвилин} other{залишилося {count} хвилини}}", + "timeagoNbHoursRemaining": "{count, plural, =1{залишилася {count} година} few{залишилося {count} години} many{залишилося {count} годин} other{залишилося {count} години}}" } \ No newline at end of file diff --git a/lib/l10n/lila_vi.arb b/lib/l10n/lila_vi.arb index a6d4b58b6a..6cbaf6c5ad 100644 --- a/lib/l10n/lila_vi.arb +++ b/lib/l10n/lila_vi.arb @@ -1,48 +1,52 @@ { + "mobileAllGames": "Tất cả ván đấu", + "mobileAreYouSure": "Bạn chắc chứ?", + "mobileBlindfoldMode": "Bịt mắt", + "mobileCancelTakebackOffer": "Hủy đề nghị đi lại", + "mobileClearButton": "Xóa", + "mobileCorrespondenceClearSavedMove": "Xóa nước cờ đã lưu", + "mobileCustomGameJoinAGame": "Tham gia một ván cờ", + "mobileFeedbackButton": "Phản hồi", + "mobileGreeting": "Xin chào, {param}", + "mobileGreetingWithoutName": "Xin chào", + "mobileHideVariation": "Ẩn các biến", "mobileHomeTab": "Trang chủ", - "mobilePuzzlesTab": "Câu đố", - "mobileToolsTab": "Công cụ", - "mobileWatchTab": "Xem", - "mobileSettingsTab": "Cài đặt", + "mobileLiveStreamers": "Các Streamer phát trực tiếp", "mobileMustBeLoggedIn": "Bạn phải đăng nhập để xem trang này.", - "mobileSystemColors": "Màu hệ thống", - "mobileFeedbackButton": "Phản hồi", + "mobileNoSearchResults": "Không có kết quả", + "mobileNotFollowingAnyUser": "Bạn chưa theo dõi người dùng nào.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "chơi với \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Phóng to quân cờ được kéo", + "mobilePuzzleStormConfirmEndRun": "Bạn có muốn kết thúc lượt chạy này không?", + "mobilePuzzleStormFilterNothingToShow": "Không có gì để hiển thị, vui lòng thay đổi bộ lọc", + "mobilePuzzleStormNothingToShow": "Không có gì để xem. Chơi một vài ván Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Giải càng nhiều câu đố càng tốt trong 3 phút.", + "mobilePuzzleStreakAbortWarning": "Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.", + "mobilePuzzleThemesSubtitle": "Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.", + "mobilePuzzlesTab": "Câu đố", + "mobileRecentSearches": "Tìm kiếm gần đây", "mobileSettingsHapticFeedback": "Rung phản hồi", "mobileSettingsImmersiveMode": "Chế độ toàn màn hình", "mobileSettingsImmersiveModeSubtitle": "Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.", - "mobileNotFollowingAnyUser": "Bạn chưa theo dõi người dùng nào.", - "mobileAllGames": "Tất cả ván đấu", - "mobileRecentSearches": "Tìm kiếm gần đây", - "mobileClearButton": "Xóa", - "mobilePlayersMatchingSearchTerm": "chơi với \"{param}\"", - "mobileNoSearchResults": "Không có kết quả", - "mobileAreYouSure": "Bạn chắc chứ?", - "mobilePuzzleStreakAbortWarning": "Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.", - "mobilePuzzleStormNothingToShow": "Không có gì để xem. Chơi một vài ván Puzzle Storm.", - "mobileSharePuzzle": "Chia sẻ câu đố này", - "mobileShareGameURL": "Chia sẻ URL ván cờ", + "mobileSettingsTab": "Cài đặt", "mobileShareGamePGN": "Chia sẻ tập tin PGN", + "mobileShareGameURL": "Chia sẻ URL ván cờ", "mobileSharePositionAsFEN": "Chia sẻ thế cờ dạng FEN", - "mobileShowVariations": "Hiện các biến", - "mobileHideVariation": "Ẩn các biến", + "mobileSharePuzzle": "Chia sẻ câu đố này", "mobileShowComments": "Hiển thị bình luận", - "mobilePuzzleStormConfirmEndRun": "Bạn có muốn kết thúc lượt chạy này không?", - "mobilePuzzleStormFilterNothingToShow": "Không có gì để hiển thị, vui lòng thay đổi bộ lọc", - "mobileCancelTakebackOffer": "Hủy đề nghị đi lại", - "mobileCancelDrawOffer": "Hủy đề nghị hòa", - "mobileWaitingForOpponentToJoin": "Đang chờ đối thủ tham gia...", - "mobileBlindfoldMode": "Bịt mắt", - "mobileLiveStreamers": "Các Streamer phát trực tiếp", - "mobileCustomGameJoinAGame": "Tham gia một ván cờ", - "mobileCorrespondenceClearSavedMove": "Xóa nước cờ đã lưu", - "mobileSomethingWentWrong": "Đã xảy ra lỗi.", "mobileShowResult": "Xem kết quả", - "mobilePuzzleThemesSubtitle": "Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.", + "mobileShowVariations": "Hiện các biến", + "mobileSomethingWentWrong": "Đã xảy ra lỗi.", + "mobileSystemColors": "Màu hệ thống", + "mobileTheme": "Giao diện", + "mobileToolsTab": "Công cụ", + "mobileWaitingForOpponentToJoin": "Đang chờ đối thủ tham gia...", + "mobileWatchTab": "Xem", "activityActivity": "Hoạt động", "activityHostedALiveStream": "Đã phát trực tiếp", "activityRankedInSwissTournament": "Đứng hạng {param1} trong giải {param2}", - "activitySignedUp": "Đã ghi danh ở lichess.org", + "activitySignedUp": "Đã ghi danh tại lichess.org", "activitySupportedNbMonths": "{count, plural, other{Đã ủng hộ lichess.org {count} tháng với tư cách là một {param2}}}", "activityPracticedNbPositions": "{count, plural, other{Đã luyện tập {count} thế cờ trên {param2}}}", "activitySolvedNbPuzzles": "{count, plural, other{Đã giải {count} câu đố}}", @@ -51,7 +55,8 @@ "activityPlayedNbMoves": "{count, plural, other{Đã đi {count} nước}}", "activityInNbCorrespondenceGames": "{count, plural, other{trong {count} ván cờ qua thư}}", "activityCompletedNbGames": "{count, plural, other{Đã hoàn thành {count} ván cờ qua thư}}", - "activityFollowedNbPlayers": "{count, plural, other{Đã theo dõi {count} người chơi}}", + "activityCompletedNbVariantGames": "{count, plural, other{Đã hoàn thành {count} ván cờ qua thư {param2}}}", + "activityFollowedNbPlayers": "{count, plural, other{Đã theo dõi {count} kỳ thủ}}", "activityGainedNbFollowers": "{count, plural, other{Đạt được {count} người theo dõi mới}}", "activityHostedNbSimuls": "{count, plural, other{Đã chủ trì {count} sự kiện cờ đồng loạt}}", "activityJoinedNbSimuls": "{count, plural, other{Đã tham gia {count} sự kiện cờ đồng loạt}}", @@ -61,7 +66,82 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{Đã hoàn thành {count} giải đấu hệ Thụy Sĩ}}", "activityJoinedNbTeams": "{count, plural, other{Đã tham gia {count} đội}}", "broadcastBroadcasts": "Các phát sóng", + "broadcastMyBroadcasts": "Các phát sóng của tôi", "broadcastLiveBroadcasts": "Các giải đấu phát sóng trực tiếp", + "broadcastBroadcastCalendar": "Lịch phát sóng", + "broadcastNewBroadcast": "Phát sóng trực tiếp mới", + "broadcastSubscribedBroadcasts": "Các phát sóng đã đăng ký theo dõi", + "broadcastAboutBroadcasts": "Giới thiệu về phát sóng", + "broadcastHowToUseLichessBroadcasts": "Cách sử dụng Phát sóng của Lichess.", + "broadcastTheNewRoundHelp": "Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước.", + "broadcastAddRound": "Thêm vòng", + "broadcastOngoing": "Đang diễn ra", + "broadcastUpcoming": "Sắp diễn ra", + "broadcastCompleted": "Đã hoàn thành", + "broadcastCompletedHelp": "Lichess phát hiện việc hoàn thành vòng đấu dựa trên các ván đấu nguồn. Sử dụng nút chuyển đổi này nếu không có nguồn.", + "broadcastRoundName": "Tên vòng", + "broadcastRoundNumber": "Vòng đấu số", + "broadcastTournamentName": "Tên giải đấu", + "broadcastTournamentDescription": "Mô tả ngắn giải đấu", + "broadcastFullDescription": "Mô tả đầy đủ giải đấu", + "broadcastFullDescriptionHelp": "Tùy chọn mô tả dài về giải đấu. Có thể sử dụng {param1}. Độ dài phải nhỏ hơn {param2} ký tự.", + "broadcastSourceSingleUrl": "URL Nguồn PGN", + "broadcastSourceUrlHelp": "URL mà Lichess sẽ khảo sát để nhận cập nhật PGN. Nó phải được truy cập công khai từ Internet.", + "broadcastSourceGameIds": "Tối đa 64 ID ván cờ trên Lichess, phân tách bằng dấu cách.", + "broadcastStartDateTimeZone": "Thời gian bắt đầu của giải theo múi giờ địa phương: {param}", + "broadcastStartDateHelp": "Tùy chọn, nếu bạn biết khi nào sự kiện bắt đầu", + "broadcastCurrentGameUrl": "URL ván đấu hiện tại", + "broadcastDownloadAllRounds": "Tải về tất cả ván đấu", + "broadcastResetRound": "Đặt lại vòng này", + "broadcastDeleteRound": "Xóa vòng này", + "broadcastDefinitivelyDeleteRound": "Dứt khoát xóa tất cả vòng đấu và các ván đấu trong đó.", + "broadcastDeleteAllGamesOfThisRound": "Xóa toàn bộ ván cờ trong vòng này. Để tạo lại chúng bạn cần thêm lại nguồn.", + "broadcastEditRoundStudy": "Chỉnh sửa vòng nghiên cứu", + "broadcastDeleteTournament": "Xóa giải đấu này", + "broadcastDefinitivelyDeleteTournament": "Xóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó.", + "broadcastShowScores": "Hiển thị điểm số của người chơi dựa trên kết quả ván đấu", + "broadcastReplacePlayerTags": "Tùy chọn: biệt danh, hệ số Elo và danh hiệu", + "broadcastFideFederations": "Các liên đoàn FIDE", + "broadcastTop10Rating": "Hệ số Elo top 10", + "broadcastFidePlayers": "Các kỳ thủ FIDE", + "broadcastFidePlayerNotFound": "Không tìm thấy kỳ thủ FIDE", + "broadcastFideProfile": "Hồ sơ FIDE", + "broadcastFederation": "Liên đoàn", + "broadcastAgeThisYear": "Tuổi năm nay", + "broadcastUnrated": "Chưa xếp hạng", + "broadcastRecentTournaments": "Các giải đấu tham gia gần đây", + "broadcastOpenLichess": "Mở trên Lichess", + "broadcastTeams": "Các đội", + "broadcastBoards": "Các bàn đấu", + "broadcastOverview": "Tổng quan", + "broadcastSubscribeTitle": "Đăng ký để được thông báo khi mỗi vòng bắt đầu. Bạn có thể chuyển đổi chuông hoặc thông báo đẩy cho các chương trình phát sóng trong tùy chọn tài khoản của mình.", + "broadcastUploadImage": "Tải hình ảnh giải đấu lên", + "broadcastNoBoardsYet": "Chưa có bàn nào. Chúng sẽ xuất hiện khi ván đấu được tải lên.", + "broadcastBoardsCanBeLoaded": "Bàn đấu có thể được tải bằng nguồn hoặc thông qua {param}", + "broadcastStartsAfter": "Bắt đầu sau {param}", + "broadcastStartVerySoon": "Chương trình phát sóng sẽ sớm bắt đầu.", + "broadcastNotYetStarted": "Chương trình phát sóng vẫn chưa bắt đầu.", + "broadcastOfficialWebsite": "Website chính thức", + "broadcastStandings": "Bảng xếp hạng", + "broadcastOfficialStandings": "Bảng xếp hạng Chính thức", + "broadcastIframeHelp": "Thêm tùy chọn trên {param}", + "broadcastWebmastersPage": "trang nhà phát triển web", + "broadcastPgnSourceHelp": "Nguồn PGN công khai, thời gian thực cho vòng này. Chúng tôi cũng cung cấp {param} để đồng bộ hóa nhanh hơn và hiệu quả hơn.", + "broadcastEmbedThisBroadcast": "Nhúng chương trình phát sóng này vào trang web của bạn", + "broadcastEmbedThisRound": "Nhúng {param} vào trang web của bạn", + "broadcastRatingDiff": "Độ thay đổi hệ số", + "broadcastGamesThisTournament": "Các ván đấu trong giải này", + "broadcastScore": "Điểm số", + "broadcastAllTeams": "Tất cả đội", + "broadcastTournamentFormat": "Điều lệ giải đấu", + "broadcastTournamentLocation": "Địa điểm tổ chức giải đấu", + "broadcastTopPlayers": "Những kỳ thủ hàng đầu", + "broadcastTimezone": "Múi giờ", + "broadcastFideRatingCategory": "Thể loại xếp hạng FIDE", + "broadcastOptionalDetails": "Tùy chọn chi tiết", + "broadcastPastBroadcasts": "Các phát sóng đã qua", + "broadcastAllBroadcastsByMonth": "Xem tất cả phát sóng theo tháng", + "broadcastNbBroadcasts": "{count, plural, other{{count} phát sóng}}", "challengeChallengesX": "Số thách đấu: {param1}", "challengeChallengeToPlay": "Thách đấu một ván cờ", "challengeChallengeDeclined": "Lời thách đấu bị từ chối.", @@ -185,6 +265,7 @@ "preferencesNotifyWeb": "Trình duyệt", "preferencesNotifyDevice": "Thiết bị", "preferencesBellNotificationSound": "Âm thanh chuông báo", + "preferencesBlindfold": "Bịt mắt", "puzzlePuzzles": "Câu đố", "puzzlePuzzleThemes": "Chủ đề câu đố", "puzzleRecommended": "Được đề xuất", @@ -325,7 +406,7 @@ "puzzleThemeMaster": "Ván đấu cao cấp", "puzzleThemeMasterDescription": "Câu đố từ các ván đấu của người có danh hiệu.", "puzzleThemeMasterVsMaster": "Ván đấu giữa 2 kiện tướng", - "puzzleThemeMasterVsMasterDescription": "Câu đố từ các ván đấu giữa hai người chơi có danh hiệu.", + "puzzleThemeMasterVsMasterDescription": "Câu đố từ các ván đấu giữa hai kiện tướng.", "puzzleThemeMate": "Chiếu hết", "puzzleThemeMateDescription": "Chiến thắng ván cờ với phong cách.", "puzzleThemeMateIn1": "Chiếu hết trong 1 nước", @@ -380,8 +461,8 @@ "puzzleThemeXRayAttackDescription": "Một quân cờ tấn công hoặc phòng thủ một ô sau một quân cờ khác của đối phương.", "puzzleThemeZugzwang": "Cưỡng ép", "puzzleThemeZugzwangDescription": "Đối phương bị giới hạn các nước mà họ có thể đi và tất cả các nước đi ấy đều hại họ.", - "puzzleThemeHealthyMix": "Phối hợp nhịp nhàng", - "puzzleThemeHealthyMixDescription": "Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!", + "puzzleThemeMix": "Phối hợp nhịp nhàng", + "puzzleThemeMixDescription": "Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!", "puzzleThemePlayerGames": "Các ván đấu của người chơi", "puzzleThemePlayerGamesDescription": "Những câu đố từ những ván cờ của bạn hoặc từ các ván cờ của những người chơi khác.", "puzzleThemePuzzleDownloadInformation": "Những câu đố này thuộc phạm vi công khai và có thể tải về từ {param}.", @@ -397,7 +478,7 @@ "playWithAFriend": "Chơi với bạn bè", "playWithTheMachine": "Chơi với máy tính", "toInviteSomeoneToPlayGiveThisUrl": "Để mời ai đó chơi, hãy gửi URL này", - "gameOver": "Kết thúc ván cờ", + "gameOver": "Ván cờ kết thúc", "waitingForOpponent": "Đang chờ đối thủ", "orLetYourOpponentScanQrCode": "Hoặc để đối thủ của bạn quét mã QR này", "waiting": "Đang chờ", @@ -437,15 +518,15 @@ "talkInChat": "Hãy cư xử thân thiện trong cuộc trò chuyện!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "Người đầu tiên sử dụng URL này sẽ bắt đầu chơi với bạn.", "whiteResigned": "Bên trắng chịu thua", - "blackResigned": "Đen chịu thua", + "blackResigned": "Bên đen chịu thua", "whiteLeftTheGame": "Bên trắng đã rời khỏi ván cờ", "blackLeftTheGame": "Bên đen đã rời khỏi ván cờ", "whiteDidntMove": "Bên trắng không đi quân", "blackDidntMove": "Bên đen không đi quân", "requestAComputerAnalysis": "Yêu cầu máy tính phân tích", - "computerAnalysis": "Máy tính phân tích", - "computerAnalysisAvailable": "Có sẵn máy tính phân tích", - "computerAnalysisDisabled": "Phân tích máy tính bị vô hiệu hóa", + "computerAnalysis": "Phân tích từ máy tính", + "computerAnalysisAvailable": "Có sẵn phân tích từ máy tính", + "computerAnalysisDisabled": "Phân tích từ máy tính bị vô hiệu hóa", "analysis": "Bàn cờ phân tích", "depthX": "Độ sâu {param}", "usingServerAnalysis": "Sử dụng phân tích nhờ máy chủ", @@ -480,7 +561,7 @@ "averageRatingX": "Hệ số bình quân: {param}", "recentGames": "Các ván cờ gần đây", "topGames": "Các ván đấu hàng đầu", - "masterDbExplanation": "Các ván đấu OTB của các kỳ thủ có hệ số Elo FIDE {param1}+ từ năm {param2} đến {param3}", + "masterDbExplanation": "Các ván đấu OTB của các kỳ thủ có hệ số Rating FIDE {param1}+ từ năm {param2} đến {param3}", "dtzWithRounding": "DTZ50\" được làm tròn, dựa vào số nước đi quân cho tới nước ăn quân hoặc đi tiến tốt tiếp theo", "noGameFound": "Không tìm thấy ván nào", "maxDepthReached": "Đã đạt độ sâu tối đa!", @@ -502,7 +583,6 @@ "replayMode": "Chế độ xem lại", "realtimeReplay": "Thời gian thực", "byCPL": "Theo phần trăm mất tốt (CPL)", - "openStudy": "Mở nghiên cứu", "enable": "Bật", "bestMoveArrow": "Mũi tên chỉ nước đi tốt nhất", "showVariationArrows": "Hiển thị mũi tên biến", @@ -512,7 +592,6 @@ "memory": "Bộ nhớ", "infiniteAnalysis": "Phân tích vô hạn", "removesTheDepthLimit": "Bỏ giới hạn độ sâu và giữ máy tính của bạn mượt hơn", - "engineManager": "Quản lý động cơ", "blunder": "Sai lầm nghiêm trọng", "mistake": "Sai lầm", "inaccuracy": "Không chính xác", @@ -556,7 +635,7 @@ "username": "Tên đăng nhập", "usernameOrEmail": "Tên đăng nhập hoặc email", "changeUsername": "Thay đổi tên đăng nhập", - "changeUsernameNotSame": "Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"johndoe\" thành \"JohnDoe\".", + "changeUsernameNotSame": "Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"dotrongkhanh04032012\" thành \"DoTrongKhanh04032012\".", "changeUsernameDescription": "Thay đổi tên người dùng của bạn. Điều này chỉ có thể thực hiện một lần và bạn chỉ được thay đổi cách viết hoa/viết thường các chữ trong tên người dùng của bạn.", "signupUsernameHint": "Hãy đảm bảo chọn tên người dùng thân thiện với mọi người. Bạn sẽ không thể thay đổi nó và bất kỳ tài khoản nào có tên người dùng không phù hợp sẽ bị đóng!", "signupEmailHint": "Chúng tôi chỉ sử dụng nó cho việc khôi phục mật khẩu.", @@ -594,6 +673,7 @@ "rank": "Hạng", "rankX": "Hạng: {param}", "gamesPlayed": "Số ván đã chơi", + "ok": "OK", "cancel": "Hủy", "whiteTimeOut": "Bên trắng hết giờ", "blackTimeOut": "Bên đen hết giờ", @@ -693,7 +773,7 @@ "importGameCaveat": "Các biến sẽ bị xóa. Để giữ chúng, hãy nhập PGN thông qua một nghiên cứu.", "importGameDataPrivacyWarning": "Ai cũng có thể truy cập PGN này. Để nhập ván cờ một cách riêng tư, hãy sử dụng nghiên cứu.", "thisIsAChessCaptcha": "Đây là mã CAPTCHA cờ vua.", - "clickOnTheBoardToMakeYourMove": "Nhấn vào bàn cờ để di chuyển và chứng minh bạn là con người.", + "clickOnTheBoardToMakeYourMove": "Nhấn vào bàn cờ để di chuyển, và chứng minh bạn là con người.", "captcha_fail": "Hãy giải mã captcha cờ vua.", "notACheckmate": "Không phải là một nước chiếu hết", "whiteCheckmatesInOneMove": "Bên trắng hãy chiếu hết trong một nước đi", @@ -710,7 +790,6 @@ "block": "Chặn", "blocked": "Đã chặn", "unblock": "Bỏ chặn", - "followsYou": "Theo dõi bạn", "xStartedFollowingY": "{param1} đã bắt đầu theo dõi {param2}", "more": "Xem thêm", "memberSince": "Thành viên từ", @@ -816,7 +895,9 @@ "cheat": "Gian lận", "troll": "Chọc tức, chơi khăm", "other": "Khác", - "reportDescriptionHelp": "Dán đường dẫn đến (các) ván cờ và giải thích về vấn đề của kỳ thủ này. Đừng chỉ nói \"họ gian lận\" mà hãy miêu tả chi tiết nhất có thể. Vấn đề sẽ được giải quyết nhanh hơn nếu bạn viết bằng tiếng Anh.", + "reportCheatBoostHelp": "Dán đường dẫn đến (các) ván cờ và giải thích hành vi sai của kỳ thủ này. Đừng chỉ nói \"họ gian lận\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào.", + "reportUsernameHelp": "Giải thích những gì về tên người dùng này là xúc phạm. Đừng chỉ nói \"nó gây khó chịu/không phù hợp\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào, đặc biệt nếu sự xúc phạm bị che giấu, không phải bằng tiếng Anh, là tiếng lóng, hoặc là một tài liệu tham khảo lịch sử/văn hóa.", + "reportProcessedFasterInEnglish": "Báo cáo của bạn sẽ được xử lý nhanh hơn nếu được viết bằng tiếng Anh.", "error_provideOneCheatedGameLink": "Hãy cung cấp ít nhất một đường dẫn đến ván cờ bị gian lận.", "by": "bởi {param}", "importedByX": "Được nhập bởi {param}", @@ -869,7 +950,7 @@ "side": "Bên", "clock": "Đồng hồ", "opponent": "Đối thủ", - "learnMenu": "Học", + "learnMenu": "Học tập", "studyMenu": "Nghiên cứu", "practice": "Luyện tập", "community": "Cộng đồng", @@ -957,7 +1038,7 @@ "playSelectedMove": "chơi nước đi đã chọn", "newTournament": "Giải đấu mới", "tournamentHomeTitle": "Giải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú", - "tournamentHomeDescription": "Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.", + "tournamentHomeDescription": "Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ đạn, Cờ chớp, Cờ nhanh, Cờ chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.", "tournamentNotFound": "Không tìm thấy giải đấu", "tournamentDoesNotExist": "Giải đấu này không tồn tại.", "tournamentMayHaveBeenCanceled": "Giải đấu có thể đã bị huỷ, nếu tất cả người chơi rời giải trước khi giải đấu bắt đầu.", @@ -1214,6 +1295,7 @@ "showMeEverything": "Cho tôi xem mọi thứ nào", "lichessPatronInfo": "Lichess là một tổ chức phi lợi nhuận và là phần mềm hoàn toàn miễn phí/mã nguồn mở.\nMọi chi phí vận hành, phát triển, và nội dung được tài trợ bởi những đóng góp của người dùng.", "nothingToSeeHere": "Không có gì để xem ở đây vào lúc này.", + "stats": "Thống kê", "opponentLeftCounter": "{count, plural, other{Đối thủ đã rời khỏi ván cờ. Bạn có thể tuyên bố thắng cuộc trong {count} giây.}}", "mateInXHalfMoves": "{count, plural, other{Chiếu hết trong {count} nước}}", "nbBlunders": "{count, plural, other{{count} sai lầm nghiêm trọng}}", @@ -1310,6 +1392,178 @@ "stormXRuns": "{count, plural, other{{count} lần chơi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{Đã chơi {count} lượt {param2}}}", "streamerLichessStreamers": "Các Streamer của Lichess", + "studyPrivate": "Riêng tư", + "studyMyStudies": "Các nghiên cứu của tôi", + "studyStudiesIContributeTo": "Các nghiên cứu tôi đóng góp", + "studyMyPublicStudies": "Nghiên cứu công khai của tôi", + "studyMyPrivateStudies": "Nghiên cứu riêng tư của tôi", + "studyMyFavoriteStudies": "Các nghiên cứu yêu thích của tôi", + "studyWhatAreStudies": "Nghiên cứu là gì?", + "studyAllStudies": "Tất cả các nghiên cứu", + "studyStudiesCreatedByX": "Các nghiên cứu được tạo bởi {param}", + "studyNoneYet": "Chưa có gì cả.", + "studyHot": "Thịnh hành", + "studyDateAddedNewest": "Ngày được thêm (mới nhất)", + "studyDateAddedOldest": "Ngày được thêm (cũ nhất)", + "studyRecentlyUpdated": "Được cập nhật gần đây", + "studyMostPopular": "Phổ biến nhất", + "studyAlphabetical": "Theo thứ tự chữ cái", + "studyAddNewChapter": "Thêm một chương mới", + "studyAddMembers": "Thêm thành viên", + "studyInviteToTheStudy": "Mời vào nghiên cứu", + "studyPleaseOnlyInvitePeopleYouKnow": "Vui lòng chỉ mời những người bạn biết và những người tích cực muốn tham gia nghiên cứu này.", + "studySearchByUsername": "Tìm kiếm theo tên người dùng", + "studySpectator": "Khán giả", + "studyContributor": "Người đóng góp", + "studyKick": "Đuổi", + "studyLeaveTheStudy": "Rời khỏi nghiên cứu", + "studyYouAreNowAContributor": "Bây giờ bạn là một người đóng góp", + "studyYouAreNowASpectator": "Bây giờ bạn là một khán giả", + "studyPgnTags": "Nhãn PGN", + "studyLike": "Thích", + "studyUnlike": "Bỏ thích", + "studyNewTag": "Nhãn mới", + "studyCommentThisPosition": "Bình luận về thế cờ này", + "studyCommentThisMove": "Bình luận về nước cờ này", + "studyAnnotateWithGlyphs": "Chú thích bằng dấu", + "studyTheChapterIsTooShortToBeAnalysed": "Chương này quá ngắn để có thể được phân tích.", + "studyOnlyContributorsCanRequestAnalysis": "Chỉ những người đóng góp nghiên cứu mới có thể yêu cầu máy tính phân tích.", + "studyGetAFullComputerAnalysis": "Nhận phân tích máy tính phía máy chủ đầy đủ về biến chính.", + "studyMakeSureTheChapterIsComplete": "Hãy chắc chắn chương đã hoàn thành. Bạn chỉ có thể yêu cầu phân tích 1 lần.", + "studyAllSyncMembersRemainOnTheSamePosition": "Đồng bộ hóa tất cả các thành viên trên cùng một thế cờ", + "studyShareChanges": "Chia sẻ các thay đổi với khán giả và lưu chúng trên máy chủ", + "studyPlaying": "Đang chơi", + "studyShowEvalBar": "Thanh lợi thế", + "studyFirst": "Trang đầu", + "studyPrevious": "Trang trước", + "studyNext": "Trang tiếp theo", + "studyLast": "Trang cuối", "studyShareAndExport": "Chia sẻ & xuất", - "studyStart": "Bắt đầu" + "studyCloneStudy": "Nhân bản", + "studyStudyPgn": "PGN nghiên cứu", + "studyDownloadAllGames": "Tải về tất cả ván đấu", + "studyChapterPgn": "PGN chương", + "studyCopyChapterPgn": "Sao chép PGN", + "studyDownloadGame": "Tải về ván cờ", + "studyStudyUrl": "URL nghiên cứu", + "studyCurrentChapterUrl": "URL chương hiện tại", + "studyYouCanPasteThisInTheForumToEmbed": "Bạn có thể dán cái này để nhúng vào diễn đàn hoặc blog Lichess cá nhân của bạn", + "studyStartAtInitialPosition": "Bắt đầu từ thế cờ ban đầu", + "studyStartAtX": "Bắt đầu tại nước {param}", + "studyEmbedInYourWebsite": "Nhúng vào trang web của bạn", + "studyReadMoreAboutEmbedding": "Đọc thêm về việc nhúng", + "studyOnlyPublicStudiesCanBeEmbedded": "Chỉ các nghiên cứu công khai mới được nhúng!", + "studyOpen": "Mở", + "studyXBroughtToYouByY": "{param1} được lấy từ {param2}", + "studyStudyNotFound": "Không tìm thấy nghiên cứu nào", + "studyEditChapter": "Sửa chương", + "studyNewChapter": "Chương mới", + "studyImportFromChapterX": "Nhập từ chương {param}", + "studyOrientation": "Nghiên cứu cho bên", + "studyAnalysisMode": "Chế độ phân tích", + "studyPinnedChapterComment": "Đã ghim bình luận chương", + "studySaveChapter": "Lưu chương", + "studyClearAnnotations": "Xóa chú thích", + "studyClearVariations": "Xóa các biến", + "studyDeleteChapter": "Xóa chương", + "studyDeleteThisChapter": "Xóa chương này. Sẽ không có cách nào để có thể khôi phục lại!", + "studyClearAllCommentsInThisChapter": "Xóa tất cả bình luận, dấu chú thích và hình vẽ trong chương này", + "studyRightUnderTheBoard": "Ngay dưới bàn cờ", + "studyNoPinnedComment": "Không có", + "studyNormalAnalysis": "Phân tích thường", + "studyHideNextMoves": "Ẩn các nước tiếp theo", + "studyInteractiveLesson": "Bài học tương tác", + "studyChapterX": "Chương {param}", + "studyEmpty": "Trống", + "studyStartFromInitialPosition": "Bắt đầu từ thế cờ ban đầu", + "studyEditor": "Chỉnh sửa bàn cờ", + "studyStartFromCustomPosition": "Bắt đầu từ thế cờ tùy chỉnh", + "studyLoadAGameByUrl": "Tải ván cờ bằng URL", + "studyLoadAPositionFromFen": "Tải thế cờ từ chuỗi FEN", + "studyLoadAGameFromPgn": "Tải ván cờ từ PGN", + "studyAutomatic": "Tự động", + "studyUrlOfTheGame": "URL của các ván, một URL mỗi dòng", + "studyLoadAGameFromXOrY": "Tải ván cờ từ {param1} hoặc {param2}", + "studyCreateChapter": "Tạo chương", + "studyCreateStudy": "Tạo nghiên cứu", + "studyEditStudy": "Chỉnh sửa nghiên cứu", + "studyVisibility": "Khả năng hiển thị", + "studyPublic": "Công khai", + "studyUnlisted": "Không công khai", + "studyInviteOnly": "Chỉ những người được mời", + "studyAllowCloning": "Cho phép tạo bản sao", + "studyNobody": "Không ai cả", + "studyOnlyMe": "Chỉ mình tôi", + "studyContributors": "Những người đóng góp", + "studyMembers": "Thành viên", + "studyEveryone": "Mọi người", + "studyEnableSync": "Kích hoạt tính năng đồng bộ hóa", + "studyYesKeepEveryoneOnTheSamePosition": "Có: giữ tất cả mọi người trên 1 thế cờ", + "studyNoLetPeopleBrowseFreely": "Không: để mọi người tự do xem xét", + "studyPinnedStudyComment": "Bình luận nghiên cứu được ghim", + "studyStart": "Bắt đầu", + "studySave": "Lưu", + "studyClearChat": "Xóa trò chuyện", + "studyDeleteTheStudyChatHistory": "Xóa lịch sử trò chuyện nghiên cứu? Không thể khôi phục lại!", + "studyDeleteStudy": "Xóa nghiên cứu", + "studyConfirmDeleteStudy": "Xóa toàn bộ nghiên cứu? Không có cách nào để khôi phục lại! Nhập tên của nghiên cứu để xác nhận: {param}", + "studyWhereDoYouWantToStudyThat": "Bạn muốn nghiên cứu ở đâu?", + "studyGoodMove": "Nước tốt", + "studyMistake": "Sai lầm", + "studyBrilliantMove": "Nước đi thiên tài", + "studyBlunder": "Sai lầm nghiêm trọng", + "studyInterestingMove": "Nước đi hay", + "studyDubiousMove": "Nước đi mơ hồ", + "studyOnlyMove": "Nước duy nhất", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Thế trận cân bằng", + "studyUnclearPosition": "Thế cờ không rõ ràng", + "studyWhiteIsSlightlyBetter": "Bên trắng có một chút lợi thế", + "studyBlackIsSlightlyBetter": "Bên đen có một chút lợi thế", + "studyWhiteIsBetter": "Bên trắng lợi thế hơn", + "studyBlackIsBetter": "Bên đen lợi thế hơn", + "studyWhiteIsWinning": "Bên trắng đang thắng dần", + "studyBlackIsWinning": "Bên đen đang thắng dần", + "studyNovelty": "Mới lạ", + "studyDevelopment": "Phát triển", + "studyInitiative": "Chủ động", + "studyAttack": "Tấn công", + "studyCounterplay": "Phản công", + "studyTimeTrouble": "Sắp hết thời gian", + "studyWithCompensation": "Có bù đắp", + "studyWithTheIdea": "Với ý tưởng", + "studyNextChapter": "Chương tiếp theo", + "studyPrevChapter": "Chương trước", + "studyStudyActions": "Các thao tác trong nghiên cứu", + "studyTopics": "Chủ đề", + "studyMyTopics": "Chủ đề của tôi", + "studyPopularTopics": "Chủ đề phổ biến", + "studyManageTopics": "Quản lý chủ đề", + "studyBack": "Quay Lại", + "studyPlayAgain": "Chơi lại", + "studyWhatWouldYouPlay": "Bạn sẽ làm gì ở thế cờ này?", + "studyYouCompletedThisLesson": "Chúc mừng! Bạn đã hoàn thành bài học này.", + "studyPerPage": "{param} mỗi trang", + "studyNbChapters": "{count, plural, other{{count} Chương}}", + "studyNbGames": "{count, plural, other{{count} Ván cờ}}", + "studyNbMembers": "{count, plural, other{{count} Thành viên}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Dán PGN ở đây, tối đa {count} ván}}", + "timeagoJustNow": "vừa mới đây", + "timeagoRightNow": "ngay bây giờ", + "timeagoCompleted": "đã hoàn thành", + "timeagoInNbSeconds": "{count, plural, other{trong {count} giây}}", + "timeagoInNbMinutes": "{count, plural, other{trong {count} phút}}", + "timeagoInNbHours": "{count, plural, other{trong {count} giờ}}", + "timeagoInNbDays": "{count, plural, other{trong {count} ngày}}", + "timeagoInNbWeeks": "{count, plural, other{trong {count} tuần}}", + "timeagoInNbMonths": "{count, plural, other{trong {count} tháng}}", + "timeagoInNbYears": "{count, plural, other{trong {count} năm}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} phút trước}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} giờ trước}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} ngày trước}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} tuần trước}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} tháng trước}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} năm trước}}", + "timeagoNbMinutesRemaining": "{count, plural, other{còn {count} phút}}", + "timeagoNbHoursRemaining": "{count, plural, other{còn {count} giờ}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh.arb b/lib/l10n/lila_zh.arb index 12b2cf1e55..56d9a47d2e 100644 --- a/lib/l10n/lila_zh.arb +++ b/lib/l10n/lila_zh.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "所有对局", + "mobileAreYouSure": "你确定吗?", + "mobileBlindfoldMode": "盲棋", + "mobileCancelTakebackOffer": "取消悔棋请求", + "mobileClearButton": "清空", + "mobileCorrespondenceClearSavedMove": "清除已保存的着法", + "mobileCustomGameJoinAGame": "加入一局游戏", + "mobileFeedbackButton": "问题反馈", + "mobileGreeting": "你好,{param}", + "mobileGreetingWithoutName": "你好!", + "mobileHideVariation": "隐藏变着", "mobileHomeTab": "主页", - "mobilePuzzlesTab": "谜题", - "mobileToolsTab": "工具", - "mobileWatchTab": "观看", - "mobileSettingsTab": "设置", + "mobileLiveStreamers": "主播", "mobileMustBeLoggedIn": "您必须登录才能浏览此页面。", - "mobileSystemColors": "系统颜色", - "mobileFeedbackButton": "问题反馈", + "mobileNoSearchResults": "无结果", + "mobileNotFollowingAnyUser": "你没有关注任何用户。", "mobileOkButton": "好", + "mobilePlayersMatchingSearchTerm": "包含\"{param}\"名称的棋手", + "mobilePrefMagnifyDraggedPiece": "放大正在拖动的棋子", + "mobilePuzzleStormConfirmEndRun": "你想结束这组吗?", + "mobilePuzzleStormFilterNothingToShow": "没有结果,请更改筛选条件", + "mobilePuzzleStormNothingToShow": "没有记录。 请下几组 Puzzle Storm。", + "mobilePuzzleStormSubtitle": "在3分钟内解决尽可能多的谜题。", + "mobilePuzzleStreakAbortWarning": "你将失去你目前的连胜,你的分数将被保存。", + "mobilePuzzleThemesSubtitle": "从你最喜欢的开局解决谜题,或选择一个主题。", + "mobilePuzzlesTab": "谜题", + "mobileRecentSearches": "最近搜索", "mobileSettingsHapticFeedback": "震动反馈", "mobileSettingsImmersiveMode": "沉浸模式", - "mobileSettingsImmersiveModeSubtitle": "播放时隐藏系统UI。 如果您对屏幕边缘的系统导航手势感到困扰,请使用此功能。 适用于游戏和益智风暴屏幕。", - "mobileNotFollowingAnyUser": "你没有关注任何用户。", - "mobileAllGames": "所有对局", - "mobileRecentSearches": "最近搜索", - "mobileClearButton": "清空", - "mobilePlayersMatchingSearchTerm": "拥有\"{param}\"的玩家", - "mobileNoSearchResults": "无结果", - "mobileAreYouSure": "你确定吗?", - "mobilePuzzleStreakAbortWarning": "你将失去你目前的连胜,你的分数将被保存。", - "mobilePuzzleStormNothingToShow": "没什么好表现的。 玩拼图风暴的一些运行。", - "mobileSharePuzzle": "分享这个谜题", - "mobileShareGameURL": "分享棋局链接", + "mobileSettingsImmersiveModeSubtitle": "下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。", + "mobileSettingsTab": "设置", "mobileShareGamePGN": "分享 PGN", + "mobileShareGameURL": "分享棋局链接", "mobileSharePositionAsFEN": "保存局面为 FEN", - "mobileShowVariations": "显示变化", - "mobileHideVariation": "隐藏变异", + "mobileSharePuzzle": "分享这个谜题", "mobileShowComments": "显示评论", - "mobilePuzzleStormConfirmEndRun": "你想结束这次跑步吗?", - "mobilePuzzleStormFilterNothingToShow": "没有显示,请更改过滤器", - "mobileCancelTakebackOffer": "取消悔棋请求", - "mobileCancelDrawOffer": "取消和棋请求", - "mobileWaitingForOpponentToJoin": "正在等待对手加入...", - "mobileBlindfoldMode": "盲棋", - "mobileLiveStreamers": "主播", - "mobileCustomGameJoinAGame": "加入一局游戏", - "mobileCorrespondenceClearSavedMove": "清除已保存的移动", - "mobileSomethingWentWrong": "发生一些错误。", "mobileShowResult": "显示结果", - "mobilePuzzleThemesSubtitle": "从你最喜欢的开口玩拼图,或选择一个主题。", - "mobilePuzzleStormSubtitle": "在3分钟内尽可能多地解决谜题", - "mobileGreeting": "你好,{param}", - "mobileGreetingWithoutName": "你好!", + "mobileShowVariations": "显示变着", + "mobileSomethingWentWrong": "出了一些问题。", + "mobileSystemColors": "系统颜色", + "mobileTheme": "主题", + "mobileToolsTab": "工具", + "mobileWaitingForOpponentToJoin": "正在等待对手加入...", + "mobileWatchTab": "观看", "activityActivity": "动态", "activityHostedALiveStream": "主持了直播", "activityRankedInSwissTournament": "在 {param2} 中获得第 #{param1} 名", @@ -64,7 +65,52 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{参加了 {count} 场 swiss 锦标赛}}", "activityJoinedNbTeams": "{count, plural, other{加入了 {count} 个团队}}", "broadcastBroadcasts": "转播", + "broadcastMyBroadcasts": "我的直播", "broadcastLiveBroadcasts": "赛事转播", + "broadcastBroadcastCalendar": "转播日程表", + "broadcastNewBroadcast": "新建实况转播", + "broadcastSubscribedBroadcasts": "已订阅的转播", + "broadcastAboutBroadcasts": "关于转播", + "broadcastHowToUseLichessBroadcasts": "如何使用Lichess转播", + "broadcastTheNewRoundHelp": "新一轮的成员和贡献者将与前一轮相同。", + "broadcastAddRound": "添加一轮", + "broadcastOngoing": "进行中", + "broadcastUpcoming": "即将举行", + "broadcastCompleted": "已完成", + "broadcastCompletedHelp": "Lichess基于源游戏检测游戏的完成状态。如果没有源,请使用此选项。", + "broadcastRoundName": "轮次名称", + "broadcastRoundNumber": "轮数", + "broadcastTournamentName": "锦标赛名称", + "broadcastTournamentDescription": "锦标赛简短描述", + "broadcastFullDescription": "赛事详情", + "broadcastFullDescriptionHelp": "转播内容的详细描述 (可选)。可以使用 {param1},字数少于 {param2} 个。", + "broadcastSourceSingleUrl": "PGN的URL源", + "broadcastSourceUrlHelp": "Lichess 将从该网址搜查 PGN 的更新。它必须是公开的。", + "broadcastSourceGameIds": "多达64个 Lichess 棋局Id,用空格隔开。", + "broadcastStartDateHelp": "如果你知道比赛开始时间 (可选)", + "broadcastCurrentGameUrl": "当前棋局链接", + "broadcastDownloadAllRounds": "下载所有棋局", + "broadcastResetRound": "重置此轮", + "broadcastDeleteRound": "删除此轮", + "broadcastDefinitivelyDeleteRound": "确定删除该回合及其游戏。", + "broadcastDeleteAllGamesOfThisRound": "删除此回合的所有游戏。源需要激活才能重新创建。", + "broadcastEditRoundStudy": "编辑该轮次的棋局研究", + "broadcastDeleteTournament": "删除该锦标赛", + "broadcastDefinitivelyDeleteTournament": "确定删除整个锦标赛、所有轮次和其中所有比赛。", + "broadcastShowScores": "根据比赛结果显示棋手分数", + "broadcastReplacePlayerTags": "可选项:替换选手的名字、等级分和头衔", + "broadcastFideFederations": "FIDE 成员国", + "broadcastTop10Rating": "前10名等级分", + "broadcastFidePlayers": "FIDE 棋手", + "broadcastFidePlayerNotFound": "未找到 FIDE 棋手", + "broadcastFideProfile": "FIDE个人资料", + "broadcastFederation": "棋联", + "broadcastAgeThisYear": "今年的年龄", + "broadcastUnrated": "未评级", + "broadcastRecentTournaments": "最近的比赛", + "broadcastPastBroadcasts": "结束的转播", + "broadcastAllBroadcastsByMonth": "按月查看所有转播", + "broadcastNbBroadcasts": "{count, plural, other{{count} 直播}}", "challengeChallengesX": "挑战: {param1}", "challengeChallengeToPlay": "发起挑战", "challengeChallengeDeclined": "拒绝挑战", @@ -91,7 +137,7 @@ "contactContact": "联系", "contactContactLichess": "联系 Lichess", "patronDonate": "捐赠", - "patronLichessPatron": "赞助 Lichess", + "patronLichessPatron": "Lichess赞助者账号", "perfStatPerfStats": "{param} 战绩", "perfStatViewTheGames": "查看棋局", "perfStatProvisional": "暂定", @@ -122,7 +168,7 @@ "perfStatMaxTimePlaying": "最长连续对局时间", "perfStatNow": "现在", "preferencesPreferences": "偏好设置", - "preferencesDisplay": "显示", + "preferencesDisplay": "界面设置", "preferencesPrivacy": "隐私设置", "preferencesNotifications": "通知", "preferencesPieceAnimation": "棋子动画", @@ -188,6 +234,7 @@ "preferencesNotifyWeb": "浏览器通知", "preferencesNotifyDevice": "设备通知", "preferencesBellNotificationSound": "通知铃声", + "preferencesBlindfold": "盲棋", "puzzlePuzzles": "谜题", "puzzlePuzzleThemes": "训练主题", "puzzleRecommended": "我们推荐:", @@ -383,8 +430,8 @@ "puzzleThemeXRayAttackDescription": "一个棋子穿过对方的棋子攻击或防守一个格子。", "puzzleThemeZugzwang": "楚茨文克(无等着)", "puzzleThemeZugzwangDescription": "对手可选的着法是有限的,并且所有着法都会使其局面更加恶化。", - "puzzleThemeHealthyMix": "健康搭配", - "puzzleThemeHealthyMixDescription": "每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。", + "puzzleThemeMix": "健康搭配", + "puzzleThemeMixDescription": "每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。", "puzzleThemePlayerGames": "玩家对局", "puzzleThemePlayerGamesDescription": "查找从你或其他玩家的对局中产生的谜题。", "puzzleThemePuzzleDownloadInformation": "这些谜题都是公开的,可以在 {param} 下载。", @@ -407,7 +454,7 @@ "yourTurn": "你的回合", "aiNameLevelAiLevel": "{param1}级別{param2}", "level": "级别", - "strength": "电脑的难度", + "strength": "强度", "toggleTheChat": "聊天开关", "chat": "聊天", "resign": "认输", @@ -419,25 +466,25 @@ "asBlack": "持黑", "randomColor": "随机选色", "createAGame": "创建对局", - "whiteIsVictorious": "白方胜", - "blackIsVictorious": "黑方胜", + "whiteIsVictorious": "白方胜利", + "blackIsVictorious": "黑方胜利", "youPlayTheWhitePieces": "你执白棋", "youPlayTheBlackPieces": "你执黑棋", - "itsYourTurn": "轮到你了!", + "itsYourTurn": "你的回合!", "cheatDetected": "检测到作弊", "kingInTheCenter": "王占中", - "threeChecks": "三次将军", - "raceFinished": "比赛结束", + "threeChecks": "三次将军胜", + "raceFinished": "竞王结束", "variantEnding": "变种结束", "newOpponent": "新对手", "yourOpponentWantsToPlayANewGameWithYou": "你的对手想和你再玩一局", "joinTheGame": "加入对局", "whitePlays": "白方走棋", "blackPlays": "黑方走棋", - "opponentLeftChoices": "您的对手可能已离开棋局。您可以宣布胜利,和棋,或继续等待。", + "opponentLeftChoices": "你的对手已离开棋局。你可以宣布胜利、和棋或继续等待。", "forceResignation": "宣布胜利", - "forceDraw": "和棋", - "talkInChat": "聊天请注意文明用语。", + "forceDraw": "宣布和棋", + "talkInChat": "聊天请注意文明用语!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "第一个访问此网址的人将与你下棋。", "whiteResigned": "白方认输", "blackResigned": "黑方认输", @@ -458,7 +505,7 @@ "cloudAnalysis": "云分析", "goDeeper": "深入分析", "showThreat": "显示威胁", - "inLocalBrowser": "在本地浏览器", + "inLocalBrowser": "本地浏览器", "toggleLocalEvaluation": "切换到本地分析", "promoteVariation": "提升变着", "makeMainLine": "做为主线", @@ -468,25 +515,25 @@ "forceVariation": "强制作为变着", "copyVariationPgn": "复制变着的PGN", "move": "着法", - "variantLoss": "变体输了", + "variantLoss": "变体输棋", "variantWin": "变体胜利", "insufficientMaterial": "子力不足", "pawnMove": "走兵", "capture": "吃子", "close": "关闭", - "winning": "赢棋", + "winning": "胜棋", "losing": "输棋", "drawn": "和棋", - "unknown": "结局未知", + "unknown": "未知", "database": "数据库", "whiteDrawBlack": "白胜/和棋/黑胜", "averageRatingX": "平均等级分:{param}", - "recentGames": "最近对局", + "recentGames": "最近棋局", "topGames": "名局", - "masterDbExplanation": "{param2}-{param3}年国际棋联等级分{param1}以上棋手的两百万局棋谱", + "masterDbExplanation": "{param2}-{param3}年国际棋联等级分{param1}以上棋手的棋谱", "dtzWithRounding": "经过四舍五入的DTZ50'',是基于到下次吃子或兵动的半步数目。", - "noGameFound": "没找到符合要求的棋局", - "maxDepthReached": "已达最大深度!", + "noGameFound": "未找到棋局", + "maxDepthReached": "已达到最大深度!", "maybeIncludeMoreGamesFromThePreferencesMenu": "请尝试在“选择”菜单内包括更多棋局。", "openings": "开局", "openingExplorer": "开局浏览器", @@ -505,7 +552,6 @@ "replayMode": "回放模式", "realtimeReplay": "实时回放", "byCPL": "按厘兵损失", - "openStudy": "进入研讨室", "enable": "启用", "bestMoveArrow": "最佳着法指示", "showVariationArrows": "显示变着箭头", @@ -515,7 +561,6 @@ "memory": "内存", "infiniteAnalysis": "开启无限分析", "removesTheDepthLimit": "取消深度限制(会提升电脑温度)", - "engineManager": "引擎管理", "blunder": "漏着", "mistake": "错着", "inaccuracy": "失准", @@ -617,7 +662,7 @@ "gameAborted": "棋局已中止", "standard": "标准国际象棋", "customPosition": "自定义位置", - "unlimited": "无限时间", + "unlimited": "无限制", "mode": "模式", "casual": "休闲", "rated": "排位", @@ -713,7 +758,6 @@ "block": "加入黑名单", "blocked": "已加入黑名单", "unblock": "移出黑名单", - "followsYou": "关注了你", "xStartedFollowingY": "{param1}开始关注{param2}", "more": "更多", "memberSince": "注册日期", @@ -800,7 +844,7 @@ "no": "否", "yes": "是", "website": "网站", - "mobile": "流动电话", + "mobile": "移动端", "help": "帮助:", "createANewTopic": "新话题", "topics": "话题", @@ -819,7 +863,9 @@ "cheat": "作弊", "troll": "捣乱", "other": "其他", - "reportDescriptionHelp": "请附上棋局链接解释该用户的行为问题。例如如果你怀疑某用户作弊,请不要只说 “对手作弊”。请解释为什么你认为对手作弊。如果你用英语举报,我们将会更快作出答复。", + "reportCheatBoostHelp": "请附上棋局链接解释该用户的行为问题。请不要只说 “对手作弊”,而是解释为什么你认为对手作弊。", + "reportUsernameHelp": "解释这个用户名为何具有冒犯性。不要只说“它具有冒犯性/不恰当”,而是要告诉我们你是如何得出这个结论的,特别是如果侮辱性内容是隐晦的、非英语的、俚语或有历史/文化参考。", + "reportProcessedFasterInEnglish": "如果您使用英语举报,我们将会更快作出答复。", "error_provideOneCheatedGameLink": "请提供至少一局作弊的棋局的链接。", "by": "来自{param}", "importedByX": "由 {param} 导入", @@ -1313,6 +1359,177 @@ "stormXRuns": "{count, plural, other{{count}组}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{玩了{count}组的{param2}}}", "streamerLichessStreamers": "Lichess 主播", + "studyPrivate": "私人", + "studyMyStudies": "我的研讨", + "studyStudiesIContributeTo": "我贡献的研讨", + "studyMyPublicStudies": "我的公开研讨", + "studyMyPrivateStudies": "我的私有研讨", + "studyMyFavoriteStudies": "我收藏的研讨", + "studyWhatAreStudies": "什么是研讨?", + "studyAllStudies": "所有研讨", + "studyStudiesCreatedByX": "由 {param} 创建的研讨", + "studyNoneYet": "暂无。", + "studyHot": "热门", + "studyDateAddedNewest": "添加时间 (最新)", + "studyDateAddedOldest": "添加时间 (最早)", + "studyRecentlyUpdated": "最近更新", + "studyMostPopular": "最受欢迎", + "studyAlphabetical": "按字母顺序", + "studyAddNewChapter": "添加一个新章节", + "studyAddMembers": "添加成员", + "studyInviteToTheStudy": "邀请参加研讨", + "studyPleaseOnlyInvitePeopleYouKnow": "请仅邀请你认识的并且积极希望参与这个研讨的成员", + "studySearchByUsername": "按用户名搜索", + "studySpectator": "旁观者", + "studyContributor": "贡献者", + "studyKick": "踢出", + "studyLeaveTheStudy": "离开研讨", + "studyYouAreNowAContributor": "你现在是一位贡献者", + "studyYouAreNowASpectator": "你现在是一位旁观者", + "studyPgnTags": "PGN 标签", + "studyLike": "赞", + "studyUnlike": "取消赞", + "studyNewTag": "新建标签", + "studyCommentThisPosition": "评论当前局面", + "studyCommentThisMove": "评论这步走法", + "studyAnnotateWithGlyphs": "用符号标注", + "studyTheChapterIsTooShortToBeAnalysed": "本章节太短,无法进行分析。", + "studyOnlyContributorsCanRequestAnalysis": "只有贡献者可以请求服务器分析。", + "studyGetAFullComputerAnalysis": "请求服务器完整地分析主线走法。", + "studyMakeSureTheChapterIsComplete": "请确保章节已完成。你只能请求分析一次。", + "studyAllSyncMembersRemainOnTheSamePosition": "SYNC 中所有成员处于相同局面", + "studyShareChanges": "与旁观者共享更改并云端保存", + "studyPlaying": "正在对局", + "studyShowEvalBar": "评估条", + "studyFirst": "首页", + "studyPrevious": "上一页", + "studyNext": "下一页", + "studyLast": "末页", "studyShareAndExport": "分享并导出", - "studyStart": "开始" + "studyCloneStudy": "复制棋局", + "studyStudyPgn": "研究 PGN", + "studyDownloadAllGames": "下载所有棋局", + "studyChapterPgn": "章节PGN", + "studyCopyChapterPgn": "复制PGN", + "studyDownloadGame": "下载棋局", + "studyStudyUrl": "研究链接", + "studyCurrentChapterUrl": "当前章节链接", + "studyYouCanPasteThisInTheForumToEmbed": "你可以将此粘贴到论坛以嵌入章节", + "studyStartAtInitialPosition": "从初始局面开始", + "studyStartAtX": "从 {param} 开始", + "studyEmbedInYourWebsite": "嵌入到你的网站上", + "studyReadMoreAboutEmbedding": "阅读更多关于嵌入的信息", + "studyOnlyPublicStudiesCanBeEmbedded": "只能嵌入隐私设置为公开的研究!", + "studyOpen": "打开", + "studyXBroughtToYouByY": "{param1} 由 {param2} 提供", + "studyStudyNotFound": "找不到研究", + "studyEditChapter": "编辑章节", + "studyNewChapter": "新章节", + "studyImportFromChapterX": "从 {param} 导入", + "studyOrientation": "视角", + "studyAnalysisMode": "分析模式", + "studyPinnedChapterComment": "置顶评论", + "studySaveChapter": "保存章节", + "studyClearAnnotations": "清除注释", + "studyClearVariations": "清除变着", + "studyDeleteChapter": "删除章节", + "studyDeleteThisChapter": "删除本章节?本操作无法撤销!", + "studyClearAllCommentsInThisChapter": "清除章节中所有信息?", + "studyRightUnderTheBoard": "正下方", + "studyNoPinnedComment": "不需要", + "studyNormalAnalysis": "普通模式", + "studyHideNextMoves": "隐藏下一步", + "studyInteractiveLesson": "互动课", + "studyChapterX": "章节 {param}", + "studyEmpty": "空白", + "studyStartFromInitialPosition": "从初始局面开始", + "studyEditor": "编辑器", + "studyStartFromCustomPosition": "从自定义局面开始", + "studyLoadAGameByUrl": "通过 URL 加载游戏", + "studyLoadAPositionFromFen": "从 FEN 加载一个局面", + "studyLoadAGameFromPgn": "从 PGN 文件加载游戏", + "studyAutomatic": "自动", + "studyUrlOfTheGame": "游戏的 URL", + "studyLoadAGameFromXOrY": "从 {param1} 或 {param2} 加载游戏", + "studyCreateChapter": "创建章节", + "studyCreateStudy": "创建课程", + "studyEditStudy": "编辑课程", + "studyVisibility": "权限", + "studyPublic": "公开", + "studyUnlisted": "未列出", + "studyInviteOnly": "仅限邀请", + "studyAllowCloning": "允许复制", + "studyNobody": "没人", + "studyOnlyMe": "仅自己", + "studyContributors": "贡献者", + "studyMembers": "成员", + "studyEveryone": "所有人", + "studyEnableSync": "允许同步", + "studyYesKeepEveryoneOnTheSamePosition": "确认:每个人都处于同样的局面", + "studyNoLetPeopleBrowseFreely": "取消:让玩家自由选择", + "studyPinnedStudyComment": "置顶评论", + "studyStart": "开始", + "studySave": "保存", + "studyClearChat": "清空对话", + "studyDeleteTheStudyChatHistory": "删除课程聊天记录?本操作无法撤销!", + "studyDeleteStudy": "删除课程", + "studyConfirmDeleteStudy": "确定删除整个研讨?该操作不可恢复,输入研讨名以确认:{param}", + "studyWhereDoYouWantToStudyThat": "你想从哪里开始此项研究?", + "studyGoodMove": "好棋", + "studyMistake": "错着", + "studyBrilliantMove": "极好", + "studyBlunder": "漏着", + "studyInterestingMove": "略好", + "studyDubiousMove": "略坏", + "studyOnlyMove": "唯一着法", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "均势", + "studyUnclearPosition": "局势不明", + "studyWhiteIsSlightlyBetter": "白方略优", + "studyBlackIsSlightlyBetter": "黑方略优", + "studyWhiteIsBetter": "白方占优", + "studyBlackIsBetter": "黑方占优", + "studyWhiteIsWinning": "白方即胜", + "studyBlackIsWinning": "黑方即胜", + "studyNovelty": "新奇的", + "studyDevelopment": "发展", + "studyInitiative": "占据主动", + "studyAttack": "攻击", + "studyCounterplay": "反击", + "studyTimeTrouble": "无暇多虑", + "studyWithCompensation": "优势补偿", + "studyWithTheIdea": "教科书式的", + "studyNextChapter": "下一章节", + "studyPrevChapter": "上一章节", + "studyStudyActions": "研讨操作", + "studyTopics": "主题", + "studyMyTopics": "我的主题", + "studyPopularTopics": "热门主题", + "studyManageTopics": "管理主题", + "studyBack": "回到起始", + "studyPlayAgain": "重玩", + "studyWhatWouldYouPlay": "你会在这个位置上怎么走?", + "studyYouCompletedThisLesson": "恭喜!你完成了这个课程!", + "studyNbChapters": "{count, plural, other{共 {count} 章}}", + "studyNbGames": "{count, plural, other{共 {count} 盘棋}}", + "studyNbMembers": "{count, plural, other{{count} 位成员}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此粘贴你的 PGN 文本,最多支持 {count} 个游戏}}", + "timeagoJustNow": "刚刚", + "timeagoRightNow": "刚刚", + "timeagoCompleted": "已完成", + "timeagoInNbSeconds": "{count, plural, other{在 {count} 秒内}}", + "timeagoInNbMinutes": "{count, plural, other{在 {count} 分钟内}}", + "timeagoInNbHours": "{count, plural, other{在 {count} 小时内}}", + "timeagoInNbDays": "{count, plural, other{在 {count} 天内}}", + "timeagoInNbWeeks": "{count, plural, other{在 {count} 周内}}", + "timeagoInNbMonths": "{count, plural, other{在 {count} 月内}}", + "timeagoInNbYears": "{count, plural, other{在 {count} 年内}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} 分钟前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} 小时前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} 天前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} 周前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} 月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} 年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{还剩 {count} 分钟}}", + "timeagoNbHoursRemaining": "{count, plural, other{还剩 {count} 小时}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh_TW.arb b/lib/l10n/lila_zh_TW.arb index 4ccbf61727..5c78ee9052 100644 --- a/lib/l10n/lila_zh_TW.arb +++ b/lib/l10n/lila_zh_TW.arb @@ -1,45 +1,145 @@ { - "mobileHomeTab": "主頁", - "mobilePuzzlesTab": "謎題", - "mobileToolsTab": "工具", - "mobileWatchTab": "觀看", - "mobileSettingsTab": "設置", - "mobileMustBeLoggedIn": "你必須登入才能查看此頁面。", - "mobileSystemColors": "系统颜色", + "mobileAllGames": "所有棋局", + "mobileAreYouSure": "您確定嗎?", + "mobileBlindfoldMode": "盲棋", + "mobileCancelTakebackOffer": "取消悔棋請求", + "mobileClearButton": "清除", + "mobileCorrespondenceClearSavedMove": "清除已儲存移動", + "mobileCustomGameJoinAGame": "加入棋局", "mobileFeedbackButton": "問題反饋", + "mobileGreeting": "您好, {param}", + "mobileGreetingWithoutName": "您好", + "mobileHideVariation": "隱藏變體", + "mobileHomeTab": "首頁", + "mobileLiveStreamers": "Lichess 實況主", + "mobileMustBeLoggedIn": "你必須登入才能查看此頁面。", + "mobileNoSearchResults": "沒有任何搜尋結果", + "mobileNotFollowingAnyUser": "您未被任何使用者追蹤。", "mobileOkButton": "確認", + "mobilePlayersMatchingSearchTerm": "名稱包含「{param}」的玩家", + "mobilePrefMagnifyDraggedPiece": "放大被拖曳的棋子", + "mobilePuzzleStormConfirmEndRun": "是否中斷於此?", + "mobilePuzzleStormFilterNothingToShow": "沒有內容可顯示,請更改篩選條件", + "mobilePuzzleStormNothingToShow": "沒有內容可顯示。您可以進行一些 Puzzle Storm 。", + "mobilePuzzleStormSubtitle": "在三分鐘內解開盡可能多的謎題", + "mobilePuzzleStreakAbortWarning": "這將失去目前的連勝並且將儲存目前成績。", + "mobilePuzzleThemesSubtitle": "從您喜歡的開局進行謎題,或選擇一個主題。", + "mobilePuzzlesTab": "謎題", + "mobileRecentSearches": "搜尋紀錄", "mobileSettingsHapticFeedback": "震動回饋", "mobileSettingsImmersiveMode": "沉浸模式", - "mobileAllGames": "所有遊戲", - "mobileRecentSearches": "最近搜尋", - "mobileClearButton": "清除", - "mobileNoSearchResults": "無結果", - "mobileAreYouSure": "您確定嗎?", + "mobileSettingsImmersiveModeSubtitle": "在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。", + "mobileSettingsTab": "設定", "mobileShareGamePGN": "分享 PGN", - "mobileCustomGameJoinAGame": "加入遊戲", + "mobileShareGameURL": "分享對局網址", + "mobileSharePositionAsFEN": "以 FEN 分享棋局位置", + "mobileSharePuzzle": "分享這個謎題", + "mobileShowComments": "顯示留言", + "mobileShowResult": "顯示結果", + "mobileShowVariations": "顯示變體", + "mobileSomethingWentWrong": "發生了一些問題。", + "mobileSystemColors": "系統顏色", + "mobileToolsTab": "工具", + "mobileWaitingForOpponentToJoin": "正在等待對手加入...", + "mobileWatchTab": "觀戰", "activityActivity": "活動", "activityHostedALiveStream": "主持一個現場直播", - "activityRankedInSwissTournament": "在{param2}中排名{param1}", - "activitySignedUp": "在lichess.org中註冊", - "activitySupportedNbMonths": "{count, plural, other{以{param2}的身分支持lichess.org{count}個月}}", - "activityPracticedNbPositions": "{count, plural, other{在{param2}練習了{count}個棋局}}", - "activitySolvedNbPuzzles": "{count, plural, other{解決了{count}個戰術題目}}", - "activityPlayedNbGames": "{count, plural, other{下了{count}場{param2}類型的棋局}}", - "activityPostedNbMessages": "{count, plural, other{在{param2}發表了{count}則訊息}}", - "activityPlayedNbMoves": "{count, plural, other{下了{count}步}}", - "activityInNbCorrespondenceGames": "{count, plural, other{在{count}場長時間棋局中}}", - "activityCompletedNbGames": "{count, plural, other{完成了{count}場長時間棋局}}", - "activityFollowedNbPlayers": "{count, plural, other{開始關注{count}個玩家}}", - "activityGainedNbFollowers": "{count, plural, other{增加了{count}個追蹤者}}", + "activityRankedInSwissTournament": "在{param2}中排名 {param1}", + "activitySignedUp": "在 lichess.org 中註冊", + "activitySupportedNbMonths": "{count, plural, other{以 {param2} 身分贊助 lichess.org {count} 個月}}", + "activityPracticedNbPositions": "{count, plural, other{在 {param2} 練習了 {count} 個棋局}}", + "activitySolvedNbPuzzles": "{count, plural, other{解決了 {count} 個戰術題目}}", + "activityPlayedNbGames": "{count, plural, other{下了 {count} 場{param2}類型的棋局}}", + "activityPostedNbMessages": "{count, plural, other{在「{param2}」發表了 {count} 則訊息}}", + "activityPlayedNbMoves": "{count, plural, other{下了 {count} 步}}", + "activityInNbCorrespondenceGames": "{count, plural, other{在 {count} 場通信棋局中}}", + "activityCompletedNbGames": "{count, plural, other{完成了 {count} 場通信棋局}}", + "activityCompletedNbVariantGames": "{count, plural, other{完成了 {count} {param2} 場通信棋局}}", + "activityFollowedNbPlayers": "{count, plural, other{開始關注 {count} 個玩家}}", + "activityGainedNbFollowers": "{count, plural, other{增加了 {count} 個追蹤者}}", "activityHostedNbSimuls": "{count, plural, other{主持了{count}場車輪戰}}", "activityJoinedNbSimuls": "{count, plural, other{加入了{count}場車輪戰}}", "activityCreatedNbStudies": "{count, plural, other{創造了{count}個新的研究}}", "activityCompetedInNbTournaments": "{count, plural, other{完成了{count}場錦標賽}}", "activityRankedInTournament": "{count, plural, other{在{param4}錦標賽中下了{param3}盤棋局,排名第{count}(前{param2}%)}}", - "activityCompetedInNbSwissTournaments": "{count, plural, other{參與過{count}'場瑞士制錦標賽}}", - "activityJoinedNbTeams": "{count, plural, other{加入{count}團隊}}", + "activityCompetedInNbSwissTournaments": "{count, plural, other{參與過 {count} 場瑞士制錦標賽}}", + "activityJoinedNbTeams": "{count, plural, other{加入 {count} 團隊}}", "broadcastBroadcasts": "比賽直播", + "broadcastMyBroadcasts": "我的直播", "broadcastLiveBroadcasts": "錦標賽直播", + "broadcastBroadcastCalendar": "直播時程表", + "broadcastNewBroadcast": "新的現場直播", + "broadcastSubscribedBroadcasts": "已訂閱的直播", + "broadcastAboutBroadcasts": "關於直播", + "broadcastHowToUseLichessBroadcasts": "如何使用 Lichess 比賽直播", + "broadcastTheNewRoundHelp": "新的一局會有跟上一局相同的成員與貢獻者", + "broadcastAddRound": "新增回合", + "broadcastOngoing": "進行中", + "broadcastUpcoming": "即將舉行", + "broadcastCompleted": "已結束", + "broadcastCompletedHelp": "Lichess 偵測棋局的結束,但有可能會偵測錯誤。請在這自行設定。", + "broadcastRoundName": "回合名稱", + "broadcastRoundNumber": "回合數", + "broadcastTournamentName": "錦標賽名稱", + "broadcastTournamentDescription": "簡短比賽說明", + "broadcastFullDescription": "完整比賽說明", + "broadcastFullDescriptionHelp": "直播內容的詳細描述 。可以利用 {param1}。字數限於{param2}個字。", + "broadcastSourceSingleUrl": "PGN 來源網址", + "broadcastSourceUrlHelp": "Lichess 將以該網址更新PGN數據,網址必須公開", + "broadcastSourceGameIds": "最多 64 個以空格分開的 Lichess 棋局序號。", + "broadcastStartDateTimeZone": "當地時區的錦標賽起始日期:{param}", + "broadcastStartDateHelp": "可選,如果知道比賽開始時間", + "broadcastCurrentGameUrl": "目前棋局連結", + "broadcastDownloadAllRounds": "下載所有棋局", + "broadcastResetRound": "重設此回合", + "broadcastDeleteRound": "刪除此回合", + "broadcastDefinitivelyDeleteRound": "刪除這局以及其所有棋局", + "broadcastDeleteAllGamesOfThisRound": "刪除所有此輪的棋局。直播來源必須是開啟的以成功重新建立棋局。", + "broadcastEditRoundStudy": "編輯此輪研究", + "broadcastDeleteTournament": "刪除此錦標賽", + "broadcastDefinitivelyDeleteTournament": "刪除錦標賽以及所有棋局", + "broadcastShowScores": "根據比賽結果顯示玩家分數", + "broadcastReplacePlayerTags": "取代玩家名字、評級、以及頭銜(選填)", + "broadcastFideFederations": "FIDE 國別", + "broadcastTop10Rating": "前 10 名平均評級", + "broadcastFidePlayers": "FIDE 玩家", + "broadcastFidePlayerNotFound": "找不到 FIDE 玩家", + "broadcastFideProfile": "FIDE 序號", + "broadcastFederation": "國籍", + "broadcastAgeThisYear": "年齡", + "broadcastUnrated": "未評級", + "broadcastRecentTournaments": "最近錦標賽", + "broadcastOpenLichess": "在 lichess 中開啟", + "broadcastTeams": "團隊", + "broadcastBoards": "棋局", + "broadcastOverview": "概覽", + "broadcastSubscribeTitle": "訂閱以在每輪開始時獲得通知。您可以在帳戶設定中切換直播的鈴聲或推播通知。", + "broadcastUploadImage": "上傳錦標賽圖片", + "broadcastNoBoardsYet": "尚無棋局。這些棋局將在對局上傳後顯示。", + "broadcastBoardsCanBeLoaded": "棋盤能夠以輸入源投放或是利用{param}", + "broadcastStartsAfter": "於{param}開始", + "broadcastStartVerySoon": "直播即將開始。", + "broadcastNotYetStarted": "直播尚未開始。", + "broadcastOfficialWebsite": "官網", + "broadcastStandings": "排行榜", + "broadcastIframeHelp": "更多選項在{param}", + "broadcastWebmastersPage": "網頁管理員頁面", + "broadcastPgnSourceHelp": "這一輪的公開實時 PGN。我們還提供{param}以實現更快和更高效的同步。", + "broadcastEmbedThisBroadcast": "將此直播嵌入您的網站", + "broadcastEmbedThisRound": "將{param}嵌入您的網站", + "broadcastRatingDiff": "評級差異", + "broadcastGamesThisTournament": "此比賽的對局", + "broadcastScore": "分數", + "broadcastAllTeams": "所有團隊", + "broadcastTournamentFormat": "錦標賽格式", + "broadcastTournamentLocation": "錦標賽地點", + "broadcastTopPlayers": "排行榜", + "broadcastTimezone": "時區", + "broadcastFideRatingCategory": "FIDE 評級類別", + "broadcastOptionalDetails": "其他細節", + "broadcastPastBroadcasts": "直播紀錄", + "broadcastAllBroadcastsByMonth": "以月份顯示所有直播", + "broadcastNbBroadcasts": "{count, plural, other{{count} 個直播}}", "challengeChallengesX": "挑戰: {param1}", "challengeChallengeToPlay": "邀請對弈", "challengeChallengeDeclined": "對弈邀請已拒絕", @@ -96,13 +196,13 @@ "perfStatLessThanOneHour": "兩場間距不到一小時", "perfStatMaxTimePlaying": "最高奕棋時間", "perfStatNow": "現在", - "preferencesPreferences": "偏好設置", + "preferencesPreferences": "偏好設定", "preferencesDisplay": "顯示", "preferencesPrivacy": "隱私", "preferencesNotifications": "通知", "preferencesPieceAnimation": "棋子動畫", "preferencesMaterialDifference": "子力差距", - "preferencesBoardHighlights": "棋盤高亮 (最後一步與將軍)", + "preferencesBoardHighlights": "國王紅色亮光(最後一步與將軍)", "preferencesPieceDestinations": "棋子目的地(有效走法與預先走棋)", "preferencesBoardCoordinates": "棋盤座標(A-H, 1-8)", "preferencesMoveListWhilePlaying": "遊戲進行時顯示棋譜", @@ -111,6 +211,7 @@ "preferencesPgnLetter": "字母 (K, Q, R, B, N)", "preferencesZenMode": "專注模式", "preferencesShowPlayerRatings": "顯示玩家等級分", + "preferencesShowFlairs": "顯示玩家身分", "preferencesExplainShowPlayerRatings": "這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。", "preferencesDisplayBoardResizeHandle": "顯示盤面大小調整區塊", "preferencesOnlyOnInitialPosition": "只在起始局面", @@ -162,6 +263,7 @@ "preferencesNotifyWeb": "瀏覽器通知", "preferencesNotifyDevice": "設備通知", "preferencesBellNotificationSound": "通知鈴聲", + "preferencesBlindfold": "盲棋", "puzzlePuzzles": "謎題", "puzzlePuzzleThemes": "謎題主題", "puzzleRecommended": "推薦", @@ -175,18 +277,26 @@ "puzzleSpecialMoves": "特殊移動", "puzzleDidYouLikeThisPuzzle": "您喜歡這道謎題嗎?", "puzzleVoteToLoadNextOne": "告訴我們加載下一題!", + "puzzleUpVote": "投票為好謎題", + "puzzleDownVote": "投票為壞謎題", "puzzleYourPuzzleRatingWillNotChange": "您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。", "puzzleFindTheBestMoveForWhite": "為白方找出最佳移動", "puzzleFindTheBestMoveForBlack": "為黑方找出最佳移動", "puzzleToGetPersonalizedPuzzles": "得到個人推薦題目:", "puzzlePuzzleId": "謎題 {param}", "puzzlePuzzleOfTheDay": "每日一題", + "puzzleDailyPuzzle": "每日謎題", "puzzleClickToSolve": "點擊解題", "puzzleGoodMove": "好棋", "puzzleBestMove": "最佳走法!", "puzzleKeepGoing": "加油!", "puzzlePuzzleSuccess": "成功!", "puzzlePuzzleComplete": "解題完成!", + "puzzleByOpenings": "以開局區分", + "puzzlePuzzlesByOpenings": "以開局區分謎題", + "puzzleOpeningsYouPlayedTheMost": "您最常使用的開局", + "puzzleUseFindInPage": "在瀏覽器中使用「在頁面中尋找」以尋找你最喜歡的開局!", + "puzzleUseCtrlF": "按下 Ctrl+f 以找出您最喜歡的開局方式!", "puzzleNotTheMove": "不是這步!", "puzzleTrySomethingElse": "試試其他的移動", "puzzleRatingX": "評級:{param}", @@ -201,6 +311,7 @@ "puzzleHardest": "超困難", "puzzleExample": "範例", "puzzleAddAnotherTheme": "加入其他主題", + "puzzleNextPuzzle": "下個謎題", "puzzleJumpToNextPuzzleImmediately": "立即跳到下一個謎題", "puzzlePuzzleDashboard": "謎題能力分析", "puzzleImprovementAreas": "弱點", @@ -217,7 +328,7 @@ "puzzleLookupOfPlayer": "尋找其他棋手的棋局謎題", "puzzleFromXGames": "來自{param}棋局的謎題", "puzzleSearchPuzzles": "尋找謎題", - "puzzleFromMyGamesNone": "你在數據庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加添加拼圖的機會!", + "puzzleFromMyGamesNone": "你在資料庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加從你的棋局中生成謎題的機會!", "puzzleFromXGamesFound": "在{param2}中找到{param1}個謎題", "puzzlePuzzleDashboardDescription": "訓練、分析、改進", "puzzlePercentSolved": "{param} 已解決", @@ -231,39 +342,71 @@ "puzzleNbToReplay": "{count, plural, other{{count} 重玩}}", "puzzleThemeAdvancedPawn": "升變兵", "puzzleThemeAdvancedPawnDescription": "你的其中一個兵已經深入了對方的棋位,或許要威脅升變。", - "puzzleThemeAdvantage": "擁有優勢", + "puzzleThemeAdvantage": "取得優勢", + "puzzleThemeAdvantageDescription": "把握機會以取得決定性優勢。(200 厘兵 ≤ 評估值 ≤ 600 厘兵)", "puzzleThemeAnastasiaMate": "阿納斯塔西亞殺法", + "puzzleThemeAnastasiaMateDescription": "馬與車或后聯手在棋盤邊困住對手國王以及另一對手棋子", "puzzleThemeArabianMate": "阿拉伯殺法", "puzzleThemeArabianMateDescription": "馬和車聯手把對方的王困住在角落的位置", - "puzzleThemeAttackingF2F7": "攻擊f2或f7", + "puzzleThemeAttackingF2F7": "攻擊 f2 或 f7", + "puzzleThemeAttackingF2F7Description": "專注於 f2 或 f7 兵的攻擊,像是 fried liver 攻擊", "puzzleThemeAttraction": "吸引", + "puzzleThemeAttractionDescription": "一種換子或犧牲強迫對手旗子到某格以好進行接下來的戰術。", "puzzleThemeBackRankMate": "後排將死", - "puzzleThemeBackRankMateDescription": "在對方的王在底線被自身的棋子困住時,將殺對方的王", - "puzzleThemeBishopEndgame": "象殘局", + "puzzleThemeBackRankMateDescription": "在對方的王於底線被自身的棋子困住時,將死對方的王", + "puzzleThemeBishopEndgame": "主教殘局", "puzzleThemeBishopEndgameDescription": "只剩象和兵的殘局", "puzzleThemeBodenMate": "波登殺法", + "puzzleThemeBodenMateDescription": "以在對角線上的兩個主教將死被自身棋子困住的王。", "puzzleThemeCastling": "易位", + "puzzleThemeCastlingDescription": "讓國王回到安全,並讓車發動攻擊。", "puzzleThemeCapturingDefender": "吃子 - 防守者", + "puzzleThemeCapturingDefenderDescription": "移除防守其他棋子的防守者以攻擊未被保護的棋子", "puzzleThemeCrushing": "壓倒性優勢", "puzzleThemeCrushingDescription": "察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)", "puzzleThemeDoubleBishopMate": "雙主教將死", + "puzzleThemeDoubleBishopMateDescription": "相鄰對角線上的兩個主教將將死被自身棋子困住的王。", + "puzzleThemeDovetailMate": "柯齊奧將死", + "puzzleThemeDovetailMateDescription": "以皇后將死被自身棋子困住的國王", "puzzleThemeEquality": "均勢", + "puzzleThemeEqualityDescription": "從劣勢中反敗為和。(分析值 ≤ 200厘兵)", "puzzleThemeKingsideAttack": "王翼攻擊", + "puzzleThemeKingsideAttackDescription": "在對方於王翼易位後的攻擊。", "puzzleThemeClearance": "騰挪", + "puzzleThemeClearanceDescription": "為了施展戰術而清除我方攻擊格上的障礙物。", "puzzleThemeDefensiveMove": "加強防守", + "puzzleThemeDefensiveMoveDescription": "一種為了避免遺失棋子或優勢而採取的必要行動。", "puzzleThemeDeflection": "引離", + "puzzleThemeDeflectionDescription": "為了分散敵人專注力所採取的戰術,容易搗亂敵人原本的計畫。", "puzzleThemeDiscoveredAttack": "閃擊", + "puzzleThemeDiscoveredAttackDescription": "將一子(例如騎士)移開長程攻擊格(例如城堡)。", "puzzleThemeDoubleCheck": "雙將", + "puzzleThemeDoubleCheckDescription": "雙重將軍,讓我方能攻擊敵人的他子。", "puzzleThemeEndgame": "殘局", "puzzleThemeEndgameDescription": "棋局中最後階段的戰術", + "puzzleThemeEnPassantDescription": "一種食敵方過路兵的戰略。", + "puzzleThemeExposedKing": "未被保護的國王", + "puzzleThemeExposedKingDescription": "攻擊未被保護的國王之戰術,常常導致將死。", "puzzleThemeFork": "捉雙", + "puzzleThemeForkDescription": "一種同時攻擊敵方多個子,使敵方只能犧牲一子的戰術。", + "puzzleThemeHangingPiece": "懸子", + "puzzleThemeHangingPieceDescription": "「免費」取得他子的戰術", + "puzzleThemeHookMate": "鉤將死", + "puzzleThemeHookMateDescription": "利用車馬兵與一敵方兵以限制敵方國王的逃生路線。", + "puzzleThemeInterference": "干擾", + "puzzleThemeInterferenceDescription": "將一子擋在兩個敵方子之間以切斷防護,例如以騎士在兩車之間阻擋。", + "puzzleThemeIntermezzo": "Intermezzo", + "puzzleThemeIntermezzoDescription": "與其走正常的棋譜,不如威脅敵方子吧!這樣不但可以破壞敵方原先計畫,還可以讓敵人必須對威脅採取對應的動作。這種戰術又稱為「Zwischenzug」或「In between」。", "puzzleThemeKnightEndgame": "馬殘局", "puzzleThemeKnightEndgameDescription": "只剩馬和兵的殘局", "puzzleThemeLong": "長謎題", "puzzleThemeLongDescription": "三步獲勝", "puzzleThemeMaster": "大師棋局", + "puzzleThemeMasterDescription": "從頭銜玩家的棋局中生成的謎題。", "puzzleThemeMasterVsMaster": "大師對局", + "puzzleThemeMasterVsMasterDescription": "從兩位頭銜玩家的棋局中生成的謎題。", "puzzleThemeMate": "將軍", + "puzzleThemeMateDescription": "以你的技能贏得勝利", "puzzleThemeMateIn1": "一步殺棋", "puzzleThemeMateIn1Description": "一步將軍", "puzzleThemeMateIn2": "兩步殺棋", @@ -273,6 +416,7 @@ "puzzleThemeMateIn4": "四步殺棋", "puzzleThemeMateIn4Description": "走四步以達到將軍", "puzzleThemeMateIn5": "五步或更高 將軍", + "puzzleThemeMateIn5Description": "看出較長的將死步驟。", "puzzleThemeMiddlegame": "中局", "puzzleThemeMiddlegameDescription": "棋局中第二階段的戰術", "puzzleThemeOneMove": "一步題", @@ -282,46 +426,61 @@ "puzzleThemePawnEndgame": "兵殘局", "puzzleThemePawnEndgameDescription": "只剩兵的殘局", "puzzleThemePin": "牽制", + "puzzleThemePinDescription": "一種涉及「牽制」,讓一敵方子無法在讓其他更高價值的子不被受到攻擊下移動的戰術。", "puzzleThemePromotion": "升變", + "puzzleThemePromotionDescription": "讓兵走到後排升變為皇后或其他高價值的子。", "puzzleThemeQueenEndgame": "后殘局", "puzzleThemeQueenEndgameDescription": "只剩后和兵的殘局", "puzzleThemeQueenRookEndgame": "后與車", "puzzleThemeQueenRookEndgameDescription": "只剩后、車和兵的殘局", "puzzleThemeQueensideAttack": "后翼攻擊", + "puzzleThemeQueensideAttackDescription": "在對方於后翼易位後的攻擊。", "puzzleThemeQuietMove": "安靜的一着", + "puzzleThemeQuietMoveDescription": "隱藏在未來敵方無法避免的攻擊。", "puzzleThemeRookEndgame": "車殘局", "puzzleThemeRookEndgameDescription": "只剩車和兵的殘局", "puzzleThemeSacrifice": "棄子", + "puzzleThemeSacrificeDescription": "犧牲我方子以在一系列的移動後得到優勢。", "puzzleThemeShort": "短謎題", "puzzleThemeShortDescription": "兩步獲勝", "puzzleThemeSkewer": "串擊", + "puzzleThemeSkewerDescription": "攻擊敵方高價值的子以讓敵方移開,以攻擊背後較為低價值未受保護的他子。為一種反向的「牽制」。", "puzzleThemeSmotheredMate": "悶殺", + "puzzleThemeSmotheredMateDescription": "一種以馬將死被自身棋子所圍困的國王。", "puzzleThemeSuperGM": "超級大師賽局", "puzzleThemeSuperGMDescription": "來自世界各地優秀玩家對局的戰術題", "puzzleThemeTrappedPiece": "被困的棋子", + "puzzleThemeTrappedPieceDescription": "一子因為被限制逃生路線而無法逃離被犧牲的命運。", "puzzleThemeUnderPromotion": "升變", "puzzleThemeUnderPromotionDescription": "升變成騎士、象或車", "puzzleThemeVeryLong": "非常長的謎題", "puzzleThemeVeryLongDescription": "四步或以上獲勝", "puzzleThemeXRayAttack": "穿透攻擊", + "puzzleThemeXRayAttackDescription": "以敵方子攻擊或防守的戰術。", "puzzleThemeZugzwang": "等著", - "puzzleThemeHealthyMix": "綜合", - "puzzleThemeHealthyMixDescription": "所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。", + "puzzleThemeZugzwangDescription": "對方的棋子因為所移動的空間有限所以所到之處都會增加對方劣勢", + "puzzleThemeMix": "綜合", + "puzzleThemeMixDescription": "所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。", + "puzzleThemePlayerGames": "玩家謎題", + "puzzleThemePlayerGamesDescription": "查詢從你或其他玩家的對奕所生成的謎題。", + "puzzleThemePuzzleDownloadInformation": "這些為公開謎題,並且在 {param} 提供下載管道。", "searchSearch": "搜尋", "settingsSettings": "設定", "settingsCloseAccount": "關閉帳戶", - "settingsClosingIsDefinitive": "您確定要刪除帳號嗎?這是不能挽回的", + "settingsManagedAccountCannotBeClosed": "您的帳號已被管理並且無法關閉。", + "settingsClosingIsDefinitive": "您確定要刪除帳號嗎?這是無法挽回的。", "settingsCantOpenSimilarAccount": "即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶", "settingsChangedMindDoNotCloseAccount": "我改變主意了,不要關閉我的帳號", - "settingsCloseAccountExplanation": "您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登錄。", + "settingsCloseAccountExplanation": "您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登入。", "settingsThisAccountIsClosed": "此帳號已被關閉。", "playWithAFriend": "和好友下棋", "playWithTheMachine": "和電腦下棋", - "toInviteSomeoneToPlayGiveThisUrl": "邀人下棋,請分享這個網址", + "toInviteSomeoneToPlayGiveThisUrl": "請分享此網址以邀人下棋", "gameOver": "遊戲結束", "waitingForOpponent": "等待對手", - "waiting": "請稍等", - "yourTurn": "該你走", + "orLetYourOpponentScanQrCode": "或是讓對手掃描這個 QR code", + "waiting": "等待對手確認中", + "yourTurn": "該您走", "aiNameLevelAiLevel": "{param1}等級 {param2}", "level": "難度", "strength": "強度", @@ -330,24 +489,24 @@ "resign": "認輸", "checkmate": "將死", "stalemate": "逼和", - "white": "白方", - "black": "黑方", - "asWhite": "使用白棋", - "asBlack": "使用黑棋", + "white": "執白", + "black": "執黑", + "asWhite": "作為白方", + "asBlack": "作為黑方", "randomColor": "隨機選色", "createAGame": "開始對局", "whiteIsVictorious": "白方勝", "blackIsVictorious": "黑方勝", "youPlayTheWhitePieces": "您執白棋", "youPlayTheBlackPieces": "您執黑棋", - "itsYourTurn": "輪到你了!", + "itsYourTurn": "該您走!", "cheatDetected": "偵測到作弊行為", "kingInTheCenter": "王居中", "threeChecks": "三次將軍", - "raceFinished": "競王結束", - "variantEnding": "另類終局", + "raceFinished": "王至第八排", + "variantEnding": "變體終局", "newOpponent": "換個對手", - "yourOpponentWantsToPlayANewGameWithYou": "你的對手想和你複賽", + "yourOpponentWantsToPlayANewGameWithYou": "您的對手想和你複賽", "joinTheGame": "加入這盤棋", "whitePlays": "白方走棋", "blackPlays": "黑方走棋", @@ -358,19 +517,19 @@ "theFirstPersonToComeOnThisUrlWillPlayWithYou": "第一個訪問該網址的人將與您下棋。", "whiteResigned": "白方認輸", "blackResigned": "黑方認輸", - "whiteLeftTheGame": "白方棄局", - "blackLeftTheGame": "黑方棄局", + "whiteLeftTheGame": "白方棄賽", + "blackLeftTheGame": "黑方棄賽", "whiteDidntMove": "白方沒有走棋", "blackDidntMove": "黑方沒有走棋", "requestAComputerAnalysis": "請求電腦分析", "computerAnalysis": "電腦分析", "computerAnalysisAvailable": "電腦分析可用", - "computerAnalysisDisabled": "電腦分析未啟用", + "computerAnalysisDisabled": "未啟用電腦分析", "analysis": "分析棋局", "depthX": "深度 {param}", "usingServerAnalysis": "正在使用伺服器分析", "loadingEngine": "正在載入引擎 ...", - "calculatingMoves": "計算著法中。。。", + "calculatingMoves": "計算著法中...", "engineFailed": "加載引擎出錯", "cloudAnalysis": "雲端分析", "goDeeper": "深入分析", @@ -380,6 +539,8 @@ "promoteVariation": "增加變化", "makeMainLine": "將這步棋導入主要流程中", "deleteFromHere": "從這處開始刪除", + "collapseVariations": "隱藏變體", + "expandVariations": "顯示變體", "forceVariation": "移除變化", "copyVariationPgn": "複製變體 PGN", "move": "走棋", @@ -402,7 +563,7 @@ "dtzWithRounding": "經過四捨五入的DTZ50'',是基於到下次吃子或兵動的半步數目。", "noGameFound": "未找到遊戲", "maxDepthReached": "已達到最大深度!", - "maybeIncludeMoreGamesFromThePreferencesMenu": "試著從偏好設置中加入更多棋局", + "maybeIncludeMoreGamesFromThePreferencesMenu": "試著從設定中加入更多棋局", "openings": "開局", "openingExplorer": "開局瀏覽器", "openingEndgameExplorer": "開局與終局瀏覽器", @@ -419,19 +580,17 @@ "deleteThisImportedGame": "刪除此匯入的棋局?", "replayMode": "重播模式", "realtimeReplay": "實時", - "byCPL": "CPL", - "openStudy": "打開研究視窗", - "enable": "開啟", + "byCPL": "以厘兵損失", + "enable": "啟用", "bestMoveArrow": "最佳移動的箭頭", "showVariationArrows": "顯示變體箭頭", - "evaluationGauge": "棋力估計表", + "evaluationGauge": "評估條", "multipleLines": "路線分析線", - "cpus": "CPU", + "cpus": "CPU 數量", "memory": "記憶體", "infiniteAnalysis": "無限分析", - "removesTheDepthLimit": "取消深度限制,使您的電腦發熱。", - "engineManager": "引擎管理", - "blunder": "嚴重錯誤", + "removesTheDepthLimit": "取消深度限制,可能會使您的電腦發熱。", + "blunder": "漏著", "mistake": "錯誤", "inaccuracy": "輕微失誤", "moveTimes": "走棋時間", @@ -456,6 +615,7 @@ "latestForumPosts": "最新論壇貼文", "players": "棋手", "friends": "朋友", + "otherPlayers": "其他玩家", "discussions": "對話", "today": "今天", "yesterday": "昨天", @@ -470,47 +630,47 @@ "time": "時間", "rating": "評級", "ratingStats": "評分數據", - "username": "用戶名", - "usernameOrEmail": "用戶名或電郵地址", - "changeUsername": "更改用戶名", + "username": "使用者名稱", + "usernameOrEmail": "使用者名稱或電郵地址", + "changeUsername": "更改使用者名稱", "changeUsernameNotSame": "只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。", - "changeUsernameDescription": "更改用戶名。您最多可以更改一次字母大小寫。", - "signupUsernameHint": "請選擇一個和諧的用戶名,用戶名無法再次更改,並且不合規的用戶名會導致帳戶被封禁!", + "changeUsernameDescription": "更改使用者名稱。您最多可以更改一次字母大小寫。", + "signupUsernameHint": "請選擇一個妥當的使用者名稱。請注意使用者名稱無法再次更改,並且不妥當的名稱會導致帳號被封禁!", "signupEmailHint": "僅用於密碼重置", "password": "密碼", "changePassword": "更改密碼", - "changeEmail": "更改電郵地址", - "email": "電郵地址", + "changeEmail": "更改電子郵件", + "email": "電子郵件", "passwordReset": "重置密碼", "forgotPassword": "忘記密碼?", "error_weakPassword": "此密碼太常見,且很容易被猜到。", - "error_namePassword": "請不要把密碼設為用戶名。", + "error_namePassword": "請不要把密碼設為使用者名稱。", "blankedPassword": "你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。", - "youAreLeavingLichess": "你正在離開 Lichess", + "youAreLeavingLichess": "你正要離開 Lichess", "neverTypeYourPassword": "不要在其他網站輸入你的 Lichess 密碼!", "proceedToX": "前往 {param}", "passwordSuggestion": "不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。", - "emailSuggestion": "不要使用他人提供的郵箱地址,他們會用它盜取你的帳戶。", - "emailConfirmHelp": "協助郵件確認", + "emailSuggestion": "不要使用他人提供的電子郵件,他們會用它盜取您的帳號。", + "emailConfirmHelp": "協助電郵確認", "emailConfirmNotReceived": "註冊後沒有收到確認郵件?", - "whatSignupUsername": "你用了什麼用戶名註冊?", - "usernameNotFound": "找不到用戶 {param}。", + "whatSignupUsername": "你用了什麼使用者名稱註冊?", + "usernameNotFound": "找不到使用者名稱 {param}。", "usernameCanBeUsedForNewAccount": "你可以使用這個用戶名創建帳戶", "emailSent": "我們向 {param} 發送了電子郵件。", "emailCanTakeSomeTime": "可能需要一些時間才能收到。", "refreshInboxAfterFiveMinutes": "等待5分鐘並刷新你的收件箱。", "checkSpamFolder": "嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。", "emailForSignupHelp": "如果其他所有的方法都失敗了,給我們發這條短信:", - "copyTextToEmail": "複製並粘貼上面的文本然後把它發給{param}", - "waitForSignupHelp": "我們很快就會給你回復,説明你完成註冊。", - "accountConfirmed": "這個使用者 {param} 成功地確認了", + "copyTextToEmail": "複製並貼上上面的文字然後把它發給{param}", + "waitForSignupHelp": "我們很快就會給你回覆,説明你完成註冊。", + "accountConfirmed": "使用者 {param} 認證成功", "accountCanLogin": "你可以做為 {param} 登入了。", "accountConfirmationEmailNotNeeded": "你不需要確認電子郵件。", "accountClosed": "帳戶 {param} 被關閉。", "accountRegisteredWithoutEmail": "帳戶 {param} 未使用電子郵箱註冊。", "rank": "排名", "rankX": "排名:{param}", - "gamesPlayed": "盤棋已結束", + "gamesPlayed": "下過局數", "cancel": "取消", "whiteTimeOut": "白方時間到", "blackTimeOut": "黑方時間到", @@ -530,6 +690,7 @@ "abortGame": "中止本局", "gameAborted": "棋局已中止", "standard": "標準", + "customPosition": "自定義局面", "unlimited": "無限", "mode": "模式", "casual": "休閒", @@ -549,13 +710,13 @@ "inbox": "收件箱", "chatRoom": "聊天室", "loginToChat": "登入以聊天", - "youHaveBeenTimedOut": "由於時間原因您不能發言", + "youHaveBeenTimedOut": "您已被禁言", "spectatorRoom": "觀眾室", "composeMessage": "寫信息", "subject": "主題", "send": "發送", "incrementInSeconds": "增加秒數", - "freeOnlineChess": "免費線上國際象棋", + "freeOnlineChess": "免費線上西洋棋", "exportGames": "導出棋局", "ratingRange": "對方級別範圍", "thisAccountViolatedTos": "此帳號違反了Lichess的使用規定", @@ -564,7 +725,7 @@ "proposeATakeback": "請求悔棋", "takebackPropositionSent": "悔棋請求已發送", "takebackPropositionDeclined": "悔棋請求被拒絕", - "takebackPropositionAccepted": "同意悔棋", + "takebackPropositionAccepted": "悔棋請求被接受", "takebackPropositionCanceled": "悔棋請求已取消", "yourOpponentProposesATakeback": "對手請求悔棋", "bookmarkThisGame": "收藏該棋局", @@ -573,7 +734,7 @@ "tournamentPoints": "錦標賽得分", "viewTournament": "觀看錦標賽", "backToTournament": "返回錦標賽主頁", - "noDrawBeforeSwissLimit": "在瑞士錦標賽中,在下三十步棋前你不能提和.", + "noDrawBeforeSwissLimit": "在積分循環制錦標賽中,在下三十步棋前無法和局。", "thematic": "特殊開局", "yourPerfRatingIsProvisional": "您目前的評分{param}為臨時評分", "yourPerfRatingIsTooHigh": "您的 {param1} 積分 ({param2}) 過高", @@ -598,16 +759,17 @@ "leaderboard": "排行榜", "screenshotCurrentPosition": "截圖當前頁面", "gameAsGIF": "保存棋局為 GIF", - "pasteTheFenStringHere": "在此處黏貼FEN棋譜", - "pasteThePgnStringHere": "在此處黏貼PGN棋譜", + "pasteTheFenStringHere": "在此處貼上 FEN 棋譜", + "pasteThePgnStringHere": "在此處貼上 PGN 棋譜", "orUploadPgnFile": "或者上傳一個PGN文件", "fromPosition": "自定義局面", - "continueFromHere": "从此處繼續", + "continueFromHere": "從此處繼續", "toStudy": "研究", "importGame": "導入棋局", "importGameExplanation": "貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。", - "importGameCaveat": "變著分支將被刪除。 若要保存這些變著,請通過導入PGN棋譜創建一個研究。", - "thisIsAChessCaptcha": "這是一個國際象棋驗證碼。", + "importGameCaveat": "變種分支將被刪除。 若要保存這些變種,請透過導入 PGN 棋譜建立一個研究。", + "importGameDataPrivacyWarning": "此為公開 PGN。若要導入私人棋局,請使用研究。", + "thisIsAChessCaptcha": "此為西洋棋驗證碼。", "clickOnTheBoardToMakeYourMove": "點擊棋盤走棋以證明您是人類。", "captcha_fail": "請完成驗證。", "notACheckmate": "沒有將死", @@ -615,6 +777,7 @@ "blackCheckmatesInOneMove": "黑方一步棋將死對手", "retry": "重試", "reconnecting": "重新連接中", + "noNetwork": "離線", "favoriteOpponents": "最喜歡的對手", "follow": "關注", "following": "已關注", @@ -624,7 +787,6 @@ "block": "加入黑名單", "blocked": "已加入黑名單", "unblock": "移除出黑名單", - "followsYou": "關注您", "xStartedFollowingY": "{param1}開始關注{param2}", "more": "更多", "memberSince": "註冊日期", @@ -635,12 +797,12 @@ "required": "必填項目。", "openTournaments": "公開錦標賽", "duration": "持續時間", - "winner": "勝利者", + "winner": "贏家", "standing": "名次", "createANewTournament": "建立新的錦標賽", "tournamentCalendar": "錦標賽日程", "conditionOfEntry": "加入限制:", - "advancedSettings": "高級設定", + "advancedSettings": "進階設定", "safeTournamentName": "幫錦標賽挑選一個適合的名字", "inappropriateNameWarning": "即便只是一點點的違規都有可能導致您的帳號被封鎖。", "emptyTournamentName": "若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。", @@ -675,7 +837,7 @@ "chess960StartPosition": "960棋局開局位置: {param}", "startPosition": "初始佈局", "clearBoard": "清空棋盤", - "loadPosition": "裝入佈局", + "loadPosition": "載入佈局", "isPrivate": "私人", "reportXToModerators": "將{param}報告給管理人員", "profileCompletion": "個人檔案完成度:{param}", @@ -683,9 +845,10 @@ "ifNoneLeaveEmpty": "如果沒有,請留空", "profile": "資料", "editProfile": "編輯資料", - "setFlair": "設置你的圖標", - "flair": "圖標", - "youCanHideFlair": "有一個設置可以隱藏整個網站上所有用户圖標。", + "realName": "真實名稱", + "setFlair": "設置你的身分", + "flair": "身分", + "youCanHideFlair": "你可以在設定中隱藏使用者身分。", "biography": "個人簡介", "countryRegion": "國家或地區", "thankYou": "謝謝!", @@ -702,41 +865,46 @@ "automaticallyProceedToNextGameAfterMoving": "移动棋子后自动进入下一盘棋", "autoSwitch": "自动更换", "puzzles": "謎題", - "name": "名", + "onlineBots": "線上機器人", + "name": "名稱", "description": "描述", "descPrivate": "內部簡介", "descPrivateHelp": "僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。", "no": "否", "yes": "是", + "website": "網頁版", + "mobile": "行動裝置", "help": "幫助:", - "createANewTopic": "新话题", - "topics": "话题", + "createANewTopic": "新話題", + "topics": "話題", "posts": "貼文", "lastPost": "最近貼文", - "views": "浏览", - "replies": "回复", - "replyToThisTopic": "回复此话题", - "reply": "回复", - "message": "信息", - "createTheTopic": "创建话题", - "reportAUser": "举报用户", - "user": "用户", + "views": "瀏覽", + "replies": "回覆", + "replyToThisTopic": "回覆此話題", + "reply": "回覆", + "message": "訊息", + "createTheTopic": "建立話題", + "reportAUser": "舉報使用者", + "user": "使用者", "reason": "原因", - "whatIsIheMatter": "举报原因?", + "whatIsIheMatter": "舉報原因?", "cheat": "作弊", - "troll": "钓鱼", + "troll": "搗亂", "other": "其他", - "reportDescriptionHelp": "附上游戏的网址解释该用户的行为问题", + "reportCheatBoostHelp": "請詳細說明你舉報此使用者的具體原因並貼上遊戲連結。「他作弊」等簡短說明是不被接受的。", + "reportUsernameHelp": "請詳細說明你舉報此使用者的具體原因。若必要請解釋其名詞的歷史意義、網路用語、或是此使用者名稱如何指桑罵槐。「他的使用者名稱不妥」等簡短說明是不被接受的。", + "reportProcessedFasterInEnglish": "若舉報內容為英文將會更快的被處理。", "error_provideOneCheatedGameLink": "請提供至少一局作弊棋局的連結。", - "by": "{param}作", - "importedByX": "由{param}滙入", - "thisTopicIsNowClosed": "本话题已关闭。", - "blog": "博客", - "notes": "笔记", - "typePrivateNotesHere": "在此輸入私人筆記", + "by": "作者:{param}", + "importedByX": "由{param}導入", + "thisTopicIsNowClosed": "此話題已關閉", + "blog": "部落格", + "notes": "備註", + "typePrivateNotesHere": "在此輸入私人備註", "writeAPrivateNoteAboutThisUser": "備註用戶資訊", - "noNoteYet": "尚無筆記", - "invalidUsernameOrPassword": "用户名或密碼錯誤", + "noNoteYet": "尚無備註", + "invalidUsernameOrPassword": "使用者名稱或密碼錯誤", "incorrectPassword": "舊密碼錯誤", "invalidAuthenticationCode": "驗證碼無效", "emailMeALink": "通過電郵發送連結給我", @@ -749,62 +917,63 @@ "clockIncrement": "加秒", "privacy": "隱私", "privacyPolicy": "隱私條款", - "letOtherPlayersFollowYou": "允许其他玩家关注", - "letOtherPlayersChallengeYou": "允许其他玩家挑战", + "letOtherPlayersFollowYou": "允許其他玩家關注", + "letOtherPlayersChallengeYou": "允許其他玩家發起挑戰", "letOtherPlayersInviteYouToStudy": "允許其他棋手邀請你參加研討", - "sound": "聲音", + "sound": "音效", "none": "無", "fast": "快", "normal": "普通", "slow": "慢", "insideTheBoard": "棋盤內", "outsideTheBoard": "棋盤外", + "allSquaresOfTheBoard": "包括所有棋盤內的格子", "onSlowGames": "慢棋時", "always": "總是", "never": "永不", - "xCompetesInY": "{param1}参加{param2}", - "victory": "成功!", + "xCompetesInY": "{param1}在{param2}參加", + "victory": "勝利", "defeat": "戰敗", "victoryVsYInZ": "{param1}在{param3}模式下贏了{param2}", "defeatVsYInZ": "{param1}在{param3}模式下輸給了{param2}", - "drawVsYInZ": "{param1}在{param3}模式下和{param2}平手", - "timeline": "时间线", - "starting": "开始时间:", - "allInformationIsPublicAndOptional": "所有資料是公開的,同時是可選的。", - "biographyDescription": "給我們一個您的自我介紹,像是您的興趣、您喜愛的選手等", - "listBlockedPlayers": "显示黑名单用户列表", - "human": "人类", + "drawVsYInZ": "{param1}在{param3}模式下和{param2}和棋", + "timeline": "時間軸", + "starting": "起始時間:", + "allInformationIsPublicAndOptional": "所有資料為公開並且可被隱藏。", + "biographyDescription": "給一個自我介紹,例如興趣或您喜愛的選手等", + "listBlockedPlayers": "顯示黑名單", + "human": "人類", "computer": "電腦", "side": "方", - "clock": "鐘", - "opponent": "对手", - "learnMenu": "學棋", + "clock": "棋鐘", + "opponent": "對手", + "learnMenu": "學習", "studyMenu": "研究", "practice": "練習", - "community": "社區", + "community": "社群", "tools": "工具", "increment": "加秒", "error_unknown": "無效值", - "error_required": "本项必填", - "error_email": "這個電子郵件地址無效", - "error_email_acceptable": "該電子郵件地址是不可用。請重新檢查後重試。", + "error_required": "本項必填", + "error_email": "無效電子郵件", + "error_email_acceptable": "該電子郵件地址無效。請重新檢查後重試。", "error_email_unique": "電子郵件地址無效或已被使用", "error_email_different": "這已經是您的電子郵件地址", - "error_minLength": "至少應有 {param} 個字元長", - "error_maxLength": "最多不能超過 {param} 個字元長", - "error_min": "最少 {param} 個字符", - "error_max": "最大不能超過 {param}", - "ifRatingIsPlusMinusX": "允许评级范围±{param}", - "ifRegistered": "如已註冊", + "error_minLength": "至少包含 {param} 個字元", + "error_maxLength": "最多包含 {param} 個字元", + "error_min": "最少包含 {param} 個字符", + "error_max": "最多不能超過 {param}", + "ifRatingIsPlusMinusX": "允許評級範圍±{param}", + "ifRegistered": "已登入者", "onlyExistingConversations": "僅目前對話", "onlyFriends": "只允許好友", - "menu": "菜单", - "castling": "王车易位", + "menu": "選單", + "castling": "王車易位", "whiteCastlingKingside": "白方短易位", "blackCastlingKingside": "黑方短易位", "tpTimeSpentPlaying": "花在下棋上的時間:{param}", "watchGames": "觀看對局直播", - "tpTimeSpentOnTV": "花在Lichess TV觀看直播的時間:{param}", + "tpTimeSpentOnTV": "花在Lichess TV的時間:{param}", "watch": "觀看", "videoLibrary": "影片庫", "streamersMenu": "實況主", @@ -812,112 +981,113 @@ "webmasters": "網站管理員", "about": "關於", "aboutX": "關於 {param}", - "xIsAFreeYLibreOpenSourceChessServer": "{param1}是一個免費的({param2}),開放性的,無廣告,開放資源的網站", + "xIsAFreeYLibreOpenSourceChessServer": "{param1}是一個完全免費({param2})、開放性、無廣告、並且開源的網站", "really": "真的", "contribute": "協助", "termsOfService": "服務條款", "sourceCode": "原始碼", "simultaneousExhibitions": "車輪戰", "host": "主持", - "hostColorX": "主持者使用旗子顏色:{param}", - "yourPendingSimuls": "你待處理的車輪戰", - "createdSimuls": "最近开始的同步赛", - "hostANewSimul": "主持新同步赛", + "hostColorX": "主持人所使用旗子顏色:{param}", + "yourPendingSimuls": "正在載入比賽", + "createdSimuls": "觀看最近開始的車輪戰", + "hostANewSimul": "主持車輪戰", "signUpToHostOrJoinASimul": "註冊以舉辦或參與車輪戰", - "noSimulFound": "找不到该同步赛", + "noSimulFound": "找不到該車輪戰", "noSimulExplanation": "此車輪戰不存在。", - "returnToSimulHomepage": "返回表演赛主页", + "returnToSimulHomepage": "返回車輪戰首頁", "aboutSimul": "車輪戰涉及到一個人同時和幾位棋手下棋。", - "aboutSimulImage": "在50个对手中,菲舍尔赢了47局,和了2局,输了1局。", - "aboutSimulRealLife": "这个概念来自真实的国际赛事。 在现实中,这涉及到主持在桌与桌之间来回穿梭走棋。", - "aboutSimulRules": "当表演赛开始的时候, 每个玩家都与主持开始对弈, 而主持用白方。 当所有的对局都结束时,表演赛就结束了。", - "aboutSimulSettings": "表演赛总是不定级的。 复赛、悔棋和\"加时\"功能将被禁用。", - "create": "创建", + "aboutSimulImage": "在50位對手中,費雪贏了47局、和了2局、並輸了1局。", + "aboutSimulRealLife": "這種賽制來自於真實的國際賽事。 在現實中,這涉及到主持人在棋局與棋局之間來回走棋。", + "aboutSimulRules": "當車輪賽開始時,每個玩家都會與主持人對奕,主持人持白。當所有對局結束表示車輪賽也一併結束。", + "aboutSimulSettings": "車輪賽事較為非正式的賽制。重賽、悔棋、以及加時功能皆會被禁用。", + "create": "建立", "whenCreateSimul": "當您創建車輪戰時,您要同時跟幾個棋手一起下棋。", - "simulVariantsHint": "如果您选择几个变体,每个玩家都要选择下哪一种。", - "simulClockHint": "菲舍爾時鐘設定。棋手越多,您需要的時間可能就越多。", + "simulVariantsHint": "如果您選擇多個變體,每個玩家可以選擇自己所好的變體。", + "simulClockHint": "費雪棋鐘設定。棋手越多,您所需的時間可能就越多。", "simulAddExtraTime": "您可以給您的時鍾多加點時間以幫助您應對車輪戰。", - "simulHostExtraTime": "主持人的额外时间", + "simulHostExtraTime": "主持人的額外時間", "simulAddExtraTimePerPlayer": "每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。", - "simulHostExtraTimePerPlayer": "每個玩家加入后棋鐘增加的額外時間", - "lichessTournaments": "Lichess比赛", - "tournamentFAQ": "比赛常见问题", - "timeBeforeTournamentStarts": "比赛准备时间", - "averageCentipawnLoss": "平均厘兵损失", + "simulHostExtraTimePerPlayer": "於每位玩家加入後棋鐘增加的額外時間", + "lichessTournaments": "Lichess 錦標賽", + "tournamentFAQ": "競技場錦標賽常見問題", + "timeBeforeTournamentStarts": "錦標賽準備時間", + "averageCentipawnLoss": "平均厘兵損失", "accuracy": "精準度", - "keyboardShortcuts": "快捷键", - "keyMoveBackwardOrForward": "后退/前进", - "keyGoToStartOrEnd": "跳到开始/结束", + "keyboardShortcuts": "快捷鍵", + "keyMoveBackwardOrForward": "後退/前進", + "keyGoToStartOrEnd": "跳轉到開始/結束", "keyCycleSelectedVariation": "循環已選取的變體", - "keyShowOrHideComments": "显示/隐藏评论", - "keyEnterOrExitVariation": "进入/退出变体", + "keyShowOrHideComments": "顯示/隱藏評論", + "keyEnterOrExitVariation": "進入/退出變體", "keyRequestComputerAnalysis": "請求引擎分析,從你的失誤中學習", "keyNextLearnFromYourMistakes": "下一個 (從你的失誤中學習)", "keyNextBlunder": "下一個漏著", - "keyNextMistake": "下一個錯著", - "keyNextInaccuracy": "下一個疑著", + "keyNextMistake": "下一個錯誤", + "keyNextInaccuracy": "下一個輕微失誤", "keyPreviousBranch": "上一個分支", "keyNextBranch": "下一個分支", - "toggleVariationArrows": "切換變體箭頭", + "toggleVariationArrows": "顯示變體箭頭", "cyclePreviousOrNextVariation": "循環上一個/下一個變體", - "toggleGlyphAnnotations": "切換圖形標註", + "toggleGlyphAnnotations": "顯示圖形標註", + "togglePositionAnnotations": "顯示位置標註", "variationArrowsInfo": "變體箭頭讓你不需棋步列表導航", - "playSelectedMove": "走已選的棋步", - "newTournament": "新比赛", - "tournamentHomeTitle": "国际象棋赛事均设有不同的时间控制和变体", + "playSelectedMove": "走已選取的棋步", + "newTournament": "新比賽", + "tournamentHomeTitle": "富有各種時間以及變體的西洋棋錦標賽", "tournamentHomeDescription": "加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。", - "tournamentNotFound": "找不到该比赛", - "tournamentDoesNotExist": "这个比赛不存在。", - "tournamentMayHaveBeenCanceled": "它可能已被取消,假如所有的对手在比赛开始之前离开。", - "returnToTournamentsHomepage": "返回比赛主页", - "weeklyPerfTypeRatingDistribution": "本月{param}的分数分布", - "yourPerfTypeRatingIsRating": "您的{param1}分数是{param2}分。", - "youAreBetterThanPercentOfPerfTypePlayers": "您比{param1}的{param2}棋手更强。", + "tournamentNotFound": "找不到該錦標賽", + "tournamentDoesNotExist": "這個錦標賽不存在。", + "tournamentMayHaveBeenCanceled": "錦標賽可能因為沒有其他玩家而取消。", + "returnToTournamentsHomepage": "返回錦標賽首頁", + "weeklyPerfTypeRatingDistribution": "本月{param}的分數分布", + "yourPerfTypeRatingIsRating": "您的{param1}目前{param2}分。", + "youAreBetterThanPercentOfPerfTypePlayers": "您比{param1}的{param2}棋手更強。", "userIsBetterThanPercentOfPerfTypePlayers": "{param1}比{param3}之中的{param2}棋手強。", "betterThanPercentPlayers": "您比{param1}的{param2}棋手更強。", - "youDoNotHaveAnEstablishedPerfTypeRating": "您没有准确的{param}评级。", + "youDoNotHaveAnEstablishedPerfTypeRating": "您沒有準確的{param}評級。", "yourRating": "您的評分", "cumulative": "平均累積", "glicko2Rating": "Glicko-2 積分", "checkYourEmail": "請檢查您的電子郵件", - "weHaveSentYouAnEmailClickTheLink": "我們已經發送了一封電子郵件到你的郵箱. 點擊郵件中的連結以激活您的賬號.", + "weHaveSentYouAnEmailClickTheLink": "我們已經發送了一封電子郵件到你的郵箱。點擊郵件中的連結以啟用帳號。", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。", "weHaveSentYouAnEmailTo": "我們發送了一封郵件到 {param}。點擊郵件中的連結來重置您的密碼。", - "byRegisteringYouAgreeToBeBoundByOur": "您一登记,我们就假设您同意尊重我们的使用规则({param})。", + "byRegisteringYouAgreeToBeBoundByOur": "註冊帳號表示同意並且遵守 {param}", "readAboutOur": "閱讀我們的{param}", - "networkLagBetweenYouAndLichess": "您和 lichess 之間的網絡時滯", + "networkLagBetweenYouAndLichess": "您和 Lichess 之間的網路停滯", "timeToProcessAMoveOnLichessServer": "lichess 伺服器上處理走棋的時間", - "downloadAnnotated": "下载带笔记的记录", - "downloadRaw": "下载无笔记的记录", - "downloadImported": "下载已导入棋局", - "crosstable": "历史表", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "您也可以用滚动键在棋盘游戏中移动。", - "scrollOverComputerVariationsToPreviewThem": "將鼠標移到電腦分析變招上進行預覽", - "analysisShapesHowTo": "按shift点击或右键棋盘上绘制圆圈和箭头。", + "downloadAnnotated": "下載含有棋子走動方向的棋局", + "downloadRaw": "下載純文字", + "downloadImported": "下載導入的棋局", + "crosstable": "歷程表", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "您也可以捲動棋盤以移動。", + "scrollOverComputerVariationsToPreviewThem": "將鼠標移到電腦分析變種上進行預覽", + "analysisShapesHowTo": "按 shift 點及或右鍵棋盤上以繪製圓圈與箭頭。", "letOtherPlayersMessageYou": "允許其他人發送私訊給您", "receiveForumNotifications": "在論壇中被提及時接收通知", - "shareYourInsightsData": "分享您的慧眼数据", - "withNobody": "不分享", - "withFriends": "與好友分享", - "withEverybody": "與所有人分享", + "shareYourInsightsData": "顯示您的洞察數據", + "withNobody": "不顯示", + "withFriends": "好友", + "withEverybody": "所有人", "kidMode": "兒童模式", - "kidModeIsEnabled": "已啓用兒童模式", + "kidModeIsEnabled": "已啟用兒童模式", "kidModeExplanation": "考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。", "inKidModeTheLichessLogoGetsIconX": "在兒童模式下,Lichess的標誌會有一個{param}圖示,讓你知道你的孩子是安全的。", "askYourChessTeacherAboutLiftingKidMode": "你的帳戶被管理,詢問你的老師解除兒童模式。", "enableKidMode": "啟用兒童模式", "disableKidMode": "停用兒童模式", "security": "資訊安全相關設定", - "sessions": "會話", + "sessions": "裝置", "revokeAllSessions": "登出所有裝置", - "playChessEverywhere": "随处下棋!", - "asFreeAsLichess": "完全又永遠的免費。", - "builtForTheLoveOfChessNotMoney": "不是為了錢,是為了國際象棋所創建。", + "playChessEverywhere": "隨處下棋!", + "asFreeAsLichess": "完全、永遠免費。", + "builtForTheLoveOfChessNotMoney": "不是為了錢,是為了西洋棋所創建。", "everybodyGetsAllFeaturesForFree": "每個人都能免費使用所有功能", "zeroAdvertisement": "沒有廣告", "fullFeatured": "功能全面", "phoneAndTablet": "手機和平板電腦", - "bulletBlitzClassical": "子彈,閃電,經典", + "bulletBlitzClassical": "快或慢都隨你!", "correspondenceChess": "通訊賽", "onlineAndOfflinePlay": "線上或離線下棋", "viewTheSolution": "看解答", @@ -936,23 +1106,29 @@ "dark": "暗", "transparent": "透明度", "deviceTheme": "設備主題", - "backgroundImageUrl": "背景圖片網址:", + "backgroundImageUrl": "背景圖片網址:", + "board": "棋盤外觀", + "size": "大小", + "opacity": "透明度", + "brightness": "亮度", + "hue": "色調", + "boardReset": "回復預設顏色設定", "pieceSet": "棋子外觀設定", "embedInYourWebsite": "嵌入您的網站", - "usernameAlreadyUsed": "此用戶名已經有人在使用,請嘗試使用別的", + "usernameAlreadyUsed": "該使用者名稱已被使用,請換一個試試!", "usernamePrefixInvalid": "使用者名稱必須以字母開頭", "usernameSuffixInvalid": "使用者名稱的結尾必須為字母或數字", "usernameCharsInvalid": "使用者名稱只能包含字母、 數字、 底線和短劃線。", - "usernameUnacceptable": "此使用者名稱不可用", + "usernameUnacceptable": "無法套用此使用者名稱", "playChessInStyle": "下棋也要穿得好看", - "chessBasics": "基本知識", + "chessBasics": "基本常識", "coaches": "教練", - "invalidPgn": "無效的PGN", - "invalidFen": "無效的FEN", + "invalidPgn": "無效的 PGN", + "invalidFen": "無效的 FEN", "custom": "自訂設定", "notifications": "通知", - "notificationsX": "通知: {param1}", - "perfRatingX": "評分:{param}", + "notificationsX": "通知:{param1}", + "perfRatingX": "評分:{param}", "practiceWithComputer": "和電腦練習", "anotherWasX": "另一個是{param}", "bestWasX": "最好的一步是{param}", @@ -974,8 +1150,8 @@ "xWasPlayed": "走了{param}", "findBetterMoveForWhite": "找出白方的最佳著法", "findBetterMoveForBlack": "找出黑方的最佳著法", - "resumeLearning": "回復學習", - "youCanDoBetter": "您還可以做得更好", + "resumeLearning": "繼續學習", + "youCanDoBetter": "還有更好的移動", "tryAnotherMoveForWhite": "嘗試白方更好其他的著法", "tryAnotherMoveForBlack": "嘗試黑方更好其他的著法", "solution": "解決方案", @@ -984,7 +1160,7 @@ "noMistakesFoundForBlack": "沒有找到黑方的失誤", "doneReviewingWhiteMistakes": "已完成觀看白方的失誤", "doneReviewingBlackMistakes": "已完成觀看黑方的失誤", - "doItAgain": "重作一次", + "doItAgain": "再試一次", "reviewWhiteMistakes": "複習白方失誤", "reviewBlackMistakes": "複習黑方失誤", "advantage": "優勢", @@ -992,25 +1168,25 @@ "middlegame": "中場", "endgame": "殘局", "conditionalPremoves": "預設棋譜", - "addCurrentVariation": "加入現有變化", - "playVariationToCreateConditionalPremoves": "著一步不同的位置以創建預估走位", + "addCurrentVariation": "加入現有變種", + "playVariationToCreateConditionalPremoves": "走一種變種以建立棋譜", "noConditionalPremoves": "無預設棋譜", "playX": "移動至{param}", - "showUnreadLichessMessage": "你收到一個來自 Lichess 的私人信息。", - "clickHereToReadIt": "點擊閱讀", - "sorry": "抱歉:(", - "weHadToTimeYouOutForAWhile": "您被封鎖了,在一陣子的時間內將不能下棋", + "showUnreadLichessMessage": "你收到一個來自 Lichess 的私訊。", + "clickHereToReadIt": "點擊以閱讀", + "sorry": "抱歉:(", + "weHadToTimeYouOutForAWhile": "我們必須將您暫時封鎖", "why": "為什麼?", - "pleasantChessExperience": "我們的目的在於為所有人提供愉快的國際象棋體驗", + "pleasantChessExperience": "我們的目的在於維持良好的下棋環境", "goodPractice": "為此,我們必須確保所有參與者都遵循良好做法", "potentialProblem": "當檢測到不良行為時,我們將顯示此消息", "howToAvoidThis": "如何避免這件事發生?", - "playEveryGame": "下好每一盤您加入的棋", + "playEveryGame": "避免在棋局中任意退出", "tryToWin": "試著在每個棋局裡獲勝(或至少平手)", - "resignLostGames": "棄權(不要讓時間耗盡)", - "temporaryInconvenience": "對於給您帶來的不便,我們深表歉意", - "wishYouGreatGames": "並祝您在lichess.org上玩得開心。", - "thankYouForReading": "感謝您的閱讀!", + "resignLostGames": "投降(不要讓時間耗盡)", + "temporaryInconvenience": "我們對於給您帶來的不便深表歉意", + "wishYouGreatGames": "並祝您在 lichess.org 上玩得開心。", + "thankYouForReading": "感謝您的閱讀!", "lifetimeScore": "帳戶總分", "currentMatchScore": "現時的對局分數", "agreementAssistance": "我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)", @@ -1019,22 +1195,23 @@ "agreementPolicy": "我同意我將會遵守Lichess的規則", "searchOrStartNewDiscussion": "尋找或開始聊天", "edit": "編輯", - "blitz": "快棋", + "bullet": "Bullet", + "blitz": "Blitz", "rapid": "快速模式", "classical": "經典", - "ultraBulletDesc": "瘋狂速度模式: 低於30秒", - "bulletDesc": "非常速度模式:低於3分鐘", - "blitzDesc": "快速模式:3到8分鐘", - "rapidDesc": "一般模式:8到25分鐘", - "classicalDesc": "經典模式:25分鐘以上", - "correspondenceDesc": "長期模式:一天或好幾天一步", + "ultraBulletDesc": "瘋狂速度模式:低於30秒", + "bulletDesc": "極快速模式:低於3分鐘", + "blitzDesc": "快速模式:3到8分鐘", + "rapidDesc": "一般模式:8到25分鐘", + "classicalDesc": "經典模式:25分鐘以上", + "correspondenceDesc": "通信模式:一天或好幾天一步", "puzzleDesc": "西洋棋戰術教練", "important": "重要", "yourQuestionMayHaveBeenAnswered": "您的問題可能已經有答案了{param1}", - "inTheFAQ": "在F.A.Q裡", - "toReportSomeoneForCheatingOrBadBehavior": "舉報一位作弊或者是不良行為的玩家,{param1}", - "useTheReportForm": "請造訪回報頁面", - "toRequestSupport": "需要請求協助,{param1}", + "inTheFAQ": "在常見問答內", + "toReportSomeoneForCheatingOrBadBehavior": "舉報一位作弊或違反善良風俗的玩家,{param1}", + "useTheReportForm": "請填寫回報表單", + "toRequestSupport": "{param1}以獲取協助", "tryTheContactPage": "請到協助頁面", "makeSureToRead": "確保你已閱讀 {param1}", "theForumEtiquette": "論壇禮儀", @@ -1044,33 +1221,33 @@ "youCannotPostYetPlaySomeGames": "您目前不能發表文章在論壇裡,先下幾盤棋吧!", "subscribe": "訂閱", "unsubscribe": "取消訂閱", - "mentionedYouInX": "在 \"{param1}\" 中提到了您。", - "xMentionedYouInY": "{param1} 在 \"{param2}\" 中提到了您。", - "invitedYouToX": "邀請您至\"{param1}\"。", - "xInvitedYouToY": "{param1} 邀請您至\"{param2}\"。", + "mentionedYouInX": "在「{param1}」中提到了您。", + "xMentionedYouInY": "{param1} 在「{param2}」中提到了您。", + "invitedYouToX": "邀請您至「{param1}」。", + "xInvitedYouToY": "{param1} 邀請您至「{param2}」。", "youAreNowPartOfTeam": "您現在是團隊的成員了。", - "youHaveJoinedTeamX": "您已加入 \"{param1}\"。", + "youHaveJoinedTeamX": "您已加入「{param1}」。", "someoneYouReportedWasBanned": "您檢舉的玩家已被封鎖帳號", "congratsYouWon": "恭喜,您贏了!", "gameVsX": "與{param1}對局", "resVsX": "{param1} vs {param2}", - "lostAgainstTOSViolator": "你輸給了違反了服務挑款的棋手", + "lostAgainstTOSViolator": "你輸給了違反了服務條款的棋手", "refundXpointsTimeControlY": "退回 {param1} {param2} 等級分。", "timeAlmostUp": "時間快到了!", - "clickToRevealEmailAddress": "[按下展示電郵位置]", + "clickToRevealEmailAddress": "[點擊以顯示電子郵件]", "download": "下載", "coachManager": "教練管理", "streamerManager": "直播管理", "cancelTournament": "取消錦標賽", "tournDescription": "錦標賽敘述", - "tournDescriptionHelp": "有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用Markdown網址 [name](https://url)。", + "tournDescriptionHelp": "有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用 Markdown 網址 [name](https://url)。", "ratedFormHelp": "比賽為積分賽\n會影響到棋手的積分", "onlyMembersOfTeam": "只限隊員", "noRestriction": "沒有限制", "minimumRatedGames": "評分局遊玩次數下限", "minimumRating": "評分下限", "maximumWeeklyRating": "每週最高評分", - "positionInputHelp": "將一個有效的 FEN 粘貼於此作為所有對局的起始位置。\n僅適用於標準國際象棋,對變體無效。\n你可以試用 {param} 來生成 FEN,然後將其粘貼到這裡。\n置空表示以標準位置開始比賽。", + "positionInputHelp": "將一個有效的 FEN 貼上於此作為所有對局的起始位置。\n僅適用於標準西洋棋,對變種無效。\n你可以試用 {param} 來生成 FEN,然後將其貼上到這裡。\n置空表示以預設位置開始比賽。", "cancelSimul": "取消車輪戰", "simulHostcolor": "主持所執方", "estimatedStart": "預計開始時間", @@ -1079,8 +1256,8 @@ "simulDescription": "車輪戰描述", "simulDescriptionHelp": "有甚麼要告訴參賽者的嗎?", "markdownAvailable": "{param} 可用於更高級的格式。", - "embedsAvailable": "粘貼對局URL或學習章節URL來嵌入。", - "inYourLocalTimezone": "在你的時區內", + "embedsAvailable": "貼上對局或學習章節網址來嵌入。", + "inYourLocalTimezone": "在您的時區內", "tournChat": "錦標賽聊天室", "noChat": "無聊天室", "onlyTeamLeaders": "僅限各隊隊長", @@ -1095,7 +1272,7 @@ "showHelpDialog": "顯示此說明欄", "reopenYourAccount": "重新開啟帳戶", "closedAccountChangedMind": "如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。", - "onlyWorksOnce": "這只能復原一次", + "onlyWorksOnce": "這只能復原一次。", "cantDoThisTwice": "如果你決定再次停用你的帳號,則不會有任何方式去復原。", "emailAssociatedToaccount": "和此帳號相關的電子信箱", "sentEmailWithLink": "我們已將網址寄送至你的信箱", @@ -1113,7 +1290,9 @@ "ourEventTips": "舉辦賽事的小建議", "instructions": "說明", "showMeEverything": "全部顯示", - "lichessPatronInfo": "Lichess是個慈善、完全免費之開源軟件。\n一切營運成本、開發和內容皆來自用戶之捐贈。", + "lichessPatronInfo": "Lichess是個慈善、完全免費且開源的軟體。\n一切營運成本、開發和內容皆來自用戶之捐贈。", + "nothingToSeeHere": "目前這裡沒有什麼好看的。", + "stats": "統計", "opponentLeftCounter": "{count, plural, other{您的對手已經離開了遊戲。您將在 {count} 秒後獲勝。}}", "mateInXHalfMoves": "{count, plural, other{在{count}步內將死對手}}", "nbBlunders": "{count, plural, other{{count} 次漏著}}", @@ -1148,14 +1327,14 @@ "nbFollowing": "{count, plural, other{關注{count}人}}", "lessThanNbMinutes": "{count, plural, other{小於{count}分鐘}}", "nbGamesInPlay": "{count, plural, other{{count}場對局正在進行中}}", - "maximumNbCharacters": "{count, plural, other{最多{count}个字符}}", - "blocks": "{count, plural, other{{count}位黑名单用户}}", + "maximumNbCharacters": "{count, plural, other{最多包含 {count} 個字符}}", + "blocks": "{count, plural, other{{count}位黑名單使用者}}", "nbForumPosts": "{count, plural, other{{count}個論壇貼文}}", "nbPerfTypePlayersThisWeek": "{count, plural, other{本周{count}位棋手下了{param2}模式的棋局}}", "availableInNbLanguages": "{count, plural, other{支援{count}種語言!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, other{在{count}秒前須下出第一步}}", "nbSeconds": "{count, plural, other{{count} 秒}}", - "andSaveNbPremoveLines": "{count, plural, other{以儲存{count}列預走的棋步}}", + "andSaveNbPremoveLines": "{count, plural, other{以省略{count}個預走的棋步}}", "stormMoveToStart": "移動以開始", "stormYouPlayTheWhitePiecesInAllPuzzles": "您將在所有謎題中執白", "stormYouPlayTheBlackPiecesInAllPuzzles": "您將在所有謎題中執黑", @@ -1164,9 +1343,9 @@ "stormNewWeeklyHighscore": "新的每周紀錄!", "stormNewMonthlyHighscore": "新的每月紀錄!", "stormNewAllTimeHighscore": "新歷史紀錄!", - "stormPreviousHighscoreWasX": "之前的紀錄:{param}", + "stormPreviousHighscoreWasX": "之前的最高紀錄:{param}", "stormPlayAgain": "再玩一次", - "stormHighscoreX": "最高紀錄:{param}", + "stormHighscoreX": "最高紀錄:{param}", "stormScore": "得分", "stormMoves": "走棋", "stormAccuracy": "精準度", @@ -1175,8 +1354,8 @@ "stormTimePerMove": "平均走棋時間", "stormHighestSolved": "最難解決的題目", "stormPuzzlesPlayed": "解決過的題目", - "stormNewRun": "新的一輪 (快捷鍵:空白鍵)", - "stormEndRun": "結束此輪 (快捷鍵:Enter鍵)", + "stormNewRun": "新的一輪 (快捷鍵:空白鍵)", + "stormEndRun": "結束此輪 (快捷鍵:Enter 鍵)", "stormHighscores": "最高紀錄", "stormViewBestRuns": "顯示最佳的一輪", "stormBestRunOfDay": "今日最佳的一輪", @@ -1198,17 +1377,189 @@ "stormSkip": "跳過", "stormSkipHelp": "每場賽可略一手棋", "stormSkipExplanation": "跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。", - "stormFailedPuzzles": "失敗了的謎題", - "stormSlowPuzzles": "慢 謎題", + "stormFailedPuzzles": "失敗的謎題", + "stormSlowPuzzles": "耗時謎題", + "stormSkippedPuzzle": "已跳過的謎題", "stormThisWeek": "本星期", "stormThisMonth": "本月", "stormAllTime": "總計", "stormClickToReload": "點擊以重新加載", - "stormThisRunHasExpired": "本次比賽已過期!", - "stormThisRunWasOpenedInAnotherTab": "本次沖刺已經在另一個標籤頁中打開!", + "stormThisRunHasExpired": "本輪已過期!", + "stormThisRunWasOpenedInAnotherTab": "本輪已經在另一個分頁中打開!", "stormXRuns": "{count, plural, other{{count}輪}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{玩了{count}輪的{param2}}}", - "streamerLichessStreamers": "Lichess實況主", + "streamerLichessStreamers": "Lichess 實況主", + "studyPrivate": "私人的", + "studyMyStudies": "我的研究", + "studyStudiesIContributeTo": "我有貢獻的研究", + "studyMyPublicStudies": "我的公開研究", + "studyMyPrivateStudies": "我的私人研究", + "studyMyFavoriteStudies": "我最愛的研究", + "studyWhatAreStudies": "研究是什麼?", + "studyAllStudies": "所有研究", + "studyStudiesCreatedByX": "{param}創建的研究", + "studyNoneYet": "暫時沒有...", + "studyHot": "熱門的", + "studyDateAddedNewest": "新增日期(由新到舊)", + "studyDateAddedOldest": "新增日期(由舊到新)", + "studyRecentlyUpdated": "最近更新", + "studyMostPopular": "最受歡迎", + "studyAlphabetical": "按字母順序", + "studyAddNewChapter": "加入新章節", + "studyAddMembers": "新增成員", + "studyInviteToTheStudy": "邀請加入研究", + "studyPleaseOnlyInvitePeopleYouKnow": "只邀請你所認識的人,以及願意積極投入的人來共同研究", + "studySearchByUsername": "透過使用者名稱搜尋", + "studySpectator": "觀眾", + "studyContributor": "共同研究者", + "studyKick": "踢出", + "studyLeaveTheStudy": "退出研究", + "studyYouAreNowAContributor": "你現在是一位研究者了", + "studyYouAreNowASpectator": "你現在是觀眾", + "studyPgnTags": "PGN 標籤", + "studyLike": "喜歡", + "studyUnlike": "取消喜歡", + "studyNewTag": "新標籤", + "studyCommentThisPosition": "對於目前局面的評論", + "studyCommentThisMove": "對於此棋步的評論", + "studyAnnotateWithGlyphs": "以圖形標註", + "studyTheChapterIsTooShortToBeAnalysed": "因為太短,所以此章節無法被分析", + "studyOnlyContributorsCanRequestAnalysis": "只有研究專案編輯者才能要求電腦分析", + "studyGetAFullComputerAnalysis": "請求伺服器完整的分析主要走法", + "studyMakeSureTheChapterIsComplete": "確認此章節已完成,您只能要求分析一次", + "studyAllSyncMembersRemainOnTheSamePosition": "所有的SYNC成員處於相同局面", + "studyShareChanges": "向旁觀者分享這些變動並將其保留在伺服器中", + "studyPlaying": "下棋中", + "studyShowEvalBar": "評估條", + "studyFirst": "第一頁", + "studyPrevious": "上一頁", + "studyNext": "下一頁", + "studyLast": "最後一頁", "studyShareAndExport": "分享 & 導出", - "studyStart": "開始" + "studyCloneStudy": "複製", + "studyStudyPgn": "研究 PGN", + "studyDownloadAllGames": "下載所有棋局", + "studyChapterPgn": "章節PGN", + "studyCopyChapterPgn": "複製PGN", + "studyDownloadGame": "下載棋局", + "studyStudyUrl": "研究連結", + "studyCurrentChapterUrl": "目前章節連結", + "studyYouCanPasteThisInTheForumToEmbed": "您可以將此複製到論壇以嵌入", + "studyStartAtInitialPosition": "從起始局面開始", + "studyStartAtX": "從{param}開始", + "studyEmbedInYourWebsite": "嵌入到您的網站或部落格", + "studyReadMoreAboutEmbedding": "閱讀更多與嵌入有關的內容", + "studyOnlyPublicStudiesCanBeEmbedded": "只有公開的研究可以嵌入!", + "studyOpen": "打開", + "studyXBroughtToYouByY": "{param1},由{param2}提供", + "studyStudyNotFound": "找無此研究", + "studyEditChapter": "編輯章節", + "studyNewChapter": "建立新章節", + "studyImportFromChapterX": "從 {param} 導入", + "studyOrientation": "視角", + "studyAnalysisMode": "分析模式", + "studyPinnedChapterComment": "置頂留言", + "studySaveChapter": "儲存章節", + "studyClearAnnotations": "清除註記", + "studyClearVariations": "清除變化", + "studyDeleteChapter": "刪除章節", + "studyDeleteThisChapter": "刪除此章節? 此動作將無法取消!", + "studyClearAllCommentsInThisChapter": "清除此章節中的所有註釋和圖形嗎?", + "studyRightUnderTheBoard": "棋盤下方", + "studyNoPinnedComment": "無", + "studyNormalAnalysis": "一般分析", + "studyHideNextMoves": "隱藏下一步", + "studyInteractiveLesson": "互動課程", + "studyChapterX": "章節{param}", + "studyEmpty": "空的", + "studyStartFromInitialPosition": "從起始局面開始", + "studyEditor": "編輯器", + "studyStartFromCustomPosition": "從自定的局面開始", + "studyLoadAGameByUrl": "以連結導入棋局", + "studyLoadAPositionFromFen": "透過FEN讀取局面", + "studyLoadAGameFromPgn": "以PGN文件導入棋局", + "studyAutomatic": "自動", + "studyUrlOfTheGame": "棋局連結,一行一個", + "studyLoadAGameFromXOrY": "從{param1}或{param2}載入棋局", + "studyCreateChapter": "建立章節", + "studyCreateStudy": "建立研究", + "studyEditStudy": "編輯此研究", + "studyVisibility": "權限", + "studyPublic": "公開的", + "studyUnlisted": "不公開", + "studyInviteOnly": "僅限邀請", + "studyAllowCloning": "可以複製", + "studyNobody": "没有人", + "studyOnlyMe": "僅自己", + "studyContributors": "貢獻者", + "studyMembers": "成員", + "studyEveryone": "所有人", + "studyEnableSync": "允許同步", + "studyYesKeepEveryoneOnTheSamePosition": "同步:讓所有人停留在同一個局面", + "studyNoLetPeopleBrowseFreely": "不同步:允許所有人自由進行瀏覽", + "studyPinnedStudyComment": "置頂研究留言", + "studyStart": "開始", + "studySave": "存檔", + "studyClearChat": "清空對話紀錄", + "studyDeleteTheStudyChatHistory": "確定要清空課程對話紀錄嗎?此操作無法還原!", + "studyDeleteStudy": "刪除此研究", + "studyConfirmDeleteStudy": "你確定要刪除整個研究?此動作無法反悔。輸入研究名稱確認:{param}", + "studyWhereDoYouWantToStudyThat": "要從哪裡開始研究呢?", + "studyGoodMove": "好棋", + "studyMistake": "失誤", + "studyBrilliantMove": "妙着", + "studyBlunder": "嚴重失誤", + "studyInterestingMove": "有趣的一着", + "studyDubiousMove": "值得商榷的一着", + "studyOnlyMove": "唯一著法", + "studyZugzwang": "等著", + "studyEqualPosition": "勢均力敵", + "studyUnclearPosition": "局勢不明", + "studyWhiteIsSlightlyBetter": "白方稍占優勢", + "studyBlackIsSlightlyBetter": "黑方稍占優勢", + "studyWhiteIsBetter": "白方占優勢", + "studyBlackIsBetter": "黑方占優勢", + "studyWhiteIsWinning": "白方要取得勝利了", + "studyBlackIsWinning": "黑方要取得勝利了", + "studyNovelty": "新奇的", + "studyDevelopment": "發展", + "studyInitiative": "佔據主動", + "studyAttack": "攻擊", + "studyCounterplay": "反擊", + "studyTimeTrouble": "時間壓力", + "studyWithCompensation": "優勢補償", + "studyWithTheIdea": "教科書式的", + "studyNextChapter": "下一章", + "studyPrevChapter": "上一章", + "studyStudyActions": "研討操作", + "studyTopics": "主題", + "studyMyTopics": "我的主題", + "studyPopularTopics": "熱門主題", + "studyManageTopics": "管理主題", + "studyBack": "返回", + "studyPlayAgain": "再玩一次", + "studyWhatWouldYouPlay": "你會在這個位置上怎麼走?", + "studyYouCompletedThisLesson": "恭喜!您完成了這個課程。", + "studyNbChapters": "{count, plural, other{第{count}章}}", + "studyNbGames": "{count, plural, other{{count}對局}}", + "studyNbMembers": "{count, plural, other{{count}位成員}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此貼上PGN文本,最多可導入{count}個棋局}}", + "timeagoJustNow": "剛剛", + "timeagoRightNow": "現在", + "timeagoCompleted": "已結束", + "timeagoInNbSeconds": "{count, plural, other{{count}秒後}}", + "timeagoInNbMinutes": "{count, plural, other{{count}分後}}", + "timeagoInNbHours": "{count, plural, other{{count}小時後}}", + "timeagoInNbDays": "{count, plural, other{{count}天後}}", + "timeagoInNbWeeks": "{count, plural, other{{count}週後}}", + "timeagoInNbMonths": "{count, plural, other{{count}個月後}}", + "timeagoInNbYears": "{count, plural, other{{count}年後}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count}分前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count}小時前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count}天前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count}週前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count}個月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count}年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{剩下 {count} 分鐘}}", + "timeagoNbHoursRemaining": "{count, plural, other{剩下 {count} 小時}}" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 982f02c275..0ec26f1d80 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,156 +1,70 @@ -import 'dart:convert'; - -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; -import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:path/path.dart' as path; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; -import 'firebase_options.dart'; import 'src/app.dart'; -import 'src/utils/color_palette.dart'; Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + final lichessBinding = AppLichessBinding.ensureInitialized(); - // logging setup - setupLogger(); - - // Intl and timeago setup - await setupIntl(); + // Show splash screen until app is ready + // See src/app.dart for splash screen removal + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + // Old API. + // TODO: Remove this once all SharedPreferences usage is migrated to SharedPreferencesAsync. SharedPreferences.setPrefix('lichess.'); + await migrateSharedPreferences(); - // Firebase setup - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await lichessBinding.preloadSharedPreferences(); - // Crashlytics - if (kReleaseMode) { - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; - // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); - return true; - }; + if (defaultTargetPlatform == TargetPlatform.android) { + await androidDisplayInitialization(widgetsBinding); } - // Get android 12+ core palette - try { - await DynamicColorPlugin.getCorePalette().then((value) { - setCorePalette(value); - }); - } catch (e) { - debugPrint('Could not get core palette: $e'); - } + await preloadPieceImages(); - // Show splash screen until app is ready - // See src/app.dart for splash screen removal - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - - if (defaultTargetPlatform == TargetPlatform.android) { - // lock orientation to portrait on android phones - final view = widgetsBinding.platformDispatcher.views.first; - final data = MediaQueryData.fromView(view); - if (data.size.shortestSide < FormFactor.tablet) { - await SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp], - ); - } + await setupFirstLaunch(); - // Sets edge-to-edge system UI mode on Android 12+ - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - systemNavigationBarDividerColor: Colors.transparent, - systemNavigationBarContrastEnforced: true, - ), - ); - } + await SoundService.initialize(); - runApp( - ProviderScope( - observers: [ - ProviderLogger(), - ], - child: const AppInitializationScreen(), - ), - ); -} + final locale = await setupIntl(widgetsBinding); -@pragma('vm:entry-point') -Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { - debugPrint('Handling a background message: ${message.data}'); + await initializeLocalNotifications(locale); - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; + await lichessBinding.initializeFirebase(); - // update correspondence game - if (gameFullId != null && round != null) { - final dbPath = path.join(await getDatabasesPath(), kLichessDatabaseName); - final db = await openDb(databaseFactory, dbPath); - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - final corresGame = OfflineCorrespondenceGame( - id: game.id, - fullId: fullId, - meta: game.meta, - rated: game.meta.rated, - steps: game.steps, - initialFen: game.initialFen, - status: game.status, - variant: game.meta.variant, - speed: game.meta.speed, - perf: game.meta.perf, - white: game.white, - black: game.black, - youAre: game.youAre!, - daysPerTurn: game.meta.daysPerTurn, - clock: game.correspondenceClock, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - ); + runApp(ProviderScope(observers: [ProviderLogger()], child: const AppInitializationScreen())); +} - await db.insert( - kCorrespondenceStorageTable, - { - 'userId': - corresGame.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, - 'gameId': corresGame.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(corresGame.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); +Future migrateSharedPreferences() async { + final prefs = await SharedPreferences.getInstance(); + final didMigrate = prefs.getBool('shared_preferences_did_migrate') ?? false; + if (didMigrate) { + return; } - - // update badge - final badge = message.data['lichess.iosBadge'] as String?; - if (badge != null) { - try { - BadgeService.instance.setBadge(int.parse(badge)); - } catch (e) { - debugPrint('Could not parse badge: $badge'); + final newPrefs = SharedPreferencesAsync(); + for (final key in prefs.getKeys()) { + final value = prefs.get(key); + if (value is String) { + await newPrefs.setString(key, value); + } else if (value is int) { + await newPrefs.setInt(key, value); + } else if (value is double) { + await newPrefs.setDouble(key, value); + } else if (value is bool) { + await newPrefs.setBool(key, value); + } else if (value is List) { + await newPrefs.setStringList(key, value); } } + await prefs.setBool('shared_preferences_did_migrate', true); } diff --git a/lib/src/app.dart b/lib/src/app.dart index 1b640f5065..663db27fb4 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,31 +1,24 @@ -import 'dart:async'; - import 'package:dynamic_color/dynamic_color.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/main.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; /// Application initialization and main entry point. class AppInitializationScreen extends ConsumerWidget { @@ -33,54 +26,21 @@ class AppInitializationScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen>( - appInitializationProvider, - (_, state) { - if (state.hasValue || state.hasError) { - FlutterNativeSplash.remove(); - } - }, - ); + ref.listen>(preloadedDataProvider, (_, state) { + if (state.hasValue || state.hasError) { + FlutterNativeSplash.remove(); + } + }); - return ref.watch(appInitializationProvider).when( + return ref + .watch(preloadedDataProvider) + .when( data: (_) => const Application(), // loading screen is handled by the native splash screen loading: () => const SizedBox.shrink(), error: (err, st) { - debugPrint( - 'SEVERE: [App] could not initialize app; $err\n$st', - ); - // We should really do everything we can to avoid this screen - // but in last resort, let's show an error message and invite the - // user to clear app data. - // TODO implement it on iOS - return Theme.of(context).platform == TargetPlatform.android - ? MaterialApp( - home: Scaffold( - body: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: Text( - "Something went wrong :'(\n\nIf the problem persists, you can try to clear the storage and restart the application.\n\nSorry for the inconvenience.", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18.0), - ), - ), - const SizedBox(height: 16.0), - ElevatedButton( - onPressed: () { - System.instance.clearUserData(); - }, - child: const Text('Clear storage'), - ), - ], - ), - ), - ) - : const SizedBox.shrink(); + debugPrint('SEVERE: [App] could not initialize app; $err\n$st'); + return const SizedBox.shrink(); }, ); } @@ -98,30 +58,42 @@ class Application extends ConsumerStatefulWidget { } class _AppState extends ConsumerState { - bool _correspondenceSynced = false; + /// Whether the app has checked for online status for the first time. + bool _firstTimeOnlineCheck = false; + + AppLifecycleListener? _appLifecycleListener; @override void initState() { - if (Theme.of(context).platform == TargetPlatform.android) { - setOptimalDisplayMode(); - } + _appLifecycleListener = AppLifecycleListener( + onResume: () async { + final online = await isOnline(ref.read(defaultClientProvider)); + if (online) { + ref.invalidate(ongoingGamesProvider); + } + }, + ); + + // Start services + ref.read(notificationServiceProvider).start(); + ref.read(challengeServiceProvider).start(); + // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { + final prevWasOffline = prev?.value?.isOnline == false; + final currentIsOnline = current.value?.isOnline == true; + // Play registered moves whenever the app comes back online. - if (prev?.hasValue == true && - !prev!.value!.isOnline && - !current.isRefreshing && - current.hasValue && - current.value!.isOnline) { - final nbMovesPlayed = - await ref.read(correspondenceServiceProvider).playRegisteredMoves(); + if (prevWasOffline && currentIsOnline) { + final nbMovesPlayed = await ref.read(correspondenceServiceProvider).playRegisteredMoves(); if (nbMovesPlayed > 0) { ref.invalidate(ongoingGamesProvider); } } - if (current.value?.isOnline == true && !_correspondenceSynced) { - _correspondenceSynced = true; + // Perform actions once when the app comes online. + if (current.value?.isOnline == true && !_firstTimeOnlineCheck) { + _firstTimeOnlineCheck = true; ref.read(correspondenceServiceProvider).syncGames(); } @@ -138,14 +110,18 @@ class _AppState extends ConsumerState { super.initState(); } + @override + void dispose() { + _appLifecycleListener?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); final brightness = ref.watch(currentBrightnessProvider); - final boardTheme = ref.watch( - boardPreferencesProvider.select((state) => state.boardTheme), - ); + final boardTheme = ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final remainingHeight = estimateRemainingHeightLeftBoard(context); @@ -153,51 +129,48 @@ class _AppState extends ConsumerState { builder: (lightColorScheme, darkColorScheme) { // TODO remove this workaround when the dynamic_color colorScheme bug is fixed // See: https://github.com/material-foundation/flutter-packages/issues/574 - final ( - fixedLightScheme, - fixedDarkScheme - ) = lightColorScheme != null && darkColorScheme != null - ? _generateDynamicColourSchemes(lightColorScheme, darkColorScheme) - : (null, null); + final (fixedLightScheme, fixedDarkScheme) = + lightColorScheme != null && darkColorScheme != null + ? _generateDynamicColourSchemes(lightColorScheme, darkColorScheme) + : (null, null); + + final isTablet = isTabletOrLarger(context); final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; - final colorScheme = - generalPrefs.systemColors && dynamicColorScheme != null - ? dynamicColorScheme - : ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - ); - - final cupertinoColorScheme = ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - dynamicSchemeVariant: DynamicSchemeVariant.fidelity, - ); + final ColorScheme colorScheme = switch (generalPrefs.appThemeSeed) { + AppThemeSeed.board => ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ), + AppThemeSeed.system => + dynamicColorScheme ?? + ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ), + }; final cupertinoThemeData = CupertinoThemeData( - primaryColor: cupertinoColorScheme.primary, - primaryContrastingColor: cupertinoColorScheme.onPrimary, + primaryColor: colorScheme.primary, + primaryContrastingColor: colorScheme.onPrimary, brightness: brightness, textTheme: CupertinoTheme.of(context).textTheme.copyWith( - primaryColor: cupertinoColorScheme.primary, - textStyle: CupertinoTheme.of(context) - .textTheme - .textStyle - .copyWith(color: Styles.cupertinoLabelColor), - navTitleTextStyle: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle - .copyWith(color: Styles.cupertinoTitleColor), - navLargeTitleTextStyle: CupertinoTheme.of(context) - .textTheme - .navLargeTitleTextStyle - .copyWith(color: Styles.cupertinoTitleColor), - ), + primaryColor: colorScheme.primary, + textStyle: CupertinoTheme.of( + context, + ).textTheme.textStyle.copyWith(color: Styles.cupertinoLabelColor), + navTitleTextStyle: CupertinoTheme.of( + context, + ).textTheme.navTitleTextStyle.copyWith(color: Styles.cupertinoTitleColor), + navLargeTitleTextStyle: CupertinoTheme.of( + context, + ).textTheme.navLargeTitleTextStyle.copyWith(color: Styles.cupertinoTitleColor), + ), scaffoldBackgroundColor: Styles.cupertinoScaffoldColor, - barBackgroundColor: Styles.cupertinoAppBarColor, + barBackgroundColor: + isTablet ? Styles.cupertinoTabletAppBarColor : Styles.cupertinoAppBarColor, ); return MaterialApp( @@ -207,196 +180,44 @@ class _AppState extends ConsumerState { locale: generalPrefs.locale, theme: ThemeData.from( colorScheme: colorScheme, - textTheme: Theme.of(context).platform == TargetPlatform.iOS - ? brightness == Brightness.light - ? Typography.blackCupertino - : Styles.whiteCupertinoTextTheme - : null, + textTheme: + Theme.of(context).platform == TargetPlatform.iOS + ? brightness == Brightness.light + ? Typography.blackCupertino + : Styles.whiteCupertinoTextTheme + : null, ).copyWith( cupertinoOverrideTheme: cupertinoThemeData, navigationBarTheme: NavigationBarTheme.of(context).copyWith( - height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 60 - : null, + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 60 : null, ), - extensions: [ - lichessCustomColors.harmonized( - Theme.of(context).platform == TargetPlatform.iOS - ? cupertinoColorScheme - : colorScheme, - ), - ], + extensions: [lichessCustomColors.harmonized(colorScheme)], ), - themeMode: generalPrefs.themeMode, - builder: Theme.of(context).platform == TargetPlatform.iOS - ? (context, child) { - return CupertinoTheme( - data: cupertinoThemeData, - child: Material(child: child), - ); - } - : null, - home: const _EntryPointWidget(), - navigatorObservers: [ - rootNavPageRouteObserver, - ], + themeMode: switch (generalPrefs.themeMode) { + BackgroundThemeMode.light => ThemeMode.light, + BackgroundThemeMode.dark => ThemeMode.dark, + BackgroundThemeMode.system => ThemeMode.system, + }, + builder: + Theme.of(context).platform == TargetPlatform.iOS + ? (context, child) { + return CupertinoTheme( + data: cupertinoThemeData, + child: IconTheme.merge( + data: IconThemeData( + color: CupertinoTheme.of(context).textTheme.textStyle.color, + ), + child: Material(child: child), + ), + ); + } + : null, + home: const BottomNavScaffold(), + navigatorObservers: [rootNavPageRouteObserver], ); }, ); } - - // Code taken from https://stackoverflow.com/questions/63631522/flutter-120fps-issue - /// Enables high refresh rate for devices where it was previously disabled - Future setOptimalDisplayMode() async { - final List supported = await FlutterDisplayMode.supported; - final DisplayMode active = await FlutterDisplayMode.active; - - final List sameResolution = supported - .where( - (DisplayMode m) => - m.width == active.width && m.height == active.height, - ) - .toList() - ..sort( - (DisplayMode a, DisplayMode b) => - b.refreshRate.compareTo(a.refreshRate), - ); - - final DisplayMode mostOptimalMode = - sameResolution.isNotEmpty ? sameResolution.first : active; - - // This setting is per session. - await FlutterDisplayMode.setPreferredMode(mostOptimalMode); - } -} - -/// The entry point widget for the application. -/// -/// This widget needs to be a desendant of [MaterialApp] to be able to handle -/// the [Navigator] properly. -/// -/// This widget is responsible for setting up the bottom navigation scaffold and -/// the main navigation routes. -/// -/// It also sets up the push notifications and handles incoming messages. -class _EntryPointWidget extends ConsumerStatefulWidget { - const _EntryPointWidget(); - - @override - ConsumerState<_EntryPointWidget> createState() => _EntryPointState(); -} - -class _EntryPointState extends ConsumerState<_EntryPointWidget> { - StreamSubscription? _fcmTokenRefreshSubscription; - ProviderSubscription>? - _connectivitySubscription; - - bool _pushNotificationsSetup = false; - - @override - Widget build(BuildContext context) { - return const BottomNavScaffold(); - } - - @override - void initState() { - super.initState(); - - _connectivitySubscription = - ref.listenManual(connectivityChangesProvider, (prev, current) async { - // setup push notifications once when the app comes online - if (current.value?.isOnline == true && !_pushNotificationsSetup) { - try { - await _setupPushNotifications(); - _pushNotificationsSetup = true; - } catch (e, st) { - debugPrint('Could not sync correspondence games; $e\n$st'); - } - } - }); - } - - @override - void dispose() { - _fcmTokenRefreshSubscription?.cancel(); - _connectivitySubscription?.close(); - super.dispose(); - } - - Future _setupPushNotifications() async { - // Listen for incoming messages while the app is in the foreground. - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - ref.read(notificationServiceProvider).processDataMessage(message); - }); - - // Listen for incoming messages while the app is in the background. - FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); - - // Request permission to receive notifications. Pop-up will appear only - // once. - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - announcement: false, - carPlay: false, - criticalAlert: false, - provisional: false, - ); - - // Listen for token refresh and update the token on the server accordingly. - _fcmTokenRefreshSubscription = - FirebaseMessaging.instance.onTokenRefresh.listen((String token) { - ref.read(notificationServiceProvider).registerToken(token); - }); - - // Register the device with the server. - await ref.read(notificationServiceProvider).registerDevice(); - - // Get any messages which caused the application to open from - // a terminated state. - final RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); - - if (initialMessage != null) { - _handleMessage(initialMessage); - } - - // Also handle any interaction when the app is in the background via a - // Stream listener - FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); - } - - /// Handle a message that caused the application to open - /// - /// This method must be part of a State object which is a child of [MaterialApp] - /// otherwise the [Navigator] will not be accessible. - void _handleMessage(RemoteMessage message) { - switch (message.data['lichess.type']) { - // correspondence game message types - case 'corresAlarm': - case 'gameTakebackOffer': - case 'gameDrawOffer': - case 'gameMove': - case 'gameFinish': - final gameFullId = message.data['lichess.fullId'] as String?; - if (gameFullId != null) { - // remove any existing routes before navigating to the game - // screen to avoid stacking multiple game screens - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); - } - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - initialGameId: GameFullId(gameFullId), - ), - ); - } - } - } } // -- @@ -414,28 +235,24 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { final lightAdditionalColours = _extractAdditionalColours(lightBase); final darkAdditionalColours = _extractAdditionalColours(darkBase); - final lightScheme = - _insertAdditionalColours(lightBase, lightAdditionalColours); + final lightScheme = _insertAdditionalColours(lightBase, lightAdditionalColours); final darkScheme = _insertAdditionalColours(darkBase, darkAdditionalColours); return (lightScheme.harmonized(), darkScheme.harmonized()); } List _extractAdditionalColours(ColorScheme scheme) => [ - scheme.surface, - scheme.surfaceDim, - scheme.surfaceBright, - scheme.surfaceContainerLowest, - scheme.surfaceContainerLow, - scheme.surfaceContainer, - scheme.surfaceContainerHigh, - scheme.surfaceContainerHighest, - ]; - -ColorScheme _insertAdditionalColours( - ColorScheme scheme, - List additionalColours, -) => + scheme.surface, + scheme.surfaceDim, + scheme.surfaceBright, + scheme.surfaceContainerLowest, + scheme.surfaceContainerLow, + scheme.surfaceContainer, + scheme.surfaceContainerHigh, + scheme.surfaceContainerHighest, +]; + +ColorScheme _insertAdditionalColours(ColorScheme scheme, List additionalColours) => scheme.copyWith( surface: additionalColours[0], surfaceDim: additionalColours[1], diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart deleted file mode 100644 index 61b600704a..0000000000 --- a/lib/src/app_initialization.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'dart:convert'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/db/secure_storage.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/utils/color_palette.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; - -part 'app_initialization.freezed.dart'; -part 'app_initialization.g.dart'; - -final _logger = Logger('AppInitialization'); - -@Riverpod(keepAlive: true) -Future appInitialization( - AppInitializationRef ref, -) async { - final secureStorage = ref.watch(secureStorageProvider); - final sessionStorage = ref.watch(sessionStorageProvider); - final pInfo = await PackageInfo.fromPlatform(); - final deviceInfo = await DeviceInfoPlugin().deviceInfo; - final prefs = await SharedPreferences.getInstance(); - - final dbPath = p.join(await getDatabasesPath(), kLichessDatabaseName); - - final appVersion = Version.parse(pInfo.version); - final installedVersion = prefs.getString('installed_version'); - - if (installedVersion == null || - Version.parse(installedVersion) != appVersion) { - prefs.setString('installed_version', appVersion.canonicalizedVersion); - } - - // preload sounds - final soundTheme = GeneralPreferences.fetchFromStorage(prefs).soundTheme; - final soundService = ref.read(soundServiceProvider); - try { - await soundService.initialize(soundTheme); - } catch (e) { - _logger.warning('Cannot initialize SoundService: $e'); - } - - final db = await openDb(databaseFactory, dbPath); - - if (prefs.getBool('first_run') ?? true) { - // Clear secure storage on first run because it is not deleted on app uninstall - await secureStorage.deleteAll(); - - // on android 12+ set the default board theme as system - if (getCorePalette() != null) { - prefs.setString( - BoardPreferences.prefKey, - jsonEncode( - BoardPrefs.defaults.copyWith( - boardTheme: BoardTheme.system, - ), - ), - ); - } - - await prefs.setBool('first_run', false); - } - - // Generate a socket random identifier and store it for the app lifetime - String? storedSri; - try { - storedSri = await secureStorage.read(key: kSRIStorageKey); - if (storedSri == null) { - final sri = genRandomString(12); - _logger.info('Generated new SRI: $sri'); - await secureStorage.write(key: kSRIStorageKey, value: sri); - } - } on PlatformException catch (e) { - _logger.warning('[AppInitialization] Error while reading SRI: $e'); - // Clear all secure storage if an error occurs because it probably means the key has - // been lost - await secureStorage.deleteAll(); - } - - final sri = storedSri ?? - await secureStorage.read(key: kSRIStorageKey) ?? - genRandomString(12); - - final storedSession = await sessionStorage.read(); - if (storedSession != null) { - final client = ref.read(defaultClientProvider); - final response = await client.get( - lichessUri('/api/account'), - headers: { - 'Authorization': 'Bearer ${signBearerToken(storedSession.token)}', - 'User-Agent': makeUserAgent(pInfo, deviceInfo, sri, storedSession.user), - }, - ).timeout(const Duration(seconds: 3)); - if (response.statusCode == 401) { - await sessionStorage.delete(); - } - } - - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; - final engineMaxMemory = (physicalMemory / 10).ceil(); - - return AppInitializationData( - packageInfo: pInfo, - deviceInfo: deviceInfo, - sharedPreferences: prefs, - userSession: await sessionStorage.read(), - database: db, - sri: sri, - engineMaxMemoryInMb: engineMaxMemory, - ); -} - -@freezed -class AppInitializationData with _$AppInitializationData { - const factory AppInitializationData({ - required PackageInfo packageInfo, - required BaseDeviceInfo deviceInfo, - required SharedPreferences sharedPreferences, - required AuthSessionState? userSession, - required Database database, - required String sri, - required int engineMaxMemoryInMb, - }) = _AppInitializationData; -} diff --git a/lib/src/binding.dart b/lib/src/binding.dart new file mode 100644 index 0000000000..4b0baa6fb1 --- /dev/null +++ b/lib/src/binding.dart @@ -0,0 +1,159 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/firebase_options.dart'; +import 'package:lichess_mobile/src/log.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// A singleton class that provides access to plugins and external APIs. +/// +/// Only one instance of this class will be created during the app's lifetime. +/// See [AppLichessBinding] for the concrete implementation. +/// +/// Modeled after the Flutter framework's [WidgetsBinding] class. +/// +/// The preferred way to mock or fake a plugin or external API is to create a +/// provider with riverpod because it gives more flexibility and control over +/// the behavior of the fake. +/// However, if the plugin is used in a way that doesn't allow for easy mocking +/// with riverpod, a test binding can be used to provide a fake implementation. +abstract class LichessBinding { + LichessBinding() : assert(_instance == null) { + initInstance(); + } + + /// The single instance of [LichessBinding]. + static LichessBinding get instance => checkInstance(_instance); + static LichessBinding? _instance; + + @protected + @mustCallSuper + void initInstance() { + _instance = this; + } + + static T checkInstance(T? instance) { + assert(() { + if (instance == null) { + throw FlutterError.fromParts([ + ErrorSummary('Lichess binding has not yet been initialized.'), + ErrorHint( + 'In the app, this is done by the `AppLichessBinding.ensureInitialized()` call ' + 'in the `void main()` method.', + ), + ErrorHint( + 'In a test, one can call `TestLichessBinding.ensureInitialized()` as the ' + "first line in the test's `main()` method to initialize the binding.", + ), + ]); + } + return true; + }()); + return instance!; + } + + /// The shared preferences instance. Must be preloaded before use. + /// + /// This is a synchronous getter that throws an error if shared preferences + /// have not yet been initialized. + SharedPreferencesWithCache get sharedPreferences; + + /// Initialize Firebase. + /// + /// This wraps [Firebase.initializeApp]. + /// + /// This should be called only once before the app starts. + Future initializeFirebase(); + + /// Wraps [FirebaseMessaging.instance]. + FirebaseMessaging get firebaseMessaging; + + /// Wraps [FirebaseMessaging.onMessage]. + Stream get firebaseMessagingOnMessage; + + /// Wraps [FirebaseMessaging.onMessageOpenedApp]. + Stream get firebaseMessagingOnMessageOpenedApp; + + /// Wraps [FirebaseMessaging.onBackgroundMessage]. + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler); +} + +/// A concrete implementation of [LichessBinding] for the app. +class AppLichessBinding extends LichessBinding { + AppLichessBinding() { + setupLogging(); + } + + /// Returns an instance of the binding that implements [LichessBinding]. + /// + /// If no binding has yet been initialized, the [AppLichessBinding] class is + /// used to create and initialize one. + factory AppLichessBinding.ensureInitialized() { + if (LichessBinding._instance == null) { + AppLichessBinding(); + } + return LichessBinding.instance as AppLichessBinding; + } + + late Future _sharedPreferencesWithCache; + SharedPreferencesWithCache? _syncSharedPreferencesWithCache; + + @override + SharedPreferencesWithCache get sharedPreferences { + if (_syncSharedPreferencesWithCache == null) { + throw FlutterError.fromParts([ + ErrorSummary('Shared preferences have not yet been preloaded.'), + ErrorHint( + 'In the app, this is done by the `await AppLichessBinding.preloadSharedPreferences()` call ' + 'in the `Future main()` method.', + ), + ErrorHint( + 'In a test, one can call `TestLichessBinding.setInitialSharedPreferencesValues({})` as the ' + "first line in the test's `main()` method.", + ), + ]); + } + return _syncSharedPreferencesWithCache!; + } + + /// Preload shared preferences. + /// + /// This should be called only once before the app starts. Must be called before + /// [sharedPreferences] is accessed. + Future preloadSharedPreferences() async { + _sharedPreferencesWithCache = SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); + _syncSharedPreferencesWithCache = await _sharedPreferencesWithCache; + } + + @override + Future initializeFirebase() async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + if (kReleaseMode) { + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + } + } + + @override + FirebaseMessaging get firebaseMessaging => FirebaseMessaging.instance; + + @override + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler) { + FirebaseMessaging.onBackgroundMessage(handler); + } + + @override + Stream get firebaseMessagingOnMessage => FirebaseMessaging.onMessage; + + @override + Stream get firebaseMessagingOnMessageOpenedApp => + FirebaseMessaging.onMessageOpenedApp; +} diff --git a/lib/src/constants.dart b/lib/src/constants.dart index c363b4ab46..811cbe7051 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,14 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -const kLichessHost = String.fromEnvironment( - 'LICHESS_HOST', - defaultValue: 'localhost:9663', -); +const kLichessHost = String.fromEnvironment('LICHESS_HOST', defaultValue: 'lichess.dev'); const kLichessWSHost = String.fromEnvironment( 'LICHESS_WS_HOST', - defaultValue: 'localhost:9664', + defaultValue: 'socket.lichess.dev', ); const kLichessWSSecret = String.fromEnvironment( @@ -21,12 +18,18 @@ const kLichessCDNHost = String.fromEnvironment( defaultValue: 'https://lichess1.org', ); -const kLichessDevUser = - String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess'); +const kLichessOpeningExplorerHost = String.fromEnvironment( + 'LICHESS_OPENING_EXPLORER_HOST', + defaultValue: 'explorer.lichess.ovh', +); + +const kLichessDevUser = String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess'); const kLichessDevPassword = String.fromEnvironment('LICHESS_DEV_PASSWORD'); const kLichessClientId = 'lichess_mobile'; +const kSRIStorageKey = 'socket_random_identifier'; + // lichess // https://github.com/lichess-org/lila/blob/4562a83cdb263c3ebf7e148c0f666f0ff92b91c7/modules/rating/src/main/Glicko.scala#L71 const kProvisionalDeviation = 110; @@ -34,6 +37,10 @@ const kClueLessDeviation = 230; // UI +const kDefaultSeedColor = Color.fromARGB(255, 191, 128, 29); + +const kGoldenRatio = 1.61803398875; + /// Flex golden ratio base (flex has to be an int). const kFlexGoldenRatioBase = 100000000000; @@ -41,11 +48,11 @@ const kFlexGoldenRatioBase = 100000000000; const kFlexGoldenRatio = 161803398875; /// Use same box shadows as material widgets with elevation 1. -final List boardShadows = defaultTargetPlatform == TargetPlatform.iOS - ? [] - : kElevationToShadow[1]!; +final List boardShadows = + defaultTargetPlatform == TargetPlatform.iOS ? [] : kElevationToShadow[1]!; const kCardTextScaleFactor = 1.64; +const kCardBorderRadius = BorderRadius.all(Radius.circular(10.0)); const kMaxClockTextScaleFactor = 1.94; const kEmptyWidget = SizedBox.shrink(); const kEmptyFen = '8/8/8/8/8/8/8/8 w - - 0 1'; diff --git a/lib/src/crashlytics.dart b/lib/src/crashlytics.dart index c87f39e270..4592e3bb6c 100644 --- a/lib/src/crashlytics.dart +++ b/lib/src/crashlytics.dart @@ -1,9 +1,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -part 'crashlytics.g.dart'; - -@Riverpod(keepAlive: true) -FirebaseCrashlytics crashlytics(CrashlyticsRef ref) { +final crashlyticsProvider = Provider((ref) { return FirebaseCrashlytics.instance; -} +}); diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index bfbd957253..bde1c7f7ba 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,4 +1,9 @@ -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -13,53 +18,60 @@ const chatReadMessagesTTL = Duration(days: 60); const kStorageAnonId = '**anonymous**'; +final _logger = Logger('Database'); + @Riverpod(keepAlive: true) -Database database(DatabaseRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - final db = ref.read(appInitializationProvider).requireValue.database; - ref.onDispose(db.close); - return db; +Future database(Ref ref) async { + final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); + return openAppDatabase(databaseFactory, dbPath); } /// Returns the sqlite version as an integer. @Riverpod(keepAlive: true) -Future sqliteVersion(SqliteVersionRef ref) async { - final db = ref.read(databaseProvider); +Future sqliteVersion(Ref ref) async { + final db = await ref.read(databaseProvider.future); + return _getDatabaseVersion(db); +} + +Future _getDatabaseVersion(Database db) async { try { - final versionStr = (await db.rawQuery('SELECT sqlite_version()')) - .first - .values - .first - .toString(); - final versionCells = - versionStr.split('.').map((i) => int.parse(i)).toList(); + final versionStr = (await db.rawQuery('SELECT sqlite_version()')).first.values.first.toString(); + final versionCells = versionStr.split('.').map((i) => int.parse(i)).toList(); return versionCells[0] * 100000 + versionCells[1] * 1000 + versionCells[2]; } catch (_) { return null; } } -Future openDb(DatabaseFactory dbFactory, String path) async { +@Riverpod(keepAlive: true) +Future getDbSizeInBytes(Ref ref) async { + final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); + final dbFile = File(dbPath); + + return dbFile.length(); +} + +/// Opens the app database. +Future openAppDatabase(DatabaseFactory dbFactory, String path) async { return dbFactory.openDatabase( path, options: OpenDatabaseOptions( - version: 2, + version: 3, + onConfigure: (db) async { + final version = await _getDatabaseVersion(db); + _logger.info('SQLite version: $version'); + }, onOpen: (db) async { await Future.wait([ _deleteOldEntries(db, 'puzzle', puzzleTTL), _deleteOldEntries(db, 'correspondence_game', corresGameTTL), _deleteOldEntries(db, 'game', gameTTL), - _deleteOldEntries( - db, - 'chat_read_messages', - chatReadMessagesTTL, - ), + _deleteOldEntries(db, 'chat_read_messages', chatReadMessagesTTL), ]); }, onCreate: (db, version) async { final batch = db.batch(); - _createPuzzleBatchTableV1(batch); + _createPuzzleBatchTableV3(batch); _createPuzzleTableV1(batch); _createCorrespondenceGameTableV1(batch); _createChatReadMessagesTableV1(batch); @@ -71,6 +83,9 @@ Future openDb(DatabaseFactory dbFactory, String path) async { if (oldVersion == 1) { _createGameTableV2(batch); } + if (oldVersion < 3) { + _updatePuzzleBatchTableToV3(batch); + } await batch.commit(); }, onDowngrade: onDatabaseDowngradeDelete, @@ -78,18 +93,37 @@ Future openDb(DatabaseFactory dbFactory, String path) async { ); } -void _createPuzzleBatchTableV1(Batch batch) { +void _createPuzzleBatchTableV3(Batch batch) { batch.execute('DROP TABLE IF EXISTS puzzle_batchs'); batch.execute(''' CREATE TABLE puzzle_batchs( userId TEXT NOT NULL, angle TEXT NOT NULL, + lastModified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL, PRIMARY KEY (userId, angle) ) '''); } +void _updatePuzzleBatchTableToV3(Batch batch) { + batch.execute(''' + CREATE TABLE puzzle_batchs_new( + userId TEXT NOT NULL, + angle TEXT NOT NULL, + lastModified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + data TEXT NOT NULL, + PRIMARY KEY (userId, angle) + ) + '''); + batch.execute(''' + INSERT INTO puzzle_batchs_new(userId, angle, data) + SELECT userId, angle, data FROM puzzle_batchs + '''); + batch.execute('DROP TABLE puzzle_batchs'); + batch.execute('ALTER TABLE puzzle_batchs_new RENAME TO puzzle_batchs'); +} + void _createPuzzleTableV1(Batch batch) { batch.execute('DROP TABLE IF EXISTS puzzle'); batch.execute(''' @@ -142,15 +176,18 @@ void _createChatReadMessagesTableV1(Batch batch) { Future _deleteOldEntries(Database db, String table, Duration ttl) async { final date = DateTime.now().subtract(ttl); - final tableExists = await db.rawQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name='$table'", - ); - if (tableExists.isEmpty) { + + if (!await _doesTableExist(db, table)) { return; } - await db.delete( - table, - where: 'lastModified < ?', - whereArgs: [date.toIso8601String()], + + await db.delete(table, where: 'lastModified < ?', whereArgs: [date.toIso8601String()]); +} + +Future _doesTableExist(Database db, String table) async { + final tableExists = await db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='$table'", ); + + return tableExists.isNotEmpty; } diff --git a/lib/src/db/openings_database.dart b/lib/src/db/openings_database.dart index 23689c63a3..c9056620b4 100644 --- a/lib/src/db/openings_database.dart +++ b/lib/src/db/openings_database.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -20,7 +21,7 @@ const _kDatabaseVersion = 2; const _kDatabaseName = 'chess_openings$_kDatabaseVersion.db'; @Riverpod(keepAlive: true) -Future openingsDatabase(OpeningsDatabaseRef ref) async { +Future openingsDatabase(Ref ref) async { final dbPath = p.join(await getDatabasesPath(), _kDatabaseName); return _openDb(dbPath); } @@ -44,14 +45,12 @@ Future _openDb(String path) async { }); // Copy from asset - final ByteData data = - await rootBundle.load(p.url.join('assets', 'chess_openings.db')); - final List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + final ByteData data = await rootBundle.load(p.url.join('assets', 'chess_openings.db')); + final List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); // Write and flush the bytes written await File(path).writeAsBytes(bytes, flush: true); } - return openDatabase(path, readOnly: true); + return databaseFactory.openDatabase(path, options: OpenDatabaseOptions(readOnly: true)); } diff --git a/lib/src/db/secure_storage.dart b/lib/src/db/secure_storage.dart index 73e8e8760a..f0bb643a63 100644 --- a/lib/src/db/secure_storage.dart +++ b/lib/src/db/secure_storage.dart @@ -1,14 +1,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'secure_storage.g.dart'; AndroidOptions _getAndroidOptions() => const AndroidOptions( - encryptedSharedPreferences: true, - sharedPreferencesName: 'org.lichess.mobile.secure', - ); + encryptedSharedPreferences: true, + sharedPreferencesName: 'org.lichess.mobile.secure', +); + +class SecureStorage extends FlutterSecureStorage { + const SecureStorage._({super.aOptions}); -@Riverpod(keepAlive: true) -FlutterSecureStorage secureStorage(SecureStorageRef ref) { - return FlutterSecureStorage(aOptions: _getAndroidOptions()); + static final instance = SecureStorage._(aOptions: _getAndroidOptions()); } diff --git a/lib/src/db/shared_preferences.dart b/lib/src/db/shared_preferences.dart deleted file mode 100644 index f5e00fd9d4..0000000000 --- a/lib/src/db/shared_preferences.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'shared_preferences.g.dart'; - -@Riverpod(keepAlive: true) -SharedPreferences sharedPreferences(SharedPreferencesRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.sharedPreferences; -} diff --git a/lib/src/init.dart b/lib/src/init.dart new file mode 100644 index 0000000000..655a4a5d31 --- /dev/null +++ b/lib/src/init.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; + +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/utils/chessboard.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:logging/logging.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:pub_semver/pub_semver.dart'; + +final _logger = Logger('Init'); + +/// Run initialization tasks only once on first app launch or after an update. +Future setupFirstLaunch() async { + final prefs = LichessBinding.instance.sharedPreferences; + final pInfo = await PackageInfo.fromPlatform(); + + final appVersion = Version.parse(pInfo.version); + final installedVersion = prefs.getString('installed_version'); + + // TODO remove this migration code after a few releases + if (installedVersion != null && Version.parse(installedVersion) <= Version(0, 13, 9)) { + _migrateThemeSettings(); + } + + if (installedVersion == null || Version.parse(installedVersion) != appVersion) { + prefs.setString('installed_version', appVersion.canonicalizedVersion); + } + + if (prefs.getBool('first_run') ?? true) { + // Clear secure storage on first run because it is not deleted on app uninstall + await SecureStorage.instance.deleteAll(); + + // Generate a socket random identifier and store it for the app lifetime + final sri = genRandomString(12); + _logger.info('Generated new SRI: $sri'); + await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); + + await prefs.setBool('first_run', false); + } +} + +Future _migrateThemeSettings() async { + if (getCorePalette() == null) { + return; + } + final prefs = LichessBinding.instance.sharedPreferences; + try { + final stored = LichessBinding.instance.sharedPreferences.getString( + PrefCategory.general.storageKey, + ); + if (stored == null) { + return; + } + final generalPrefs = GeneralPrefs.fromJson(jsonDecode(stored) as Map); + final migrated = generalPrefs.copyWith( + appThemeSeed: + // ignore: deprecated_member_use_from_same_package + generalPrefs.systemColors == true ? AppThemeSeed.system : AppThemeSeed.board, + ); + await prefs.setString(PrefCategory.general.storageKey, jsonEncode(migrated.toJson())); + } catch (e) { + _logger.warning('Failed to migrate theme settings: $e'); + } +} + +Future initializeLocalNotifications(Locale locale) async { + final l10n = await AppLocalizations.delegate.load(locale); + await FlutterLocalNotificationsPlugin().initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: NotificationService.onDidReceiveNotificationResponse, + // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); +} + +Future preloadPieceImages() async { + final prefs = LichessBinding.instance.sharedPreferences; + final storedPrefs = prefs.getString(PrefCategory.board.storageKey); + BoardPrefs boardPrefs = BoardPrefs.defaults; + if (storedPrefs != null) { + try { + boardPrefs = BoardPrefs.fromJson(jsonDecode(storedPrefs) as Map); + } catch (e) { + _logger.warning('Failed to decode board preferences: $e'); + } + } + + await precachePieceImages(boardPrefs.pieceSet); +} + +/// Display setup on Android. +/// +/// This is meant to be called once during app initialization. +Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { + // On android 12+ set core palette and make system board + try { + await DynamicColorPlugin.getCorePalette().then((value) { + setCorePalette(value); + }); + } catch (e) { + _logger.fine('Device does not support core palette: $e'); + } + + // lock orientation to portrait on android phones + final view = widgetsBinding.platformDispatcher.views.first; + final data = MediaQueryData.fromView(view); + if (data.size.shortestSide < FormFactor.tablet) { + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + } + + // Sets edge-to-edge system UI mode on Android 12+ + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarContrastEnforced: true, + ), + ); + + /// Enables high refresh rate for devices where it was previously disabled + final List supported = await FlutterDisplayMode.supported; + final DisplayMode active = await FlutterDisplayMode.active; + + final List sameResolution = + supported + .where((DisplayMode m) => m.width == active.width && m.height == active.height) + .toList() + ..sort((DisplayMode a, DisplayMode b) => b.refreshRate.compareTo(a.refreshRate)); + + final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active; + + // This setting is per session. + await FlutterDisplayMode.setPreferredMode(mostOptimalMode); +} diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 47dad98260..f415ff0622 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -1,76 +1,25 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:intl/intl_standalone.dart'; -import 'package:timeago/timeago.dart' as timeago; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -Future setupIntl() async { - // we need call this because it is not setup automatically - final systemLocale = await findSystemLocale(); - Intl.defaultLocale = systemLocale; +/// Setup [Intl.defaultLocale] and timeago locale and messages. +Future setupIntl(WidgetsBinding widgetsBinding) async { + final systemLocale = widgetsBinding.platformDispatcher.locale; - // we need to setup timeago locale manually - final currentLocale = Intl.getCurrentLocale(); - final longLocale = Intl.canonicalizedLocale(currentLocale); - final messages = _timeagoLocales[longLocale]; - if (messages != null) { - timeago.setLocaleMessages(longLocale, messages); - timeago.setDefaultLocale(longLocale); - } else { - final shortLocale = Intl.shortLocale(currentLocale); - final messages = _timeagoLocales[shortLocale]; - if (messages != null) { - timeago.setLocaleMessages(shortLocale, messages); - timeago.setDefaultLocale(shortLocale); - } - } -} + // Get locale from shared preferences, if any + final json = LichessBinding.instance.sharedPreferences.getString(PrefCategory.general.storageKey); + final generalPref = + json != null + ? GeneralPrefs.fromJson(jsonDecode(json) as Map) + : GeneralPrefs.defaults; + final prefsLocale = generalPref.locale; + final locale = prefsLocale ?? systemLocale; -final Map _timeagoLocales = { - 'am': timeago.AmMessages(), - 'ar': timeago.ArMessages(), - 'az': timeago.AzMessages(), - 'be': timeago.BeMessages(), - 'bs': timeago.BsMessages(), - 'ca': timeago.CaMessages(), - 'cs': timeago.CsMessages(), - 'da': timeago.DaMessages(), - 'de': timeago.DeMessages(), - 'dv': timeago.DvMessages(), - 'en': timeago.EnMessages(), - 'es': timeago.EsMessages(), - 'et': timeago.EtMessages(), - 'fa': timeago.FaMessages(), - 'fi': timeago.FiMessages(), - 'fr': timeago.FrMessages(), - 'gr': timeago.GrMessages(), - 'he': timeago.HeMessages(), - 'hi': timeago.HiMessages(), - 'hr': timeago.HrMessages(), - 'hu': timeago.HuMessages(), - 'id': timeago.IdMessages(), - 'it': timeago.ItMessages(), - 'ja': timeago.JaMessages(), - 'km': timeago.KmMessages(), - 'ko': timeago.KoMessages(), - 'ku': timeago.KuMessages(), - 'lv': timeago.LvMessages(), - 'mn': timeago.MnMessages(), - 'ms_MY': timeago.MsMyMessages(), - 'nb_NO': timeago.NbNoMessages(), - 'nl': timeago.NlMessages(), - 'nn_NO': timeago.NnNoMessages(), - 'pl': timeago.PlMessages(), - 'pt_BR': timeago.PtBrMessages(), - 'ro': timeago.RoMessages(), - 'ru': timeago.RuMessages(), - 'sr': timeago.SrMessages(), - 'sv': timeago.SvMessages(), - 'ta': timeago.TaMessages(), - 'th': timeago.ThMessages(), - 'tk': timeago.TkMessages(), - 'tr': timeago.TrMessages(), - 'uk': timeago.UkMessages(), - 'ur': timeago.UrMessages(), - 'vi': timeago.ViMessages(), - 'zh_CN': timeago.ZhCnMessages(), - 'zh': timeago.ZhMessages(), -}; + Intl.defaultLocale = locale.toLanguageTag(); + + return locale; +} diff --git a/lib/src/localizations.dart b/lib/src/localizations.dart new file mode 100644 index 0000000000..62767000ad --- /dev/null +++ b/lib/src/localizations.dart @@ -0,0 +1,47 @@ +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'localizations.g.dart'; + +typedef ActiveLocalizations = ({Locale locale, AppLocalizations strings}); + +@Riverpod(keepAlive: true) +class Localizations extends _$Localizations { + @override + ActiveLocalizations build() { + final generalPrefs = ref.watch(generalPreferencesProvider); + final observer = _LocaleObserver((locales) { + _update(); + }); + final binding = WidgetsBinding.instance; + binding.addObserver(observer); + ref.onDispose(() => binding.removeObserver(observer)); + + return _getLocale(generalPrefs); + } + + void _update() { + final generalPrefs = ref.read(generalPreferencesProvider); + state = _getLocale(generalPrefs); + } + + ActiveLocalizations _getLocale(GeneralPrefs prefs) { + if (prefs.locale != null) { + return (locale: prefs.locale!, strings: lookupAppLocalizations(prefs.locale!)); + } + final locale = WidgetsBinding.instance.platformDispatcher.locale; + return (locale: locale, strings: lookupAppLocalizations(locale)); + } +} + +/// observer used to notify the caller when the locale changes +class _LocaleObserver extends WidgetsBindingObserver { + _LocaleObserver(this._didChangeLocales); + final void Function(List? locales) _didChangeLocales; + @override + void didChangeLocales(List? locales) { + _didChangeLocales(locales); + } +} diff --git a/lib/src/log.dart b/lib/src/log.dart index 4c1ab06426..5526448da9 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -5,12 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; // to see http requests and websocket connections in terminal -const _loggersToShowInTerminal = { - 'HttpClient', - 'Socket', -}; +const _loggersToShowInTerminal = {'HttpClient', 'Socket'}; -void setupLogger() { +/// Setup logging +void setupLogging() { if (kDebugMode) { Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { @@ -23,13 +21,11 @@ void setupLogger() { stackTrace: record.stackTrace, ); - if (_loggersToShowInTerminal.contains(record.loggerName) && - record.level >= Level.INFO) { + if (_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.INFO) { debugPrint('[${record.loggerName}] ${record.message}'); } - if (!_loggersToShowInTerminal.contains(record.loggerName) && - record.level >= Level.WARNING) { + if (!_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.WARNING) { debugPrint('[${record.loggerName}] ${record.message}'); } }); @@ -40,22 +36,12 @@ class ProviderLogger extends ProviderObserver { final _logger = Logger('Provider'); @override - void didAddProvider( - ProviderBase provider, - Object? value, - ProviderContainer container, - ) { - _logger.info( - '${provider.name ?? provider.runtimeType} initialized', - value, - ); + void didAddProvider(ProviderBase provider, Object? value, ProviderContainer container) { + _logger.info('${provider.name ?? provider.runtimeType} initialized', value); } @override - void didDisposeProvider( - ProviderBase provider, - ProviderContainer container, - ) { + void didDisposeProvider(ProviderBase provider, ProviderContainer container) { _logger.info('${provider.name ?? provider.runtimeType} disposed'); } @@ -66,10 +52,6 @@ class ProviderLogger extends ProviderObserver { StackTrace stackTrace, ProviderContainer container, ) { - _logger.severe( - '${provider.name ?? provider.runtimeType} error', - error, - stackTrace, - ); + _logger.severe('${provider.name ?? provider.runtimeType} error', error, stackTrace); } } diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index 60d89224e3..9984025ac0 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -1,7 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -9,48 +10,50 @@ import 'account_repository.dart'; part 'account_preferences.g.dart'; -typedef AccountPrefState = ({ - // game display - Zen zenMode, - PieceNotation pieceNotation, - BooleanPref showRatings, - // game behavior - BooleanPref premove, - AutoQueen autoQueen, - AutoThreefold autoThreefold, - Takeback takeback, - BooleanPref confirmResign, - SubmitMove submitMove, - // clock - Moretime moretime, - BooleanPref clockSound, - // privacy - BooleanPref follow, -}); +typedef AccountPrefState = + ({ + // game display + Zen zenMode, + PieceNotation pieceNotation, + BooleanPref showRatings, + // game behavior + BooleanPref premove, + AutoQueen autoQueen, + AutoThreefold autoThreefold, + Takeback takeback, + BooleanPref confirmResign, + SubmitMove submitMove, + // clock + Moretime moretime, + BooleanPref clockSound, + // privacy + BooleanPref follow, + Challenge challenge, + }); /// A provider that tells if the user wants to see ratings in the app. -final showRatingsPrefProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future showRatingsPref(Ref ref) async { return ref.watch( - accountPreferencesProvider - .selectAsync((state) => state?.showRatings.value ?? true), + accountPreferencesProvider.selectAsync((state) => state?.showRatings.value ?? true), ); -}); +} -final clockSoundProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future clockSound(Ref ref) async { return ref.watch( - accountPreferencesProvider - .selectAsync((state) => state?.clockSound.value ?? true), + accountPreferencesProvider.selectAsync((state) => state?.clockSound.value ?? true), ); -}); +} -final pieceNotationProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future pieceNotation(Ref ref) async { return ref.watch( accountPreferencesProvider.selectAsync( - (state) => - state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, + (state) => state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, ), ); -}); +} final defaultAccountPreferences = ( zenMode: Zen.no, @@ -63,10 +66,9 @@ final defaultAccountPreferences = ( moretime: Moretime.always, clockSound: const BooleanPref(true), confirmResign: const BooleanPref(true), - submitMove: SubmitMove({ - SubmitMoveChoice.correspondence, - }), + submitMove: SubmitMove({SubmitMoveChoice.correspondence}), follow: const BooleanPref(true), + challenge: Challenge.registered, ); /// Get the account preferences for the current user. @@ -84,40 +86,31 @@ class AccountPreferences extends _$AccountPreferences { } try { - return ref.withClient( - (client) => AccountRepository(client).getPreferences(), - ); + return ref.withClient((client) => AccountRepository(client).getPreferences()); } catch (e) { - debugPrint( - '[AccountPreferences] Error getting account preferences: $e', - ); + debugPrint('[AccountPreferences] Error getting account preferences: $e'); return defaultAccountPreferences; } } Future setZen(Zen value) => _setPref('zen', value); - Future setPieceNotation(PieceNotation value) => - _setPref('pieceNotation', value); + Future setPieceNotation(PieceNotation value) => _setPref('pieceNotation', value); Future setShowRatings(BooleanPref value) => _setPref('ratings', value); Future setPremove(BooleanPref value) => _setPref('premove', value); Future setTakeback(Takeback value) => _setPref('takeback', value); Future setAutoQueen(AutoQueen value) => _setPref('autoQueen', value); - Future setAutoThreefold(AutoThreefold value) => - _setPref('autoThreefold', value); + Future setAutoThreefold(AutoThreefold value) => _setPref('autoThreefold', value); Future setMoretime(Moretime value) => _setPref('moretime', value); - Future setClockSound(BooleanPref value) => - _setPref('clockSound', value); - Future setConfirmResign(BooleanPref value) => - _setPref('confirmResign', value); + Future setClockSound(BooleanPref value) => _setPref('clockSound', value); + Future setConfirmResign(BooleanPref value) => _setPref('confirmResign', value); Future setSubmitMove(SubmitMove value) => _setPref('submitMove', value); Future setFollow(BooleanPref value) => _setPref('follow', value); + Future setChallenge(Challenge value) => _setPref('challenge', value); Future _setPref(String key, AccountPref value) async { await Future.delayed(const Duration(milliseconds: 200)); - await ref.withClient( - (client) => AccountRepository(client).setPreference(key, value), - ); + await ref.withClient((client) => AccountRepository(client).setPreference(key, value)); ref.invalidateSelf(); } } @@ -371,9 +364,56 @@ enum Moretime implements AccountPref { } } +enum Challenge implements AccountPref { + never(1), + rating(2), + friends(3), + registered(4), + always(5); + + const Challenge(this.value); + + @override + final int value; + + @override + String get toFormData => value.toString(); + + String label(BuildContext context) { + switch (this) { + case Challenge.never: + return context.l10n.never; + case Challenge.rating: + return context.l10n.ifRatingIsPlusMinusX('300'); + case Challenge.friends: + return context.l10n.onlyFriends; + case Challenge.registered: + return context.l10n.ifRegistered; + case Challenge.always: + return context.l10n.always; + } + } + + static Challenge fromInt(int value) { + switch (value) { + case 1: + return Challenge.never; + case 2: + return Challenge.rating; + case 3: + return Challenge.friends; + case 4: + return Challenge.registered; + case 5: + return Challenge.always; + default: + throw Exception('Invalid value for Challenge'); + } + } +} + class SubmitMove implements AccountPref { - SubmitMove(Iterable choices) - : choices = ISet(choices.toSet()); + SubmitMove(Iterable choices) : choices = ISet(choices.toSet()); final ISet choices; @@ -391,10 +431,8 @@ class SubmitMove implements AccountPref { return choices.map((choice) => choice.label(context)).join(', '); } - factory SubmitMove.fromInt(int value) => SubmitMove( - SubmitMoveChoice.values - .where((choice) => _bitPresent(value, choice.value)), - ); + factory SubmitMove.fromInt(int value) => + SubmitMove(SubmitMoveChoice.values.where((choice) => _bitPresent(value, choice.value))); } enum SubmitMoveChoice { diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index b3e3a84203..443199d070 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -1,14 +1,15 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -18,7 +19,7 @@ import 'ongoing_game.dart'; part 'account_repository.g.dart'; @riverpod -Future account(AccountRef ref) async { +Future account(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return null; @@ -29,12 +30,12 @@ Future account(AccountRef ref) async { } @riverpod -Future accountUser(AccountUserRef ref) async { +Future accountUser(Ref ref) async { return ref.watch(accountProvider.selectAsync((user) => user?.lightUser)); } @riverpod -Future> accountActivity(AccountActivityRef ref) async { +Future> accountActivity(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); return ref.withClientCacheFor( @@ -44,7 +45,7 @@ Future> accountActivity(AccountActivityRef ref) async { } @riverpod -Future> ongoingGames(OngoingGamesRef ref) async { +Future> ongoingGames(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); @@ -61,44 +62,25 @@ class AccountRepository { final Logger _log = Logger('AccountRepository'); Future getProfile() { - return client.readJson( - Uri(path: '/api/account'), - mapper: User.fromServerJson, - ); + return client.readJson(Uri(path: '/api/account'), mapper: User.fromServerJson); } Future saveProfile(Map profile) async { final uri = Uri(path: '/account/profile'); - final response = await client.post( - uri, - headers: {'Accept': 'application/json'}, - body: profile, - ); + final response = await client.post(uri, headers: {'Accept': 'application/json'}, body: profile); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to post save profile: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to post save profile: ${response.statusCode}', uri); } } Future> getOngoingGames({int? nb}) { return client.readJson( - Uri( - path: '/api/account/playing', - queryParameters: nb != null - ? { - 'nb': nb.toString(), - } - : null, - ), + Uri(path: '/api/account/playing', queryParameters: nb != null ? {'nb': nb.toString()} : null), mapper: (Map json) { final list = json['nowPlaying']; if (list is! List) { - _log.severe( - 'Could not read json object as {nowPlaying: []}: expected a list.', - ); + _log.severe('Could not read json object as {nowPlaying: []}: expected a list.'); throw Exception('Could not read json object as {nowPlaying: []}'); } return list @@ -113,9 +95,7 @@ class AccountRepository { return client.readJson( Uri(path: '/api/account/preferences'), mapper: (Map json) { - return _accountPreferencesFromPick( - pick(json, 'prefs').required(), - ); + return _accountPreferencesFromPick(pick(json, 'prefs').required()); }, ); } @@ -126,44 +106,26 @@ class AccountRepository { final response = await client.post(uri, body: {prefKey: pref.toFormData}); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to set preference: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to set preference: ${response.statusCode}', uri); } } } AccountPrefState _accountPreferencesFromPick(RequiredPick pick) { return ( - zenMode: Zen.fromInt( - pick('zen').asIntOrThrow(), - ), - pieceNotation: PieceNotation.fromInt( - pick('pieceNotation').asIntOrThrow(), - ), + zenMode: Zen.fromInt(pick('zen').asIntOrThrow()), + pieceNotation: PieceNotation.fromInt(pick('pieceNotation').asIntOrThrow()), showRatings: BooleanPref.fromInt(pick('ratings').asIntOrThrow()), premove: BooleanPref(pick('premove').asBoolOrThrow()), - autoQueen: AutoQueen.fromInt( - pick('autoQueen').asIntOrThrow(), - ), - autoThreefold: AutoThreefold.fromInt( - pick('autoThreefold').asIntOrThrow(), - ), - takeback: Takeback.fromInt( - pick('takeback').asIntOrThrow(), - ), - moretime: Moretime.fromInt( - pick('moretime').asIntOrThrow(), - ), + autoQueen: AutoQueen.fromInt(pick('autoQueen').asIntOrThrow()), + autoThreefold: AutoThreefold.fromInt(pick('autoThreefold').asIntOrThrow()), + takeback: Takeback.fromInt(pick('takeback').asIntOrThrow()), + moretime: Moretime.fromInt(pick('moretime').asIntOrThrow()), clockSound: BooleanPref(pick('clockSound').asBoolOrThrow()), - confirmResign: BooleanPref.fromInt( - pick('confirmResign').asIntOrThrow(), - ), - submitMove: SubmitMove.fromInt( - pick('submitMove').asIntOrThrow(), - ), + confirmResign: BooleanPref.fromInt(pick('confirmResign').asIntOrThrow()), + submitMove: SubmitMove.fromInt(pick('submitMove').asIntOrThrow()), follow: BooleanPref(pick('follow').asBoolOrThrow()), + challenge: Challenge.fromInt(pick('challenge').asIntOrThrow()), ); } diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 66957d6bd3..008f3c249f 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -17,90 +17,100 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_controller.freezed.dart'; part 'analysis_controller.g.dart'; -const standaloneAnalysisId = StringId('standalone_analysis'); final _dateFormat = DateFormat('yyyy.MM.dd'); +typedef StandaloneAnalysis = ({String pgn, Variant variant, bool isComputerAnalysisAllowed}); + @freezed class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); + + @Assert('standalone != null || gameId != null') const factory AnalysisOptions({ - /// The ID of the analysis. Can be a game ID or a standalone analysis ID. - required StringId id, - required bool isLocalEvaluationAllowed, required Side orientation, - required Variant variant, + StandaloneAnalysis? standalone, + GameId? gameId, int? initialMoveCursor, - LightOpening? opening, - Division? division, - - /// Optional server analysis to display player stats. - ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, }) = _AnalysisOptions; - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; - - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - id != standaloneAnalysisId ? GameAnyId(id.value) : null; + bool get isLichessGameAnalysis => gameId != null; } @riverpod -class AnalysisController extends _$AnalysisController { +class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { late Root _root; + late Variant _variant; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); Timer? _startEngineEvalTimer; @override - AnalysisState build(String pgn, AnalysisOptions options) { + Future build(AnalysisOptions options) async { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); - ref.onDispose(() { - _startEngineEvalTimer?.cancel(); - _engineEvalDebounce.dispose(); - evaluationService.disposeEngine(); - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); - }); - - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); + late final String pgn; + late final LightOpening? opening; + late final ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis; + late final Division? division; + + if (options.gameId != null) { + final game = await ref.watch(archivedGameProvider(id: options.gameId!).future); + _variant = game.meta.variant; + pgn = game.makePgn(); + opening = game.data.opening; + serverAnalysis = game.serverAnalysis; + division = game.meta.division; + } else { + _variant = options.standalone!.variant; + pgn = options.standalone!.pgn; + opening = null; + serverAnalysis = null; + division = null; + } UciPath path = UciPath.empty; Move? lastMove; final game = PgnGame.parsePgn( pgn, - initHeaders: () => options.isLichessGameAnalysis - ? {} - : { - 'Event': '?', - 'Site': '?', - 'Date': _dateFormat.format(DateTime.now()), - 'Round': '?', - 'White': '?', - 'Black': '?', - 'Result': '*', - 'WhiteElo': '?', - 'BlackElo': '?', - }, + initHeaders: + () => + options.isLichessGameAnalysis + ? {} + : { + 'Event': '?', + 'Site': '?', + 'Date': _dateFormat.format(DateTime.now()), + 'Round': '?', + 'White': '?', + 'Black': '?', + 'Result': '*', + 'WhiteElo': '?', + 'BlackElo': '?', + }, ); final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - Future? openingFuture; + final isComputerAnalysisAllowed = + options.isLichessGameAnalysis + ? pgnHeaders['Result'] != '*' + : options.standalone!.isComputerAnalysisAllowed; + + final List> openingFutures = []; _root = Root.fromPgnGame( game, @@ -109,33 +119,60 @@ class AnalysisController extends _$AnalysisController { onVisitNode: (root, branch, isMainline) { if (isMainline && options.initialMoveCursor != null && - branch.position.ply <= - root.position.ply + options.initialMoveCursor!) { + branch.position.ply <= root.position.ply + options.initialMoveCursor!) { path = path + branch.id; lastMove = branch.sanMove.move; } - if (isMainline && options.opening == null && branch.position.ply <= 5) { - openingFuture = _fetchOpening(root, path); + if (isMainline && opening == null && branch.position.ply <= 10) { + openingFutures.add(_fetchOpening(root, path)); } }, ); // wait for the opening to be fetched to recompute the branch opening - openingFuture?.then((_) { - _setPath(state.currentPath); - }); + Future.wait(openingFutures) + .then((list) { + bool hasOpening = false; + for (final updated in list) { + if (updated != null) { + hasOpening = true; + final (path, opening) = updated; + _root.updateAt(path, (node) => node.opening = opening); + } + } + return hasOpening; + }) + .then((hasOpening) { + if (hasOpening) { + scheduleMicrotask(() { + _setPath(state.requireValue.currentPath); + }); + } + }); - final currentPath = - options.initialMoveCursor == null ? _root.mainlinePath : path; + final currentPath = options.initialMoveCursor == null ? _root.mainlinePath : path; final currentNode = _root.nodeAt(currentPath); // don't use ref.watch here: we don't want to invalidate state when the // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); + final isEngineAllowed = engineSupportedVariants.contains(_variant); + + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } + serverAnalysisService.lastAnalysisEvent.removeListener(_listenToServerAnalysisEvents); + }); + + serverAnalysisService.lastAnalysisEvent.addListener(_listenToServerAnalysisEvents); + final analysisState = AnalysisState( - variant: options.variant, - id: options.id, + variant: _variant, + gameId: options.gameId, currentPath: currentPath, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, @@ -144,24 +181,17 @@ class AnalysisController extends _$AnalysisController { pgnRootComments: rootComments, lastMove: lastMove, pov: options.orientation, - contextOpening: options.opening, - isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + contextOpening: opening, + isComputerAnalysisAllowed: isComputerAnalysisAllowed, + isComputerAnalysisEnabled: prefs.enableComputerAnalysis, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - displayMode: DisplayMode.moves, - playersAnalysis: options.serverAnalysis, - acplChartData: _makeAcplChartData(), + playersAnalysis: serverAnalysis, + acplChartData: serverAnalysis != null ? _makeAcplChartData() : null, + division: division, ); if (analysisState.isEngineAvailable) { - evaluationService - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - ), - ) - .then((_) { + evaluationService.initEngine(_evaluationContext, options: _evaluationOptions).then((_) { _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { _startEngineEval(); }); @@ -171,27 +201,47 @@ class AnalysisController extends _$AnalysisController { return analysisState; } - EvaluationContext get _evaluationContext => EvaluationContext( - variant: options.variant, - initialPosition: _root.position, - ); + EvaluationContext get _evaluationContext => + EvaluationContext(variant: _variant, initialPosition: _root.position); + + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + + void onUserMove(NormalMove move, {bool shouldReplace = false}) { + if (!state.requireValue.position.isLegal(move)) return; - void onUserMove(Move move) { - if (!state.position.isLegal(move)) return; - final (newPath, isNewNode) = _root.addMoveAt(state.currentPath, move); + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncValue.data(state.requireValue.copyWith(promotionMove: move)); + return; + } + + final (newPath, isNewNode) = _root.addMoveAt( + state.requireValue.currentPath, + move, + replace: shouldReplace, + ); if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); + } + } + + void onPromotionSelection(Role? role) { + if (role == null) { + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.requireValue.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); } } void userNext() { - if (!state.currentNode.hasChild) return; + final curState = state.requireValue; + if (!curState.currentNode.hasChild) return; _setPath( - state.currentPath + _root.nodeAt(state.currentPath).children.first.id, + curState.currentPath + _root.nodeAt(curState.currentPath).children.first.id, replaying: true, ); } @@ -220,61 +270,93 @@ class AnalysisController extends _$AnalysisController { } void toggleBoard() { - state = state.copyWith(pov: state.pov.opposite); + final curState = state.requireValue; + state = AsyncData(curState.copyWith(pov: curState.pov.opposite)); } void userPrevious() { - _setPath(state.currentPath.penultimate, replaying: true); + _setPath(state.requireValue.currentPath.penultimate, replaying: true); } + @override void userJump(UciPath path) { _setPath(path); } - void showAllVariations(UciPath path) { - final parent = _root.parentAt(path); - for (final node in parent.children) { - node.isHidden = false; + @override + void expandVariations(UciPath path) { + final node = _root.nodeAt(path); + + final childrenToShow = _root.isOnMainline(path) ? node.children.skip(1) : node.children; + + for (final child in childrenToShow) { + child.isCollapsed = false; + for (final grandChild in child.children) { + grandChild.isCollapsed = false; + } } - state = state.copyWith(root: _root.view); + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } - void hideVariation(UciPath path) { - _root.hideVariationAt(path); - state = state.copyWith(root: _root.view); + @override + void collapseVariations(UciPath path) { + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isCollapsed = true; + } + + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } + @override void promoteVariation(UciPath path, bool toMainline) { _root.promoteAt(path, toMainline: toMainline); - state = state.copyWith( - isOnMainline: _root.isOnMainline(state.currentPath), - root: _root.view, + final curState = state.requireValue; + state = AsyncData( + curState.copyWith(isOnMainline: _root.isOnMainline(curState.currentPath), root: _root.view), ); } + @override void deleteFromHere(UciPath path) { _root.deleteAt(path); _setPath(path.penultimate, shouldRecomputeRootView: true); } + /// Toggles the computer analysis on/off. + /// + /// Acts both on local evaluation and server analysis. + Future toggleComputerAnalysis() async { + await ref.read(analysisPreferencesProvider.notifier).toggleEnableComputerAnalysis(); + + final curState = state.requireValue; + final engineWasAvailable = curState.isEngineAvailable; + + state = AsyncData( + curState.copyWith(isComputerAnalysisEnabled: !curState.isComputerAnalysisEnabled), + ); + + final computerAllowed = state.requireValue.isComputerAnalysisEnabled; + if (!computerAllowed && engineWasAvailable) { + toggleLocalEvaluation(); + } + } + + /// Toggles the local evaluation on/off. Future toggleLocalEvaluation() async { - ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableLocalEvaluation(); + await ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); - state = state.copyWith( - isLocalEvaluationEnabled: !state.isLocalEvaluationEnabled, + state = AsyncData( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), ); - if (state.isEngineAvailable) { - final prefs = ref.read(analysisPreferencesProvider); - await ref.read(evaluationServiceProvider).initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - ), - ); + if (state.requireValue.isEngineAvailable) { + await ref + .read(evaluationServiceProvider) + .initEngine(_evaluationContext, options: _evaluationOptions); _startEngineEval(); } else { _stopEngineEval(); @@ -283,62 +365,47 @@ class AnalysisController extends _$AnalysisController { } void setNumEvalLines(int numEvalLines) { - ref - .read(analysisPreferencesProvider.notifier) - .setNumEvalLines(numEvalLines); + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref.read(evaluationServiceProvider).setOptions( - EvaluationOptions( - multiPv: numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _root.updateAll((node) => node.eval = null); - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + ), ); _startEngineEval(); } void setEngineCores(int numEngineCores) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineCores(numEngineCores); + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref.read(evaluationServiceProvider).setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: numEngineCores, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } - void updatePgnHeader(String key, String value) { - final headers = state.pgnHeaders.add(key, value); - state = state.copyWith(pgnHeaders: headers); + void setEngineSearchTime(Duration searchTime) { + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _startEngineEval(); } - void toggleDisplayMode() { - state = state.copyWith( - displayMode: state.displayMode == DisplayMode.moves - ? DisplayMode.summary - : DisplayMode.moves, - ); + void updatePgnHeader(String key, String value) { + final headers = state.requireValue.pgnHeaders.add(key, value); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); } Future requestServerAnalysis() { - if (state.canRequestServerAnalysis) { + if (state.requireValue.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis( - options.id as GameAnyId, - options.orientation, - ); + return service.requestAnalysis(options.gameId!, options.orientation); } return Future.error('Cannot request server analysis'); } @@ -354,8 +421,16 @@ class AnalysisController extends _$AnalysisController { } } - String makeGamePgn() { - return _root.makePgn(state.pgnHeaders, state.pgnRootComments); + /// Makes a full PGN string (including headers and comments) of the current game state. + String makeExportPgn() { + final curState = state.requireValue; + return _root.makePgn(curState.pgnHeaders, curState.pgnRootComments); + } + + /// Makes a PGN string up to the current node only. + String makeCurrentNodePgn() { + final nodes = _root.branchesOn(state.requireValue.currentPath); + return nodes.map((node) => node.sanMove.san).join(' '); } void _setPath( @@ -364,34 +439,30 @@ class AnalysisController extends _$AnalysisController { bool shouldRecomputeRootView = false, bool replaying = false, }) { - final pathChange = state.currentPath != path; + final curState = state.requireValue; + final pathChange = curState.currentPath != path; final (currentNode, opening) = _nodeOpeningAt(_root, path); // always show variation if the user plays a move - if (shouldForceShowVariation && - currentNode is Branch && - currentNode.isHidden) { + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; + if (node is Branch) node.isCollapsed = false; }); } // root view is only used to display move list, so we need to // recompute the root view only when the nodelist length changes // or a variation is hidden/shown - final rootView = shouldForceShowVariation || shouldRecomputeRootView - ? _root.view - : state.root; + final rootView = + shouldForceShowVariation || shouldRecomputeRootView ? _root.view : curState.root; - final isForward = path.size > state.currentPath.size; + final isForward = path.size > curState.currentPath.size; if (currentNode is Branch) { if (!replaying) { if (isForward) { final isCheck = currentNode.sanMove.isCheck; if (currentNode.sanMove.isCapture) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); } else { ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); } @@ -406,67 +477,96 @@ class AnalysisController extends _$AnalysisController { } if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path); + _fetchOpening(_root, path).then((value) { + if (value != null) { + final (path, opening) = value; + _updateOpening(path, opening); + } + }); } - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - lastMove: currentNode.sanMove.move, - currentBranchOpening: opening, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + ), ); } else { - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: null, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: null, + promotionMove: null, + root: rootView, + ), ); } - if (pathChange) { + if (pathChange && curState.isEngineAvailable) { _debouncedStartEngineEval(); } } - Future _fetchOpening(Node fromNode, UciPath path) async { - if (!kOpeningAllowedVariants.contains(options.variant)) return; + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + + Future<(UciPath, FullOpening)?> _fetchOpening(Node fromNode, UciPath path) async { + if (!kOpeningAllowedVariants.contains(_variant)) return null; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); - if (moves.isEmpty) return; - if (moves.length > 40) return; - - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); + if (moves.isEmpty) return null; + if (moves.length > 40) return null; + final opening = await ref.read(openingServiceProvider).fetchFromMoves(moves); if (opening != null) { - fromNode.updateAt(path, (node) => node.opening = opening); + return (path, opening); + } + return null; + } - if (state.currentPath == path) { - state = state.copyWith( - currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), - ); - } + void _updateOpening(UciPath path, FullOpening opening) { + _root.updateAt(path, (node) => node.opening = opening); + + final curState = state.requireValue; + if (curState.currentPath == path) { + _refreshCurrentNode(); } } - void _startEngineEval() { - if (!state.isEngineAvailable) return; + Future _startEngineEval() async { + final curState = state.requireValue; + if (!curState.isEngineAvailable) return; + await ref + .read(evaluationServiceProvider) + .ensureEngineInitialized(_evaluationContext, options: _evaluationOptions); ref .read(evaluationServiceProvider) .start( - state.currentPath, - _root.branchesOn(state.currentPath).map(Step.fromNode), + curState.currentPath, + _root.branchesOn(curState.currentPath).map(Step.fromNode), initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == state.currentPath, + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach( - (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), - ); + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == curState.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); } void _debouncedStartEngineEval() { @@ -478,23 +578,22 @@ class AnalysisController extends _$AnalysisController { void _stopEngineEval() { ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), - ); + _refreshCurrentNode(); } void _listenToServerAnalysisEvents() { - final event = - ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; - if (event != null && event.$1 == state.id) { + final event = ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; + if (event != null && event.$1 == state.requireValue.gameId) { _mergeOngoingAnalysis(_root, event.$2.tree); - state = state.copyWith( - acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? (white: event.$2.analysis!.white, black: event.$2.analysis!.black) - : null, - root: _root.view, + state = AsyncData( + state.requireValue.copyWith( + acplChartData: _makeAcplChartData(), + playersAnalysis: + event.$2.analysis != null + ? (white: event.$2.analysis!.white, black: event.$2.analysis!.black) + : null, + root: _root.view, + ), ); } } @@ -503,19 +602,18 @@ class AnalysisController extends _$AnalysisController { final eval = n2['eval'] as Map?; final cp = eval?['cp'] as int?; final mate = eval?['mate'] as int?; - final pgnEval = cp != null - ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) - : mate != null + final pgnEval = + cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) + : mate != null ? PgnEvaluation.mate(mate: mate) : null; final glyphs = n2['glyphs'] as List?; final glyph = glyphs?.first as Map?; final comments = n2['comments'] as List?; - final comment = - (comments?.first as Map?)?['text'] as String?; + final comment = (comments?.first as Map?)?['text'] as String?; final children = n2['children'] as List? ?? []; - final pgnComment = - pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; + final pgnComment = pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; if (n1 is Branch) { if (pgnComment != null) { if (n1.lichessAnalysisComments == null) { @@ -538,12 +636,12 @@ class AnalysisController extends _$AnalysisController { } else { final uci = n2child['uci'] as String; final san = n2child['san'] as String; - final move = Move.fromUci(uci)!; + final move = Move.parse(uci)!; n1.addChild( Branch( position: n1.position.playUnchecked(move), sanMove: SanMove(san, move), - isHidden: children.length > 1, + isCollapsed: children.length > 1, ), ); } @@ -556,50 +654,43 @@ class AnalysisController extends _$AnalysisController { } final list = _root.mainline .map( - (node) => ( - node.position.isCheckmate, - node.position.turn, - node.lichessAnalysisComments - ?.firstWhereOrNull((c) => c.eval != null) - ?.eval - ), - ) - .map( - (el) { - final (isCheckmate, side, eval) = el; - return eval != null - ? ExternalEval( + (node) => ( + node.position.isCheckmate, + node.position.turn, + node.lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval, + ), + ) + .map((el) { + final (isCheckmate, side, eval) = el; + return eval != null + ? ExternalEval( cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, mate: eval.mate, depth: eval.depth, ) - : ExternalEval( + : ExternalEval( cp: null, // hack to display checkmate as the max eval - mate: isCheckmate - ? side == Side.white - ? -1 - : 1 - : null, + mate: + isCheckmate + ? side == Side.white + ? -1 + : 1 + : null, ); - }, - ).toList(growable: false); + }) + .toList(growable: false); return list.isEmpty ? null : IList(list); } } -enum DisplayMode { - moves, - summary, -} - @freezed class AnalysisState with _$AnalysisState { const AnalysisState._(); const factory AnalysisState({ - /// Analysis ID - required StringId id, + /// The ID of the game if it's a lichess game. + required GameId? gameId, /// The variant of the analysis. required Variant variant, @@ -623,18 +714,27 @@ class AnalysisState with _$AnalysisState { /// The side to display the board from. required Side pov, - /// Whether local evaluation is allowed for this analysis. - required bool isLocalEvaluationAllowed, + /// Whether computer evaluation is allowed for this analysis. + /// + /// Acts on both local and server analysis. + required bool isComputerAnalysisAllowed, + + /// Whether the user has enabled computer analysis. + /// + /// This is a user preference and acts both on local and server analysis. + required bool isComputerAnalysisEnabled, /// Whether the user has enabled local evaluation. + /// + /// This is a user preference and acts only on local analysis. required bool isLocalEvaluationEnabled, - /// Whether to show the ACPL chart instead of tree view. - required DisplayMode displayMode, - /// The last move played. Move? lastMove, + /// Possible promotion move to be played. + NormalMove? promotionMove, + /// Opening of the analysis context (from lichess archived games). Opening? contextOpening, @@ -644,6 +744,9 @@ class AnalysisState with _$AnalysisState { /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, + /// Optional game division data, given by server analysis. + Division? division, + /// Optional ACPL chart data of the game, coming from lichess server analysis. IList? acplChartData, @@ -656,45 +759,48 @@ class AnalysisState with _$AnalysisState { IList? pgnRootComments, }) = _AnalysisState; - IMap> get validMoves => - algebraicLegalMoves(currentNode.position); + /// Whether the analysis is for a lichess game. + bool get isLichessGameAnalysis => gameId != null; + + IMap> get validMoves => + makeLegalMoves(currentNode.position, isChess960: variant == Variant.chess960); /// Whether the user can request server analysis. /// /// It must be a lichess game, which is finished and not already analyzed. bool get canRequestServerAnalysis => - id != standaloneAnalysisId && - (id.length == 8 || id.length == 12) && - !hasServerAnalysis && - pgnHeaders['Result'] != '*'; - - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + gameId != null && !hasServerAnalysis && pgnHeaders['Result'] != '*'; + /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isLocalEvaluationAllowed && - acplChartData != null && - acplChartData!.isNotEmpty); + (isComputerAnalysisAllowedAndEnabled && acplChartData != null && acplChartData!.isNotEmpty); + + bool get isComputerAnalysisAllowedAndEnabled => + isComputerAnalysisAllowed && isComputerAnalysisEnabled; + + /// Whether the engine is allowed for this analysis and variant. + bool get isEngineAllowed => + isComputerAnalysisAllowedAndEnabled && engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation - bool get isEngineAvailable => - isLocalEvaluationAllowed && - engineSupportedVariants.contains(variant) && - isLocalEvaluationEnabled; + bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; Position get position => currentNode.position; bool get canGoNext => currentNode.hasChild; bool get canGoBack => currentPath.size > UciPath.empty.size; - EngineGaugeParams get engineGaugeParams => EngineGaugeParams( - orientation: pov, - isLocalEngineAvailable: isEngineAvailable, - position: position, - savedEval: currentNode.eval ?? currentNode.serverEval, - ); + EngineGaugeParams get engineGaugeParams => ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); } @freezed @@ -743,14 +849,13 @@ class AnalysisCurrentNode with _$AnalysisCurrentNode { /// /// For now we only trust the eval coming from lichess analysis. ExternalEval? get serverEval { - final pgnEval = - lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; + final pgnEval = lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; return pgnEval != null ? ExternalEval( - cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, - mate: pgnEval.mate, - depth: pgnEval.depth, - ) + cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, + mate: pgnEval.mate, + depth: pgnEval.depth, + ) : null; } } diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 4cfc0e1daa..5b16d74da4 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -1,127 +1,129 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_preferences.freezed.dart'; part 'analysis_preferences.g.dart'; -@Riverpod(keepAlive: true) -class AnalysisPreferences extends _$AnalysisPreferences { - static const prefKey = 'preferences.analysis'; +@riverpod +class AnalysisPreferences extends _$AnalysisPreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.analysis; + // ignore: avoid_public_notifier_properties @override - AnalysisPrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - - final stored = prefs.getString(prefKey); - return stored != null - ? AnalysisPrefState.fromJson( - jsonDecode(stored) as Map, - ) - : AnalysisPrefState.defaults; + AnalysisPrefs get defaults => AnalysisPrefs.defaults; + + @override + AnalysisPrefs fromJson(Map json) => AnalysisPrefs.fromJson(json); + + @override + AnalysisPrefs build() { + return fetch(); + } + + Future toggleEnableComputerAnalysis() { + return save(state.copyWith(enableComputerAnalysis: !state.enableComputerAnalysis)); } Future toggleEnableLocalEvaluation() { - return _save( - state.copyWith( - enableLocalEvaluation: !state.enableLocalEvaluation, - ), - ); + return save(state.copyWith(enableLocalEvaluation: !state.enableLocalEvaluation)); } Future toggleShowEvaluationGauge() { - return _save( - state.copyWith( - showEvaluationGauge: !state.showEvaluationGauge, - ), - ); + return save(state.copyWith(showEvaluationGauge: !state.showEvaluationGauge)); } Future toggleAnnotations() { - return _save( - state.copyWith( - showAnnotations: !state.showAnnotations, - ), - ); + return save(state.copyWith(showAnnotations: !state.showAnnotations)); } Future togglePgnComments() { - return _save( - state.copyWith( - showPgnComments: !state.showPgnComments, - ), - ); + return save(state.copyWith(showPgnComments: !state.showPgnComments)); } Future toggleShowBestMoveArrow() { - return _save( - state.copyWith( - showBestMoveArrow: !state.showBestMoveArrow, - ), - ); + return save(state.copyWith(showBestMoveArrow: !state.showBestMoveArrow)); } Future setNumEvalLines(int numEvalLines) { - assert(numEvalLines >= 1 && numEvalLines <= 3); - return _save( - state.copyWith( - numEvalLines: numEvalLines, - ), - ); + assert(numEvalLines >= 0 && numEvalLines <= 3); + return save(state.copyWith(numEvalLines: numEvalLines)); } Future setEngineCores(int numEngineCores) { assert(numEngineCores >= 1 && numEngineCores <= maxEngineCores); - return _save( - state.copyWith( - numEngineCores: numEngineCores, - ), - ); + return save(state.copyWith(numEngineCores: numEngineCores)); } - Future _save(AnalysisPrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; + Future setEngineSearchTime(Duration engineSearchTime) { + return save(state.copyWith(engineSearchTime: engineSearchTime)); } } @Freezed(fromJson: true, toJson: true) -class AnalysisPrefState with _$AnalysisPrefState { - const AnalysisPrefState._(); +class AnalysisPrefs with _$AnalysisPrefs implements Serializable { + const AnalysisPrefs._(); - const factory AnalysisPrefState({ + const factory AnalysisPrefs({ + @JsonKey(defaultValue: true) required bool enableComputerAnalysis, required bool enableLocalEvaluation, required bool showEvaluationGauge, required bool showBestMoveArrow, required bool showAnnotations, required bool showPgnComments, - @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, - @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') - required int numEngineCores, - }) = _AnalysisPrefState; - - static final defaults = AnalysisPrefState( + @Assert('numEvalLines >= 0 && numEvalLines <= 3') required int numEvalLines, + @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, + @JsonKey( + defaultValue: _searchTimeDefault, + fromJson: _searchTimeFromJson, + toJson: _searchTimeToJson, + ) + required Duration engineSearchTime, + }) = _AnalysisPrefs; + + static const defaults = AnalysisPrefs( + enableComputerAnalysis: true, enableLocalEvaluation: true, showEvaluationGauge: true, showBestMoveArrow: true, showAnnotations: true, showPgnComments: true, numEvalLines: 2, - numEngineCores: defaultEngineCores, + numEngineCores: 1, + engineSearchTime: Duration(seconds: 10), ); - factory AnalysisPrefState.fromJson(Map json) { - try { - return _$AnalysisPrefStateFromJson(json); - } catch (_) { - return defaults; - } + factory AnalysisPrefs.fromJson(Map json) { + return _$AnalysisPrefsFromJson(json); } + + EvaluationOptions get evaluationOptions => + EvaluationOptions(multiPv: numEvalLines, cores: numEngineCores, searchTime: engineSearchTime); +} + +Duration _searchTimeDefault() { + return const Duration(seconds: 10); +} + +Duration _searchTimeFromJson(int seconds) { + return Duration(seconds: seconds); +} + +int _searchTimeToJson(Duration duration) { + return duration.inSeconds; } + +const kAvailableEngineSearchTimes = [ + Duration(seconds: 4), + Duration(seconds: 6), + Duration(seconds: 8), + Duration(seconds: 10), + Duration(seconds: 12), + Duration(seconds: 15), + Duration(seconds: 20), + Duration(seconds: 30), + Duration(hours: 1), +]; diff --git a/lib/src/model/analysis/opening_service.dart b/lib/src/model/analysis/opening_service.dart index ab850064a9..74c8a8bb9c 100644 --- a/lib/src/model/analysis/opening_service.dart +++ b/lib/src/model/analysis/opening_service.dart @@ -1,5 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/openings_database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -15,31 +16,23 @@ const kOpeningAllowedVariants = ISetConst({ }); @Riverpod(keepAlive: true) -OpeningService openingService(OpeningServiceRef ref) { +OpeningService openingService(Ref ref) { return OpeningService(ref); } class OpeningService { - OpeningService(this.ref); + OpeningService(this._ref); - final OpeningServiceRef ref; + final Ref _ref; - Future get _db => ref.read(openingsDatabaseProvider.future); + Future get _db => _ref.read(openingsDatabaseProvider.future); Future fetchFromMoves(Iterable moves) async { final db = await _db; final movesString = moves - .map( - (move) => altCastles.containsKey(move.uci) - ? altCastles[move.uci] - : move.uci, - ) + .map((move) => altCastles.containsKey(move.uci) ? altCastles[move.uci] : move.uci) .join(' '); - final list = await db.query( - 'openings', - where: 'uci = ?', - whereArgs: [movesString], - ); + final list = await db.query('openings', where: 'uci = ?', whereArgs: [movesString]); final first = list.firstOrNull; if (first != null) { diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index 6db960d91f..c7926e294b 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -2,17 +2,19 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'server_analysis_service.g.dart'; @Riverpod(keepAlive: true) -ServerAnalysisService serverAnalysisService(ServerAnalysisServiceRef ref) { +ServerAnalysisService serverAnalysisService(Ref ref) { return ServerAnalysisService(ref); } @@ -21,7 +23,7 @@ class ServerAnalysisService { (GameAnyId, StreamSubscription)? _socketSubscription; - final ServerAnalysisServiceRef ref; + final Ref ref; final _currentAnalysis = ValueNotifier(null); @@ -31,18 +33,15 @@ class ServerAnalysisService { ValueListenable get currentAnalysis => _currentAnalysis; /// The last analysis progress event received from the server. - ValueListenable<(GameAnyId, ServerEvalEvent)?> get lastAnalysisEvent => - _analysisProgress; + ValueListenable<(GameAnyId, ServerEvalEvent)?> get lastAnalysisEvent => _analysisProgress; /// Request server analysis for a game. /// /// This will return a future that completes when the server analysis is /// launched (but not when it is finished). - Future requestAnalysis(GameAnyId id, [Side? side]) async { + Future requestAnalysis(GameId id, [Side? side]) async { final socketPool = ref.read(socketPoolProvider); - final uri = id.isFullId - ? Uri(path: '/play/$id/v6') - : Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); + final uri = Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); final socketClient = socketPool.open(uri); _socketSubscription?.$2.cancel(); @@ -51,8 +50,7 @@ class ServerAnalysisService { socketClient.stream.listen( (event) { if (event.topic == 'analysisProgress') { - final data = - ServerEvalEvent.fromJson(event.data as Map); + final data = ServerEvalEvent.fromJson(event.data as Map); _analysisProgress.value = (id, data); @@ -69,13 +67,11 @@ class ServerAnalysisService { _socketSubscription = null; }, cancelOnError: true, - ) + ), ); try { - await ref.withClient( - (client) => GameRepository(client).requestServerAnalysis(id.gameId), - ); + await ref.withClient((client) => GameRepository(client).requestServerAnalysis(id.gameId)); _currentAnalysis.value = id.gameId; } catch (e) { _socketSubscription?.$2.cancel(); @@ -100,8 +96,7 @@ class CurrentAnalysis extends _$CurrentAnalysis { } void _listener() { - final gameId = - ref.read(serverAnalysisServiceProvider).currentAnalysis.value; + final gameId = ref.read(serverAnalysisServiceProvider).currentAnalysis.value; if (state != gameId) { state = gameId; } diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index c87e9c1719..52cedd91b1 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -1,7 +1,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'auth_repository.dart'; @@ -21,8 +21,7 @@ class AuthController extends _$AuthController { final appAuth = ref.read(appAuthProvider); try { - final session = await ref - .withClient((client) => AuthRepository(client, appAuth).signIn()); + final session = await ref.withClient((client) => AuthRepository(client, appAuth).signIn()); await ref.read(authSessionProvider.notifier).update(session); @@ -46,13 +45,11 @@ class AuthController extends _$AuthController { final appAuth = ref.read(appAuthProvider); try { - await ref.withClient( - (client) => AuthRepository(client, appAuth).signOut(), - ); - ref.read(notificationServiceProvider).unregister(); - // force reconnect to the current socket - ref.read(socketPoolProvider).currentClient.connect(); + await ref.read(notificationServiceProvider).unregister(); + await ref.withClient((client) => AuthRepository(client, appAuth).signOut()); await ref.read(authSessionProvider.notifier).delete(); + // force reconnect to the current socket + await ref.read(socketPoolProvider).currentClient.connect(); state = const AsyncValue.data(null); } catch (e, st) { state = AsyncValue.error(e, st); diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 3311f11a40..242d68ed45 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -1,11 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -15,16 +16,14 @@ const redirectUri = 'org.lichess.mobile://login-callback'; const oauthScopes = ['web:mobile']; @Riverpod(keepAlive: true) -FlutterAppAuth appAuth(AppAuthRef ref) { +FlutterAppAuth appAuth(Ref ref) { return const FlutterAppAuth(); } class AuthRepository { - AuthRepository( - LichessClient client, - FlutterAppAuth appAuth, - ) : _client = client, - _appAuth = appAuth; + AuthRepository(LichessClient client, FlutterAppAuth appAuth) + : _client = client, + _appAuth = appAuth; final LichessClient _client; final Logger _log = Logger('AuthRepository'); @@ -50,12 +49,6 @@ class AuthRepository { ), ); - if (authResp == null) { - throw Exception( - 'FlutterAppAuth.authorizeAndExchangeCode failed to get token', - ); - } - _log.fine('Got oAuth response $authResp'); final token = authResp.accessToken; @@ -66,25 +59,17 @@ class AuthRepository { final user = await _client.readJson( Uri(path: '/api/account'), - headers: { - 'Authorization': 'Bearer ${signBearerToken(token)}', - }, + headers: {'Authorization': 'Bearer ${signBearerToken(token)}'}, mapper: User.fromServerJson, ); - return AuthSessionState( - token: token, - user: user.lightUser, - ); + return AuthSessionState(token: token, user: user.lightUser); } Future signOut() async { final url = Uri(path: '/api/token'); final response = await _client.delete(Uri(path: '/api/token')); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to delete token: ${response.statusCode}', - url, - ); + throw http.ClientException('Failed to delete token: ${response.statusCode}', url); } } } diff --git a/lib/src/model/auth/auth_session.dart b/lib/src/model/auth/auth_session.dart index 3bf6b3508f..a5ae274926 100644 --- a/lib/src/model/auth/auth_session.dart +++ b/lib/src/model/auth/auth_session.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,11 +12,7 @@ part 'auth_session.g.dart'; class AuthSession extends _$AuthSession { @override AuthSessionState? build() { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - return ref.watch( - appInitializationProvider.select((data) => data.requireValue.userSession), - ); + return ref.read(preloadedDataProvider).requireValue.userSession; } Future update(AuthSessionState session) async { @@ -34,11 +30,8 @@ class AuthSession extends _$AuthSession { @Freezed(fromJson: true, toJson: true) class AuthSessionState with _$AuthSessionState { - const factory AuthSessionState({ - required LightUser user, - required String token, - }) = _AuthSessionState; + const factory AuthSessionState({required LightUser user, required String token}) = + _AuthSessionState; - factory AuthSessionState.fromJson(Map json) => - _$AuthSessionStateFromJson(json); + factory AuthSessionState.fromJson(Map json) => _$AuthSessionStateFromJson(json); } diff --git a/lib/src/model/auth/session_storage.dart b/lib/src/model/auth/session_storage.dart index a749963fc9..2b33ba16b7 100644 --- a/lib/src/model/auth/session_storage.dart +++ b/lib/src/model/auth/session_storage.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -8,38 +8,32 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'session_storage.g.dart'; +const kSessionStorageKey = '$kLichessHost.userSession'; + @Riverpod(keepAlive: true) -SessionStorage sessionStorage(SessionStorageRef ref) { - return SessionStorage(ref); +SessionStorage sessionStorage(Ref ref) { + return const SessionStorage(); } class SessionStorage { - const SessionStorage(SessionStorageRef ref) : _ref = ref; - - final SessionStorageRef _ref; - - FlutterSecureStorage get _storage => _ref.read(secureStorageProvider); + const SessionStorage(); Future read() async { - final string = await _storage.read(key: _kSessionStorageKey); + final string = await SecureStorage.instance.read(key: kSessionStorageKey); if (string != null) { - return AuthSessionState.fromJson( - jsonDecode(string) as Map, - ); + return AuthSessionState.fromJson(jsonDecode(string) as Map); } return null; } Future write(AuthSessionState session) async { - await _storage.write( - key: _kSessionStorageKey, + await SecureStorage.instance.write( + key: kSessionStorageKey, value: jsonEncode(session.toJson()), ); } Future delete() async { - await _storage.delete(key: _kSessionStorageKey); + await SecureStorage.instance.delete(key: kSessionStorageKey); } } - -const _kSessionStorageKey = '$kLichessHost.userSession'; diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart new file mode 100644 index 0000000000..3a6061a4ed --- /dev/null +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -0,0 +1,257 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'board_editor_controller.freezed.dart'; +part 'board_editor_controller.g.dart'; + +@riverpod +class BoardEditorController extends _$BoardEditorController { + @override + BoardEditorState build(String? initialFen) { + final setup = Setup.parseFen(initialFen ?? kInitialFEN); + return BoardEditorState( + orientation: Side.white, + sideToPlay: Side.white, + pieces: readFen(initialFen ?? kInitialFEN).lock, + castlingRights: IMap(const { + CastlingRight.whiteKing: true, + CastlingRight.whiteQueen: true, + CastlingRight.blackKing: true, + CastlingRight.blackQueen: true, + }), + editorPointerMode: EditorPointerMode.drag, + enPassantOptions: SquareSet.empty, + enPassantSquare: null, + pieceToAddOnEdit: null, + halfmoves: setup.halfmoves, + fullmoves: setup.fullmoves, + ); + } + + void updateMode(EditorPointerMode mode, [Piece? pieceToAddOnEdit]) { + state = state.copyWith(editorPointerMode: mode, pieceToAddOnEdit: pieceToAddOnEdit); + } + + void discardPiece(Square square) { + _updatePosition(state.pieces.remove(square)); + } + + void movePiece(Square? origin, Square destination, Piece piece) { + if (origin != destination) { + _updatePosition(state.pieces.remove(origin ?? destination).add(destination, piece)); + } + } + + void editSquare(Square square) { + final piece = state.pieceToAddOnEdit; + if (piece != null) { + _updatePosition(state.pieces.add(square, piece)); + } else { + discardPiece(square); + } + } + + void flipBoard() { + state = state.copyWith(orientation: state.orientation.opposite); + } + + void setSideToPlay(Side side) { + state = state.copyWith( + sideToPlay: side, + enPassantOptions: _calculateEnPassantOptions(state.pieces, side), + ); + } + + void loadFen(String fen) { + _updatePosition(readFen(fen).lock); + } + + /// Calculates the squares where an en passant capture could be possible. + SquareSet _calculateEnPassantOptions(IMap pieces, Side side) { + SquareSet enPassantOptions = SquareSet.empty; + final boardFen = writeFen(pieces.unlock); + final board = Board.parseFen(boardFen); + + /// For en passant to be possible, there needs to be an adjacent pawn which has moved two squares forward. + /// So the two squares behind must be empty + void checkEnPassant(Square square, int fileOffset) { + final adjacentSquare = Square.fromCoords(square.file.offset(fileOffset)!, square.rank); + final targetSquare = Square.fromCoords( + square.file.offset(fileOffset)!, + square.rank.offset(side == Side.white ? 1 : -1)!, + ); + final originSquare = Square.fromCoords( + square.file.offset(fileOffset)!, + square.rank.offset(side == Side.white ? 2 : -2)!, + ); + + if (board.sideAt(adjacentSquare) == side.opposite && + board.roleAt(adjacentSquare) == Role.pawn && + board.sideAt(targetSquare) == null && + board.sideAt(originSquare) == null) { + enPassantOptions = enPassantOptions.union(SquareSet.fromSquare(targetSquare)); + } + } + + pieces.forEach((square, piece) { + if (piece.color == side && piece.role == Role.pawn) { + if ((side == Side.white && square.rank == Rank.fifth) || + (side == Side.black && square.rank == Rank.fourth)) { + if (square.file != File.a) checkEnPassant(square, -1); + if (square.file != File.h) checkEnPassant(square, 1); + } + } + }); + + return enPassantOptions; + } + + void toggleEnPassantSquare(Square square) { + state = state.copyWith(enPassantSquare: state.enPassantSquare == square ? null : square); + } + + void _updatePosition(IMap pieces) { + state = state.copyWith( + pieces: pieces, + enPassantOptions: _calculateEnPassantOptions(pieces, state.sideToPlay), + ); + } + + void setCastling(Side side, CastlingSide castlingSide, bool allowed) { + switch (side) { + case Side.white: + switch (castlingSide) { + case CastlingSide.king: + state = state.copyWith( + castlingRights: state.castlingRights.add(CastlingRight.whiteKing, allowed), + ); + case CastlingSide.queen: + state = state.copyWith( + castlingRights: state.castlingRights.add(CastlingRight.whiteQueen, allowed), + ); + } + case Side.black: + switch (castlingSide) { + case CastlingSide.king: + state = state.copyWith( + castlingRights: state.castlingRights.add(CastlingRight.blackKing, allowed), + ); + case CastlingSide.queen: + state = state.copyWith( + castlingRights: state.castlingRights.add(CastlingRight.blackQueen, allowed), + ); + } + } + } +} + +enum CastlingRight { whiteKing, whiteQueen, blackKing, blackQueen } + +@freezed +class BoardEditorState with _$BoardEditorState { + const BoardEditorState._(); + + const factory BoardEditorState({ + required Side orientation, + required Side sideToPlay, + required IMap pieces, + required IMap castlingRights, + required EditorPointerMode editorPointerMode, + required SquareSet enPassantOptions, + required Square? enPassantSquare, + required int halfmoves, + required int fullmoves, + + /// When null, clears squares when in edit mode. Has no effect in drag mode. + required Piece? pieceToAddOnEdit, + }) = _BoardEditorState; + + bool isCastlingAllowed(Side side, CastlingSide castlingSide) => switch (side) { + Side.white => switch (castlingSide) { + CastlingSide.king => castlingRights[CastlingRight.whiteKing]!, + CastlingSide.queen => castlingRights[CastlingRight.whiteQueen]!, + }, + Side.black => switch (castlingSide) { + CastlingSide.king => castlingRights[CastlingRight.blackKing]!, + CastlingSide.queen => castlingRights[CastlingRight.blackQueen]!, + }, + }; + + /// Returns the castling rights part of the FEN string. + /// + /// If the rook is missing on one side of the king, or the king is missing on the + /// backrank, the castling right is removed. + String get _castlingRightsPart { + final parts = []; + final Map hasRook = {}; + final Board board = Board.parseFen(writeFen(pieces.unlock)); + for (final side in Side.values) { + final backrankKing = SquareSet.backrankOf(side) & board.kings; + final rooksAndKings = + (board.bySide(side) & SquareSet.backrankOf(side)) & (board.rooks | board.kings); + for (final castlingSide in CastlingSide.values) { + final candidate = + castlingSide == CastlingSide.king + ? rooksAndKings.squares.lastOrNull + : rooksAndKings.squares.firstOrNull; + final isCastlingPossible = + candidate != null && board.rooks.has(candidate) && backrankKing.singleSquare != null; + switch ((side, castlingSide)) { + case (Side.white, CastlingSide.king): + hasRook[CastlingRight.whiteKing] = isCastlingPossible; + case (Side.white, CastlingSide.queen): + hasRook[CastlingRight.whiteQueen] = isCastlingPossible; + case (Side.black, CastlingSide.king): + hasRook[CastlingRight.blackKing] = isCastlingPossible; + case (Side.black, CastlingSide.queen): + hasRook[CastlingRight.blackQueen] = isCastlingPossible; + } + } + } + for (final right in CastlingRight.values) { + if (hasRook[right]! && castlingRights[right]!) { + switch (right) { + case CastlingRight.whiteKing: + parts.add('K'); + case CastlingRight.whiteQueen: + parts.add('Q'); + case CastlingRight.blackKing: + parts.add('k'); + case CastlingRight.blackQueen: + parts.add('q'); + } + } + } + return parts.isEmpty ? '-' : parts.join(''); + } + + Piece? get activePieceOnEdit => + editorPointerMode == EditorPointerMode.edit ? pieceToAddOnEdit : null; + + bool get deletePiecesActive => + editorPointerMode == EditorPointerMode.edit && pieceToAddOnEdit == null; + + String get fen { + final boardFen = writeFen(pieces.unlock); + return '$boardFen ${sideToPlay == Side.white ? 'w' : 'b'} $_castlingRightsPart ${enPassantSquare?.name ?? '-'} $halfmoves $fullmoves'; + } + + /// Returns the PGN representation of the current position if it is valid. + /// + /// Returns `null` if the position is invalid. + String? get pgn { + try { + final position = Chess.fromSetup(Setup.parseFen(fen)); + return PgnGame( + headers: {'FEN': position.fen}, + moves: PgnNode(), + comments: [], + ).makePgn(); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index ac8ccdb1e8..9188498756 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -5,19 +5,16 @@ import 'package:lichess_mobile/src/model/common/id.dart'; part 'broadcast.freezed.dart'; -typedef BroadcastsList = ({ - IList active, - IList upcoming, - IList past, - int? nextPage, -}); +typedef BroadcastList = ({IList active, IList past, int? nextPage}); + +enum BroadcastResult { whiteWins, blackWins, draw, ongoing, noResultPgnTag } @freezed class Broadcast with _$Broadcast { const Broadcast._(); const factory Broadcast({ - required BroadcastTournament tour, + required BroadcastTournamentData tour, required BroadcastRound round, required String? group, @@ -32,55 +29,130 @@ class Broadcast with _$Broadcast { String get title => group ?? tour.name; } -typedef BroadcastTournament = ({ - String name, - String? imageUrl, -}); +@freezed +class BroadcastTournament with _$BroadcastTournament { + const factory BroadcastTournament({ + required BroadcastTournamentData data, + required IList rounds, + required BroadcastRoundId defaultRoundId, + required IList? group, + }) = _BroadcastTournament; +} @freezed -class BroadcastRound with _$BroadcastRound { - const BroadcastRound._(); +class BroadcastTournamentData with _$BroadcastTournamentData { + const factory BroadcastTournamentData({ + required BroadcastTournamentId id, + required String name, + required String slug, + required String? imageUrl, + required String? description, + // PRIVATE=-1, NORMAL=3, HIGH=4, BEST=5 + int? tier, + required BroadcastTournamentInformation information, + }) = _BroadcastTournamentData; +} + +typedef BroadcastTournamentInformation = + ({ + String? format, + String? timeControl, + String? players, + String? location, + BroadcastTournamentDates? dates, + Uri? website, + }); +typedef BroadcastTournamentDates = ({DateTime startsAt, DateTime? endsAt}); + +typedef BroadcastTournamentGroup = ({BroadcastTournamentId id, String name}); + +@freezed +class BroadcastRound with _$BroadcastRound { const factory BroadcastRound({ required BroadcastRoundId id, required String name, + required String slug, required RoundStatus status, - required DateTime startsAt, + required DateTime? startsAt, + required DateTime? finishedAt, + required bool startsAfterPrevious, }) = _BroadcastRound; } -typedef BroadcastRoundGames = IMap; +typedef BroadcastRoundWithGames = ({BroadcastRound round, BroadcastRoundGames games}); + +typedef BroadcastRoundGames = IMap; @freezed -class BroadcastGameSnapshot with _$BroadcastGameSnapshot { - const BroadcastGameSnapshot._(); +class BroadcastGame with _$BroadcastGame { + const BroadcastGame._(); - const factory BroadcastGameSnapshot({ + const factory BroadcastGame({ + required BroadcastGameId id, required IMap players, required String fen, required Move? lastMove, - required String status, - - /// The amount of time that the player whose turn it is has been thinking since his last move - required Duration? thinkTime, - }) = _BroadcastGameSnapshot; + required BroadcastResult status, + required DateTime updatedClockAt, + }) = _BroadcastGame; + + bool get isOngoing => status == BroadcastResult.ongoing; + bool get isOver => + status == BroadcastResult.draw || + status == BroadcastResult.whiteWins || + status == BroadcastResult.blackWins; + Side get sideToMove => Setup.parseFen(fen).turn; } @freezed class BroadcastPlayer with _$BroadcastPlayer { - const BroadcastPlayer._(); - const factory BroadcastPlayer({ required String name, required String? title, required int? rating, required Duration? clock, required String? federation, + required FideId? fideId, }) = _BroadcastPlayer; } -enum RoundStatus { - live, - finished, - upcoming, +@freezed +class BroadcastPlayerExtended with _$BroadcastPlayerExtended { + const factory BroadcastPlayerExtended({ + required String name, + required String? title, + required int? rating, + required String? federation, + required FideId? fideId, + required int played, + required double? score, + required int? ratingDiff, + required int? performance, + }) = _BroadcastPlayerExtended; } + +typedef BroadcastFideData = ({({int? standard, int? rapid, int? blitz}) ratings, int? birthYear}); + +typedef BroadcastPlayerResults = + ({ + BroadcastPlayerExtended player, + BroadcastFideData fideData, + IList games, + }); + +enum BroadcastPoints { one, half, zero } + +@freezed +class BroadcastPlayerResultData with _$BroadcastPlayerResultData { + const factory BroadcastPlayerResultData({ + required BroadcastRoundId roundId, + required BroadcastGameId gameId, + required Side color, + required BroadcastPoints? points, + required int? ratingDiff, + required BroadcastPlayer opponent, + }) = _BroadcastPlayerResult; +} + +enum RoundStatus { live, finished, upcoming } diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart new file mode 100644 index 0000000000..267b53fc69 --- /dev/null +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -0,0 +1,205 @@ +const federationIdToName = { + 'FID': 'FIDE', + 'USA': 'United States of America', + 'IND': 'India', + 'CHN': 'China', + 'RUS': 'Russia', + 'AZE': 'Azerbaijan', + 'FRA': 'France', + 'UKR': 'Ukraine', + 'ARM': 'Armenia', + 'GER': 'Germany', + 'ESP': 'Spain', + 'NED': 'Netherlands', + 'HUN': 'Hungary', + 'POL': 'Poland', + 'ENG': 'England', + 'ROU': 'Romania', + 'NOR': 'Norway', + 'UZB': 'Uzbekistan', + 'ISR': 'Israel', + 'CZE': 'Czech Republic', + 'SRB': 'Serbia', + 'CRO': 'Croatia', + 'GRE': 'Greece', + 'IRI': 'Iran', + 'TUR': 'Turkiye', + 'SLO': 'Slovenia', + 'ARG': 'Argentina', + 'SWE': 'Sweden', + 'GEO': 'Georgia', + 'ITA': 'Italy', + 'CUB': 'Cuba', + 'AUT': 'Austria', + 'PER': 'Peru', + 'BUL': 'Bulgaria', + 'BRA': 'Brazil', + 'DEN': 'Denmark', + 'SUI': 'Switzerland', + 'CAN': 'Canada', + 'SVK': 'Slovakia', + 'LTU': 'Lithuania', + 'VIE': 'Vietnam', + 'AUS': 'Australia', + 'BEL': 'Belgium', + 'MNE': 'Montenegro', + 'MDA': 'Moldova', + 'KAZ': 'Kazakhstan', + 'ISL': 'Iceland', + 'COL': 'Colombia', + 'BIH': 'Bosnia & Herzegovina', + 'EGY': 'Egypt', + 'FIN': 'Finland', + 'MGL': 'Mongolia', + 'PHI': 'Philippines', + 'BLR': 'Belarus', + 'LAT': 'Latvia', + 'POR': 'Portugal', + 'CHI': 'Chile', + 'MEX': 'Mexico', + 'MKD': 'North Macedonia', + 'INA': 'Indonesia', + 'PAR': 'Paraguay', + 'EST': 'Estonia', + 'SGP': 'Singapore', + 'SCO': 'Scotland', + 'VEN': 'Venezuela', + 'IRL': 'Ireland', + 'URU': 'Uruguay', + 'TKM': 'Turkmenistan', + 'MAR': 'Morocco', + 'MAS': 'Malaysia', + 'BAN': 'Bangladesh', + 'ALG': 'Algeria', + 'RSA': 'South Africa', + 'AND': 'Andorra', + 'ALB': 'Albania', + 'KGZ': 'Kyrgyzstan', + 'KOS': 'Kosovo *', + 'FAI': 'Faroe Islands', + 'ZAM': 'Zambia', + 'MYA': 'Myanmar', + 'NZL': 'New Zealand', + 'ECU': 'Ecuador', + 'CRC': 'Costa Rica', + 'NGR': 'Nigeria', + 'JPN': 'Japan', + 'SYR': 'Syria', + 'DOM': 'Dominican Republic', + 'LUX': 'Luxembourg', + 'WLS': 'Wales', + 'BOL': 'Bolivia', + 'TUN': 'Tunisia', + 'UAE': 'United Arab Emirates', + 'MNC': 'Monaco', + 'TJK': 'Tajikistan', + 'PAN': 'Panama', + 'LBN': 'Lebanon', + 'NCA': 'Nicaragua', + 'ESA': 'El Salvador', + 'ANG': 'Angola', + 'TTO': 'Trinidad & Tobago', + 'SRI': 'Sri Lanka', + 'IRQ': 'Iraq', + 'JOR': 'Jordan', + 'UGA': 'Uganda', + 'MAD': 'Madagascar', + 'ZIM': 'Zimbabwe', + 'MLT': 'Malta', + 'SUD': 'Sudan', + 'KOR': 'South Korea', + 'PUR': 'Puerto Rico', + 'HON': 'Honduras', + 'GUA': 'Guatemala', + 'PAK': 'Pakistan', + 'JAM': 'Jamaica', + 'THA': 'Thailand', + 'YEM': 'Yemen', + 'LBA': 'Libya', + 'CYP': 'Cyprus', + 'NEP': 'Nepal', + 'HKG': 'Hong Kong, China', + 'SSD': 'South Sudan', + 'BOT': 'Botswana', + 'PLE': 'Palestine', + 'KEN': 'Kenya', + 'AHO': 'Netherlands Antilles', + 'MAW': 'Malawi', + 'LIE': 'Liechtenstein', + 'TPE': 'Chinese Taipei', + 'AFG': 'Afghanistan', + 'MOZ': 'Mozambique', + 'KSA': 'Saudi Arabia', + 'BAR': 'Barbados', + 'NAM': 'Namibia', + 'HAI': 'Haiti', + 'ARU': 'Aruba', + 'CIV': 'Cote d’Ivoire', + 'CPV': 'Cape Verde', + 'SUR': 'Suriname', + 'LBR': 'Liberia', + 'IOM': 'Isle of Man', + 'MTN': 'Mauritania', + 'BRN': 'Bahrain', + 'GHA': 'Ghana', + 'OMA': 'Oman', + 'BRU': 'Brunei Darussalam', + 'GCI': 'Guernsey', + 'GUM': 'Guam', + 'KUW': 'Kuwait', + 'JCI': 'Jersey', + 'MRI': 'Mauritius', + 'SEN': 'Senegal', + 'BAH': 'Bahamas', + 'MDV': 'Maldives', + 'NRU': 'Nauru', + 'TOG': 'Togo', + 'FIJ': 'Fiji', + 'PLW': 'Palau', + 'GUY': 'Guyana', + 'LES': 'Lesotho', + 'CAY': 'Cayman Islands', + 'SOM': 'Somalia', + 'SWZ': 'Eswatini', + 'TAN': 'Tanzania', + 'LCA': 'Saint Lucia', + 'ISV': 'US Virgin Islands', + 'SLE': 'Sierra Leone', + 'BER': 'Bermuda', + 'SMR': 'San Marino', + 'BDI': 'Burundi', + 'QAT': 'Qatar', + 'ETH': 'Ethiopia', + 'DJI': 'Djibouti', + 'SEY': 'Seychelles', + 'PNG': 'Papua New Guinea', + 'DMA': 'Dominica', + 'STP': 'Sao Tome and Principe', + 'MAC': 'Macau', + 'CAM': 'Cambodia', + 'VIN': 'Saint Vincent and the Grenadines', + 'BUR': 'Burkina Faso', + 'COM': 'Comoros Islands', + 'GAB': 'Gabon', + 'RWA': 'Rwanda', + 'CMR': 'Cameroon', + 'MLI': 'Mali', + 'ANT': 'Antigua and Barbuda', + 'CHA': 'Chad', + 'GAM': 'Gambia', + 'COD': 'Democratic Republic of the Congo', + 'SKN': 'Saint Kitts and Nevis', + 'BHU': 'Bhutan', + 'NIG': 'Niger', + 'GRN': 'Grenada', + 'BIZ': 'Belize', + 'CAF': 'Central African Republic', + 'ERI': 'Eritrea', + 'GEQ': 'Equatorial Guinea', + 'IVB': 'British Virgin Islands', + 'LAO': 'Laos', + 'SOL': 'Solomon Islands', + 'TGA': 'Tonga', + 'TLS': 'Timor-Leste', + 'VAN': 'Vanuatu', +}; diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart new file mode 100644 index 0000000000..1a13c90547 --- /dev/null +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -0,0 +1,631 @@ +import 'dart:async'; + +import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_controller.freezed.dart'; +part 'broadcast_game_controller.g.dart'; + +@riverpod +class BroadcastGameController extends _$BroadcastGameController implements PgnTreeNotifier { + static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => + Uri(path: 'study/$broadcastRoundId/socket/v6'); + + AppLifecycleListener? _appLifecycleListener; + StreamSubscription? _subscription; + StreamSubscription? _socketOpenSubscription; + + late SocketClient _socketClient; + late Root _root; + + final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + final _syncDebouncer = Debouncer(const Duration(milliseconds: 150)); + + Timer? _startEngineEvalTimer; + + Object? _key = Object(); + + @override + Future build(BroadcastRoundId roundId, BroadcastGameId gameId) async { + _socketClient = ref + .watch(socketPoolProvider) + .open(BroadcastGameController.broadcastSocketUri(roundId)); + + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + await _socketClient.firstConnection; + + _socketOpenSubscription = _socketClient.connectedStream.listen((_) { + if (state.valueOrNull?.isOngoing == true) { + _syncDebouncer(() { + _reloadPgn(); + }); + } + }); + + final evaluationService = ref.watch(evaluationServiceProvider); + + _appLifecycleListener = AppLifecycleListener( + onResume: () { + if (state.valueOrNull?.isOngoing == true) { + _syncDebouncer(() { + _reloadPgn(); + }); + } + }, + ); + + ref.onDispose(() { + _key = null; + _subscription?.cancel(); + _socketOpenSubscription?.cancel(); + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + evaluationService.disposeEngine(); + _appLifecycleListener?.dispose(); + _syncDebouncer.dispose(); + }); + + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), + ); + + final game = PgnGame.parsePgn(pgn); + final pgnHeaders = IMap(game.headers); + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + _root = Root.fromPgnGame(game); + final currentPath = _root.mainlinePath; + final currentNode = _root.nodeAt(currentPath); + final lastMove = _root.branchAt(_root.mainlinePath)?.sanMove.move; + + // don't use ref.watch here: we don't want to invalidate state when the + // analysis preferences change + final prefs = ref.read(analysisPreferencesProvider); + final broadcastState = BroadcastGameState( + id: gameId, + currentPath: currentPath, + broadcastPath: currentPath, + isOnMainline: _root.isOnMainline(currentPath), + root: _root.view, + currentNode: AnalysisCurrentNode.fromNode(currentNode), + pgnHeaders: pgnHeaders, + pgnRootComments: rootComments, + lastMove: lastMove, + pov: Side.white, + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + clocks: _getClocks(currentPath), + ); + + if (broadcastState.isLocalEvaluationEnabled) { + evaluationService.initEngine(_evaluationContext, options: _evaluationOptions).then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); + } + + return broadcastState; + } + + Future _reloadPgn() async { + if (!state.hasValue) return; + final key = _key; + + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), + ); + + // check provider is still mounted + if (key == _key) { + final curState = state.requireValue; + final wasOnLivePath = curState.broadcastLivePath == curState.currentPath; + final game = PgnGame.parsePgn(pgn); + final pgnHeaders = IMap(game.headers); + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + final newRoot = Root.fromPgnGame(game); + + final broadcastPath = newRoot.mainlinePath; + final lastMove = newRoot.branchAt(newRoot.mainlinePath)?.sanMove.move; + + newRoot.merge(_root); + + _root = newRoot; + + final newCurrentPath = wasOnLivePath ? broadcastPath : curState.currentPath; + state = AsyncData( + state.requireValue.copyWith( + currentPath: newCurrentPath, + pgnHeaders: pgnHeaders, + pgnRootComments: rootComments, + broadcastPath: broadcastPath, + root: _root.view, + lastMove: lastMove, + clocks: _getClocks(newCurrentPath), + ), + ); + } + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) return; + + switch (event.topic) { + // Sent when a node is recevied from the broadcast + case 'addNode': + _handleAddNodeEvent(event); + // Sent when a pgn tag changes + case 'setTags': + _handleSetTagsEvent(event); + } + } + + void _handleAddNodeEvent(SocketEvent event) { + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received + final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + + // We check that the event we received is for the last move of the game + if (currentPath.value != '!') return; + + // The path for the node that was received + final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); + final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); + final clock = pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); + + final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); + + if (newPath != null && isNewNode == false) { + _root.updateAt(newPath, (node) { + if (node is Branch) { + node.comments = [...node.comments ?? [], PgnComment(clock: clock)]; + } + }); + } + + if (newPath != null) { + _root.promoteAt(newPath, toMainline: true); + _setPath( + (state.requireValue.broadcastPath == state.requireValue.currentPath) + ? newPath + : state.requireValue.currentPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + broadcastPath: newPath, + ); + } + } + + void _handleSetTagsEvent(SocketEvent event) { + final broadcastGameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + final pgnHeadersEntries = pick( + event.data, + 'tags', + ).asListOrThrow((header) => MapEntry(header(0).asStringOrThrow(), header(1).asStringOrThrow())); + + final pgnHeaders = state.requireValue.pgnHeaders.addEntries(pgnHeadersEntries); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: pgnHeaders)); + } + + EvaluationContext get _evaluationContext => + EvaluationContext(variant: Variant.standard, initialPosition: _root.position); + + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + + void onUserMove(NormalMove move) { + if (!state.hasValue) return; + + if (!state.requireValue.position.isLegal(move)) return; + + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncData(state.requireValue.copyWith(promotionMove: move)); + return; + } + + final (newPath, isNewNode) = _root.addMoveAt(state.requireValue.currentPath, move); + if (newPath != null) { + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); + } + } + + void onPromotionSelection(Role? role) { + if (!state.hasValue) return; + + if (role == null) { + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.requireValue.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void userNext() { + if (!state.hasValue) return; + + if (!state.requireValue.currentNode.hasChild) return; + _setPath( + state.requireValue.currentPath + + _root.nodeAt(state.requireValue.currentPath).children.first.id, + replaying: true, + ); + } + + void jumpToNthNodeOnMainline(int n) { + UciPath path = _root.mainlinePath; + while (!path.penultimate.isEmpty) { + path = path.penultimate; + } + Node? node = _root.nodeAt(path); + int count = 0; + + while (node != null && count < n) { + if (node.children.isNotEmpty) { + path = path + node.children.first.id; + node = _root.nodeAt(path); + count++; + } else { + break; + } + } + + if (node != null) { + userJump(path); + } + } + + void toggleBoard() { + if (!state.hasValue) return; + + state = AsyncData(state.requireValue.copyWith(pov: state.requireValue.pov.opposite)); + } + + void userPrevious() { + _setPath(state.requireValue.currentPath.penultimate, replaying: true); + } + + @override + void userJump(UciPath path) { + _setPath(path); + } + + @override + void expandVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + for (final child in node.children) { + child.isCollapsed = false; + for (final grandChild in child.children) { + grandChild.isCollapsed = false; + } + } + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void collapseVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isCollapsed = true; + } + + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void promoteVariation(UciPath path, bool toMainline) { + if (!state.hasValue) return; + + _root.promoteAt(path, toMainline: toMainline); + state = AsyncData( + state.requireValue.copyWith( + isOnMainline: _root.isOnMainline(state.requireValue.currentPath), + root: _root.view, + ), + ); + } + + @override + void deleteFromHere(UciPath path) { + _root.deleteAt(path); + _setPath(path.penultimate, shouldRecomputeRootView: true); + } + + Future toggleLocalEvaluation() async { + if (!state.hasValue) return; + + ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); + + state = AsyncData( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), + ); + + if (state.requireValue.isLocalEvaluationEnabled) { + await ref + .read(evaluationServiceProvider) + .initEngine(_evaluationContext, options: _evaluationOptions); + _startEngineEval(); + } else { + _stopEngineEval(); + ref.read(evaluationServiceProvider).disposeEngine(); + } + } + + void setNumEvalLines(int numEvalLines) { + if (!state.hasValue) return; + + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _root.updateAll((node) => node.eval = null); + + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + + _startEngineEval(); + } + + void setEngineCores(int numEngineCores) { + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _startEngineEval(); + } + + void setEngineSearchTime(Duration searchTime) { + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _startEngineEval(); + } + + void _setPath( + UciPath path, { + bool shouldForceShowVariation = false, + bool shouldRecomputeRootView = false, + bool replaying = false, + UciPath? broadcastPath, + }) { + if (!state.hasValue) return; + + final pathChange = state.requireValue.currentPath != path; + final currentNode = _root.nodeAt(path); + + // always show variation if the user plays a move + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { + _root.updateAt(path, (node) { + if (node is Branch) node.isCollapsed = false; + }); + } + + // root view is only used to display move list, so we need to + // recompute the root view only when the nodelist length changes + // or a variation is hidden/shown + final rootView = + shouldForceShowVariation || shouldRecomputeRootView ? _root.view : state.requireValue.root; + + final isForward = path.size > state.requireValue.currentPath.size; + if (currentNode is Branch) { + if (!replaying) { + if (isForward) { + final isCheck = currentNode.sanMove.isCheck; + if (currentNode.sanMove.isCapture) { + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } + } else if (isForward) { + final soundService = ref.read(soundServiceProvider); + if (currentNode.sanMove.isCapture) { + soundService.play(Sound.capture); + } else { + soundService.play(Sound.move); + } + } + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastPath: broadcastPath ?? state.requireValue.broadcastPath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + clocks: _getClocks(path), + ), + ); + } else { + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastPath: broadcastPath ?? state.requireValue.broadcastPath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + lastMove: null, + promotionMove: null, + root: rootView, + clocks: _getClocks(path), + ), + ); + } + + if (pathChange && state.requireValue.isLocalEvaluationEnabled) { + _debouncedStartEngineEval(); + } + } + + Future _startEngineEval() async { + if (!state.hasValue) return; + + if (!state.requireValue.isLocalEvaluationEnabled) return; + await ref + .read(evaluationServiceProvider) + .ensureEngineInitialized(_evaluationContext, options: _evaluationOptions); + ref + .read(evaluationServiceProvider) + .start( + state.requireValue.currentPath, + _root.branchesOn(state.requireValue.currentPath).map(Step.fromNode), + initialPositionEval: _root.eval, + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, + ) + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); + } + + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + + void _debouncedStartEngineEval() { + _engineEvalDebounce(() { + _startEngineEval(); + }); + } + + void _stopEngineEval() { + if (!state.hasValue) return; + + ref.read(evaluationServiceProvider).stop(); + // update the current node with last cached eval + _refreshCurrentNode(); + } + + ({Duration? parentClock, Duration? clock}) _getClocks(UciPath path) { + final node = _root.nodeAt(path); + final parent = _root.parentAt(path); + + return ( + parentClock: (parent is Branch) ? parent.clock : null, + clock: (node is Branch) ? node.clock : null, + ); + } +} + +@freezed +class BroadcastGameState with _$BroadcastGameState { + const BroadcastGameState._(); + + const factory BroadcastGameState({ + /// Broadcast game ID + required StringId id, + + /// Immutable view of the whole tree + required ViewRoot root, + + /// The current node in the analysis view. + /// + /// This is an immutable copy of the actual [Node] at the `currentPath`. + /// We don't want to use [Node.view] here because it'd copy the whole tree + /// under the current node and it's expensive. + required AnalysisCurrentNode currentNode, + + /// The path to the current node in the analysis view. + required UciPath currentPath, + + /// The path to the last broadcast move. + required UciPath broadcastPath, + + /// Whether the current path is on the mainline. + required bool isOnMainline, + + /// The side to display the board from. + required Side pov, + + /// Whether the user has enabled local evaluation. + required bool isLocalEvaluationEnabled, + + /// Clocks if available. + ({Duration? parentClock, Duration? clock})? clocks, + + /// The last move played. + Move? lastMove, + + /// Possible promotion move to be played. + NormalMove? promotionMove, + + /// The PGN headers of the game. + required IMap pgnHeaders, + + /// The PGN comments of the game. + /// + /// This field is only used with user submitted PGNS. + IList? pgnRootComments, + }) = _BroadcastGameState; + + IMap> get validMoves => makeLegalMoves(currentNode.position); + + Position get position => currentNode.position; + bool get canGoNext => currentNode.hasChild; + bool get canGoBack => currentPath.size > UciPath.empty.size; + + /// Whether the game is still ongoing + bool get isOngoing => pgnHeaders['Result'] == '*'; + + /// The path to the current broadcast live move + UciPath? get broadcastLivePath => isOngoing ? broadcastPath : null; + + EngineGaugeParams get engineGaugeParams => ( + orientation: pov, + isLocalEngineAvailable: isLocalEvaluationEnabled, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); +} diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 0642c913e6..c8696d35eb 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,6 +1,10 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_providers.g.dart'; @@ -10,7 +14,7 @@ part 'broadcast_providers.g.dart'; @riverpod class BroadcastsPaginator extends _$BroadcastsPaginator { @override - Future build() async { + Future build() async { final broadcastList = await ref.withClient( (client) => BroadcastRepository(client).getBroadcasts(), ); @@ -22,21 +26,60 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { final broadcastList = state.requireValue; final nextPage = broadcastList.nextPage; - if (nextPage != null && nextPage > 20) return; + if (nextPage == null || nextPage > 20) return; state = const AsyncLoading(); final broadcastListNewPage = await ref.withClient( - (client) => BroadcastRepository(client).getBroadcasts(page: nextPage!), + (client) => BroadcastRepository(client).getBroadcasts(page: nextPage), ); - state = AsyncData( - ( - active: broadcastList.active, - upcoming: broadcastList.upcoming, - past: broadcastList.past.addAll(broadcastListNewPage.past), - nextPage: broadcastListNewPage.nextPage, - ), - ); + state = AsyncData(( + active: broadcastList.active, + past: broadcastList.past.addAll(broadcastListNewPage.past), + nextPage: broadcastListNewPage.nextPage, + )); + } +} + +@riverpod +Future broadcastTournament( + Ref ref, + BroadcastTournamentId broadcastTournamentId, +) { + return ref.withClient( + (client) => BroadcastRepository(client).getTournament(broadcastTournamentId), + ); +} + +@riverpod +Future> broadcastPlayers( + Ref ref, + BroadcastTournamentId tournamentId, +) { + return ref.withClient((client) => BroadcastRepository(client).getPlayers(tournamentId)); +} + +@riverpod +Future broadcastPlayerResult( + Ref ref, + BroadcastTournamentId broadcastTournamentId, + String playerId, +) { + return ref.withClient( + (client) => BroadcastRepository(client).getPlayerResults(broadcastTournamentId, playerId), + ); +} + +@Riverpod(keepAlive: true) +BroadcastImageWorkerFactory broadcastImageWorkerFactory(Ref ref) { + return const BroadcastImageWorkerFactory(); +} + +class BroadcastImageWorkerFactory { + const BroadcastImageWorkerFactory(); + + Future spawn() async { + return ImageColorWorker.spawn(); } } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 05c6b6e7fb..832e0e3c8a 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -3,8 +3,8 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; class BroadcastRepository { @@ -12,105 +12,259 @@ class BroadcastRepository { final LichessClient client; - Future getBroadcasts({int page = 1}) { + Future getBroadcasts({int page = 1}) { return client.readJson( - Uri( - path: '/api/broadcast/top', - queryParameters: {'page': page.toString()}, - ), - headers: {'Accept': 'application/json'}, + Uri(path: '/api/broadcast/top', queryParameters: {'page': page.toString()}), mapper: _makeBroadcastResponseFromJson, ); } - Future getRound( - BroadcastRoundId broadcastRoundId, - ) { + Future getTournament(BroadcastTournamentId broadcastTournamentId) { + return client.readJson( + Uri(path: 'api/broadcast/$broadcastTournamentId'), + mapper: _makeTournamentFromJson, + ); + } + + Future getRound(BroadcastRoundId broadcastRoundId) { return client.readJson( Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), - // The path parameters with - are the broadcast tournament and round slug + // The path parameters with - are the broadcast tournament and round slugs // They are only used for SEO, so we can safely use - for these parameters - headers: {'Accept': 'application/x-ndjson'}, - mapper: _makeGamesFromJson, + mapper: _makeRoundWithGamesFromJson, + ); + } + + Future getGamePgn(BroadcastRoundId roundId, BroadcastGameId gameId) { + return client.read(Uri(path: 'api/study/$roundId/$gameId.pgn')); + } + + Future> getPlayers(BroadcastTournamentId tournamentId) { + return client.readJsonList( + Uri(path: '/broadcast/$tournamentId/players'), + mapper: _makePlayerFromJson, + ); + } + + Future getPlayerResults( + BroadcastTournamentId tournamentId, + String playerId, + ) { + return client.readJson( + Uri(path: 'broadcast/$tournamentId/players/$playerId'), + mapper: _makePlayerResultsFromJson, ); } } -BroadcastsList _makeBroadcastResponseFromJson( - Map json, -) { +BroadcastList _makeBroadcastResponseFromJson(Map json) { return ( active: pick(json, 'active').asListOrThrow(_broadcastFromPick).toIList(), - upcoming: - pick(json, 'upcoming').asListOrThrow(_broadcastFromPick).toIList(), - past: pick(json, 'past', 'currentPageResults') - .asListOrThrow(_broadcastFromPick) - .toIList(), + past: pick(json, 'past', 'currentPageResults').asListOrThrow(_broadcastFromPick).toIList(), nextPage: pick(json, 'past', 'nextPage').asIntOrNull(), ); } Broadcast _broadcastFromPick(RequiredPick pick) { - final live = pick('round', 'ongoing').asBoolOrFalse(); - final finished = pick('round', 'finished').asBoolOrFalse(); - final status = live - ? RoundStatus.live - : finished - ? RoundStatus.finished - : RoundStatus.upcoming; final roundId = pick('round', 'id').asBroadcastRoundIdOrThrow(); return Broadcast( - tour: ( - name: pick('tour', 'name').asStringOrThrow(), - imageUrl: pick('tour', 'image').asStringOrNull(), - ), - round: BroadcastRound( - id: roundId, - name: pick('round', 'name').asStringOrThrow(), - status: status, - startsAt: pick('round', 'startsAt') - .asDateTimeFromMillisecondsOrThrow() - .toLocal(), - ), + tour: _tournamentDataFromPick(pick('tour').required()), + round: _roundFromPick(pick('round').required()), group: pick('group').asStringOrNull(), - roundToLinkId: - pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + roundToLinkId: pick('roundToLink', 'id').asBroadcastRoundIdOrNull() ?? roundId, ); } -BroadcastRoundGames _makeGamesFromJson(Map json) => - _gamesFromPick(pick(json).required()); - -BroadcastRoundGames _gamesFromPick( - RequiredPick pick, -) => - IMap.fromEntries(pick('games').asListOrThrow(gameFromPick)); - -MapEntry gameFromPick( - RequiredPick pick, -) => - MapEntry( - pick('id').asBroadcastGameIdOrThrow(), - BroadcastGameSnapshot( - players: IMap({ - Side.white: _playerFromPick(pick('players', 0).required()), - Side.black: _playerFromPick(pick('players', 1).required()), - }), - fen: pick('fen').asStringOrNull() ?? - Variant.standard.initialPosition.fen, - lastMove: pick('lastMove').asUciMoveOrNull(), - status: pick('status').asStringOrThrow(), - thinkTime: pick('thinkTime').asDurationFromSecondsOrNull(), +BroadcastTournamentData _tournamentDataFromPick(RequiredPick pick) => BroadcastTournamentData( + id: pick('id').asBroadcastTournamentIdOrThrow(), + name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), + tier: pick('tier').asIntOrNull(), + imageUrl: pick('image').asStringOrNull(), + description: pick('description').asStringOrNull(), + information: ( + format: pick('info', 'format').asStringOrNull(), + timeControl: pick('info', 'tc').asStringOrNull(), + players: pick('info', 'players').asStringOrNull(), + location: pick('info', 'location').asStringOrNull(), + dates: pick('dates').letOrNull( + (pick) => ( + startsAt: pick(0).asDateTimeFromMillisecondsOrThrow(), + endsAt: pick(1).asDateTimeFromMillisecondsOrNull(), ), - ); + ), + website: pick('info', 'website').letOrNull((p) => Uri.tryParse(p.asStringOrThrow())), + ), +); + +BroadcastTournament _makeTournamentFromJson(Map json) { + return BroadcastTournament( + data: _tournamentDataFromPick(pick(json, 'tour').required()), + rounds: pick(json, 'rounds').asListOrThrow(_roundFromPick).toIList(), + defaultRoundId: pick(json, 'defaultRoundId').asBroadcastRoundIdOrThrow(), + group: pick(json, 'group', 'tours').asListOrNull(_tournamentGroupFromPick)?.toIList(), + ); +} + +BroadcastTournamentGroup _tournamentGroupFromPick(RequiredPick pick) { + final id = pick('id').asBroadcastTournamentIdOrThrow(); + final name = pick('name').asStringOrThrow(); + + return (id: id, name: name); +} + +BroadcastRound _roundFromPick(RequiredPick pick) { + final live = pick('ongoing').asBoolOrFalse(); + final finished = pick('finished').asBoolOrFalse(); + final status = + live + ? RoundStatus.live + : finished + ? RoundStatus.finished + : RoundStatus.upcoming; + + return BroadcastRound( + id: pick('id').asBroadcastRoundIdOrThrow(), + name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), + status: status, + startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), + finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), + startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), + ); +} -BroadcastPlayer _playerFromPick(RequiredPick pick) { +BroadcastRoundWithGames _makeRoundWithGamesFromJson(Map json) { + final round = pick(json, 'round').required(); + final games = pick(json, 'games').required(); + return (round: _roundFromPick(round), games: _gamesFromPick(games)); +} + +BroadcastRoundGames _gamesFromPick(RequiredPick pick) => + IMap.fromEntries(pick.asListOrThrow(gameFromPick)); + +MapEntry gameFromPick(RequiredPick pick) { + final stringStatus = pick('status').asStringOrNull(); + + final status = + (stringStatus == null) + ? BroadcastResult.noResultPgnTag + : switch (stringStatus) { + '½-½' => BroadcastResult.draw, + '1-0' => BroadcastResult.whiteWins, + '0-1' => BroadcastResult.blackWins, + '*' => BroadcastResult.ongoing, + _ => + throw FormatException( + "value $stringStatus can't be interpreted as a broadcast result", + ), + }; + + /// The amount of time that the player whose turn it is has been thinking since his last move + final thinkTime = pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero; + final fen = pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen; + final playingSide = Setup.parseFen(fen).turn; + + return MapEntry( + pick('id').asBroadcastGameIdOrThrow(), + BroadcastGame( + id: pick('id').asBroadcastGameIdOrThrow(), + players: IMap({ + Side.white: _playerFromPick( + pick('players', 0).required(), + isPlaying: playingSide == Side.white, + thinkingTime: thinkTime, + ), + Side.black: _playerFromPick( + pick('players', 1).required(), + isPlaying: playingSide == Side.black, + thinkingTime: thinkTime, + ), + }), + fen: pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen, + lastMove: pick('lastMove').asUciMoveOrNull(), + status: status, + updatedClockAt: DateTime.now(), + ), + ); +} + +BroadcastPlayer _playerFromPick( + RequiredPick pick, { + required bool isPlaying, + required Duration thinkingTime, +}) { + final clock = pick('clock').asDurationFromCentiSecondsOrNull(); + final updatedClock = clock != null && isPlaying ? clock - thinkingTime : clock; return BroadcastPlayer( name: pick('name').asStringOrThrow(), title: pick('title').asStringOrNull(), rating: pick('rating').asIntOrNull(), - clock: pick('clock').asDurationFromCentiSecondsOrNull(), + clock: updatedClock, federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + ); +} + +BroadcastPlayerExtended _makePlayerFromJson(Map json) { + return _playerExtendedFromPick(pick(json).required()); +} + +BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { + return BroadcastPlayerExtended( + name: pick('name').asStringOrThrow(), + title: pick('title').asStringOrNull(), + rating: pick('rating').asIntOrNull(), + federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + played: pick('played').asIntOrThrow(), + score: pick('score').asDoubleOrNull(), + ratingDiff: pick('ratingDiff').asIntOrNull(), + performance: pick('performance').asIntOrNull(), + ); +} + +BroadcastPlayerResults _makePlayerResultsFromJson(Map json) { + return ( + player: _playerExtendedFromPick(pick(json).required()), + fideData: _fideDataFromPick(pick(json, 'fide')), + games: pick(json, 'games').asListOrThrow(_makePlayerResultFromPick).toIList(), + ); +} + +BroadcastFideData _fideDataFromPick(Pick pick) { + return ( + ratings: ( + standard: pick('ratings', 'standard').asIntOrNull(), + rapid: pick('ratings', 'rapid').asIntOrNull(), + blitz: pick('ratings', 'blitz').asIntOrNull(), + ), + birthYear: pick('year').asIntOrNull(), + ); +} + +BroadcastPlayerResultData _makePlayerResultFromPick(RequiredPick pick) { + final pointsString = pick('points').asStringOrNull(); + BroadcastPoints? points; + if (pointsString == '1') { + points = BroadcastPoints.one; + } else if (pointsString == '1/2') { + points = BroadcastPoints.half; + } else if (pointsString == '0') { + points = BroadcastPoints.zero; + } + + return BroadcastPlayerResultData( + roundId: pick('round').asBroadcastRoundIdOrThrow(), + gameId: pick('id').asBroadcastGameIdOrThrow(), + color: pick('color').asSideOrThrow(), + ratingDiff: pick('ratingDiff').asIntOrNull(), + points: points, + opponent: _playerFromPick( + pick('opponent').required(), + isPlaying: false, + thinkingTime: Duration.zero, + ), ); } diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index d14118d8e2..4596c417bc 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -3,13 +3,16 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_round_controller.g.dart'; @@ -20,24 +23,64 @@ class BroadcastRoundController extends _$BroadcastRoundController { Uri(path: 'study/$broadcastRoundId/socket/v6'); StreamSubscription? _subscription; + StreamSubscription? _socketOpenSubscription; + AppLifecycleListener? _appLifecycleListener; late SocketClient _socketClient; + final _debouncer = Debouncer(const Duration(milliseconds: 150)); + + Object? _key = Object(); + @override - Future build(BroadcastRoundId broadcastRoundId) async { + Future build(BroadcastRoundId broadcastRoundId) async { _socketClient = ref - .read(socketPoolProvider) + .watch(socketPoolProvider) .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); _subscription = _socketClient.stream.listen(_handleSocketEvent); + await _socketClient.firstConnection; + + _socketOpenSubscription = _socketClient.connectedStream.listen((_) { + if (state.valueOrNull?.round.status == RoundStatus.live) { + _debouncer(() { + _syncRound(); + }); + } + }); + + _appLifecycleListener = AppLifecycleListener( + onResume: () { + if (state.valueOrNull?.round.status == RoundStatus.live) { + _debouncer(() { + _syncRound(); + }); + } + }, + ); + ref.onDispose(() { + _key = null; _subscription?.cancel(); + _socketOpenSubscription?.cancel(); + _appLifecycleListener?.dispose(); + _debouncer.dispose(); }); - return await ref.withClient( + return ref.withClient((client) => BroadcastRepository(client).getRound(broadcastRoundId)); + } + + Future _syncRound() async { + if (state.hasValue == false) return; + final key = _key; + final round = await ref.withClient( (client) => BroadcastRepository(client).getRound(broadcastRoundId), ); + // check provider is still mounted + if (key == _key) { + state = AsyncData(round); + } } void _handleSocketEvent(SocketEvent event) { @@ -47,7 +90,10 @@ class BroadcastRoundController extends _$BroadcastRoundController { // Sent when a node is recevied from the broadcast case 'addNode': _handleAddNodeEvent(event); - // Sent when a game ends + // Sent when a new board is added + case 'addChapter': + _handleAddChapterEvent(event); + // Sent when the state of games changes case 'chapters': _handleChaptersEvent(event); // Sent when clocks are updated from the broadcast @@ -58,71 +104,69 @@ class BroadcastRoundController extends _$BroadcastRoundController { void _handleAddNodeEvent(SocketEvent event) { // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - // The path for the node that was received - final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); - final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); // We check that the event we received is for the last move of the game - if (currentPath != path + nodeId) return; + if (currentPath.value != '!') return; - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); final fen = pick(event.data, 'n', 'fen').asStringOrThrow(); - final playingSide = Setup.parseFen(fen).turn.opposite; + final playingSide = Setup.parseFen(fen).turn; - state = AsyncData( - state.requireValue.update( + state = AsyncData(( + round: state.requireValue.round, + games: state.requireValue.games.update( broadcastGameId, - (broadcastGameSnapshot) => broadcastGameSnapshot.copyWith( - players: IMap( - { - playingSide: broadcastGameSnapshot.players[playingSide]!.copyWith( - clock: pick(event.data, 'n', 'clock') - .asDurationFromCentiSecondsOrNull(), - ), - playingSide.opposite: - broadcastGameSnapshot.players[playingSide.opposite]!, - }, - ), + (broadcastGame) => broadcastGame.copyWith( + players: IMap({ + playingSide: broadcastGame.players[playingSide]!, + playingSide.opposite: broadcastGame.players[playingSide.opposite]!.copyWith( + clock: pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(), + ), + }), fen: fen, lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - thinkTime: null, + updatedClockAt: DateTime.now(), ), ), - ); + )); + } + + void _handleAddChapterEvent(SocketEvent event) { + ref.invalidateSelf(); } void _handleChaptersEvent(SocketEvent event) { final games = pick(event.data).asListOrThrow(gameFromPick); - state = AsyncData(IMap.fromEntries(games)); + state = AsyncData((round: state.requireValue.round, games: IMap.fromEntries(games))); } void _handleClockEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - final whiteClock = pick(event.data, 'p', 'relayClocks', 0) - .asDurationFromCentiSecondsOrNull(); - final blackClock = pick(event.data, 'p', 'relayClocks', 1) - .asDurationFromCentiSecondsOrNull(); - state = AsyncData( - state.requireValue.update( + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final relayClocks = pick(event.data, 'p', 'relayClocks'); + + // We check that the clocks for the broadcast game preview have been updated else we do nothing + if (relayClocks.value == null) return; + + state = AsyncData(( + round: state.requireValue.round, + games: state.requireValue.games.update( broadcastGameId, - (broadcastGameSnapshot) => broadcastGameSnapshot.copyWith( - players: IMap( - { - Side.white: broadcastGameSnapshot.players[Side.white]!.copyWith( - clock: whiteClock, - ), - Side.black: broadcastGameSnapshot.players[Side.black]!.copyWith( - clock: blackClock, - ), - }, - ), + (broadcastsGame) => broadcastsGame.copyWith( + players: IMap({ + Side.white: broadcastsGame.players[Side.white]!.copyWith( + clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), + ), + Side.black: broadcastsGame.players[Side.black]!.copyWith( + clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), + ), + }), + updatedClockAt: DateTime.now(), ), ), - ); + )); } } diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index d56420b316..78dea6630f 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -1,17 +1,17 @@ import 'package:deep_pick/deep_pick.dart'; -import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/json.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; part 'challenge.freezed.dart'; +part 'challenge.g.dart'; abstract mixin class BaseChallenge { Variant get variant; @@ -23,29 +23,22 @@ abstract mixin class BaseChallenge { SideChoice get sideChoice; String? get initialFen; - TimeIncrement? get timeIncrement => clock != null - ? TimeIncrement(clock!.time.inSeconds, clock!.increment.inSeconds) - : null; + TimeIncrement? get timeIncrement => + clock != null ? TimeIncrement(clock!.time.inSeconds, clock!.increment.inSeconds) : null; Perf get perf => Perf.fromVariantAndSpeed( - variant, - timeIncrement != null - ? Speed.fromTimeIncrement(timeIncrement!) - : Speed.correspondence, - ); + variant, + timeIncrement != null ? Speed.fromTimeIncrement(timeIncrement!) : Speed.correspondence, + ); } /// A challenge already created server-side. -@freezed +@Freezed(fromJson: true, toJson: true) class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { const Challenge._(); - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set but not both.', - ) const factory Challenge({ - required int socketVersion, + int? socketVersion, required ChallengeId id, GameFullId? gameFullId, required ChallengeStatus status, @@ -58,29 +51,69 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { required SideChoice sideChoice, ChallengeUser? challenger, ChallengeUser? destUser, - DeclineReason? declineReason, + ChallengeDeclineReason? declineReason, String? initialFen, ChallengeDirection? direction, }) = _Challenge; + factory Challenge.fromJson(Map json) => _$ChallengeFromJson(json); + factory Challenge.fromServerJson(Map json) { return _challengeFromPick(pick(json).required()); } factory Challenge.fromPick(RequiredPick pick) => _challengeFromPick(pick); + + /// The description of the challenge. + String description(AppLocalizations l10n) { + if (!variant.isPlaySupported) { + // TODO: l10n + return 'This variant is not yet supported on the app.'; + } + + final time = switch (timeControl) { + ChallengeTimeControlType.clock => () { + final minutes = switch (clock!.time.inSeconds) { + 15 => '¼', + 30 => '½', + 45 => '¾', + 90 => '1.5', + _ => clock!.time.inMinutes, + }; + return '$minutes+${clock!.increment.inSeconds}'; + }(), + ChallengeTimeControlType.correspondence => '${l10n.daysPerTurn}: $days', + ChallengeTimeControlType.unlimited => '∞', + }; + + final variantStr = variant == Variant.standard ? '' : ' • ${variant.label}'; + + final sidePiece = + sideChoice == SideChoice.black + ? '♔ ' + : sideChoice == SideChoice.white + ? '♚ ' + : ''; + + final side = + sideChoice == SideChoice.black + ? l10n.white + : sideChoice == SideChoice.white + ? l10n.black + : l10n.randomColor; + + final mode = rated ? l10n.rated : l10n.casual; + + return '$sidePiece$side • $mode • $time$variantStr'; + } } /// A challenge request to play a game with another user. @freezed -class ChallengeRequest - with _$ChallengeRequest, BaseChallenge - implements BaseChallenge { +class ChallengeRequest with _$ChallengeRequest, BaseChallenge implements BaseChallenge { const ChallengeRequest._(); - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set but not both.', - ) + @Assert('clock != null || days != null', 'Either clock or days must be set but not both.') const factory ChallengeRequest({ required LightUser destUser, required Variant variant, @@ -90,48 +123,36 @@ class ChallengeRequest required bool rated, required SideChoice sideChoice, String? initialFen, - }) = _ChallengeSetup; + }) = _ChallengeRequest; @override Speed get speed => Speed.fromTimeIncrement( - TimeIncrement( - clock != null ? clock!.time.inSeconds : 0, - clock != null ? clock!.increment.inSeconds : 0, - ), - ); + TimeIncrement( + clock != null ? clock!.time.inSeconds : 0, + clock != null ? clock!.increment.inSeconds : 0, + ), + ); - Map get toRequestBody => { - if (clock != null) 'clock.limit': clock!.time.inSeconds.toString(), - if (clock != null) - 'clock.increment': clock!.increment.inSeconds.toString(), - if (days != null) 'days': days.toString(), - 'rated': variant == Variant.fromPosition ? 'false' : rated.toString(), - 'variant': variant.name, - if (variant == Variant.fromPosition) 'fen': initialFen, - if (sideChoice != SideChoice.random) 'color': sideChoice.name, - }; + Map get toRequestBody { + return { + if (clock != null) 'clock.limit': clock!.time.inSeconds.toString(), + if (clock != null) 'clock.increment': clock!.increment.inSeconds.toString(), + if (days != null) 'days': days.toString(), + 'rated': variant == Variant.fromPosition ? 'false' : rated.toString(), + 'variant': variant.name, + if (variant == Variant.fromPosition) 'fen': initialFen, + if (sideChoice != SideChoice.random) 'color': sideChoice.name, + }; + } } -enum ChallengeDirection { - outward, - inward, -} +enum ChallengeDirection { outward, inward } -enum ChallengeStatus { - created, - offline, - canceled, - declined, - accepted, -} +enum ChallengeStatus { created, offline, canceled, declined, accepted } -enum ChallengeTimeControlType { - unlimited, - clock, - correspondence, -} +enum ChallengeTimeControlType { unlimited, clock, correspondence } -enum DeclineReason { +enum ChallengeDeclineReason { generic, later, tooFast, @@ -142,58 +163,24 @@ enum DeclineReason { standard, variant, noBot, - onlyBot, -} - -String declineReasonMessage(BuildContext context, DeclineReason key) { - switch (key) { - case DeclineReason.generic: - return context.l10n.challengeDeclineGeneric; - case DeclineReason.later: - return context.l10n.challengeDeclineLater; - case DeclineReason.tooFast: - return context.l10n.challengeDeclineTooFast; - case DeclineReason.tooSlow: - return context.l10n.challengeDeclineTooSlow; - case DeclineReason.timeControl: - return context.l10n.challengeDeclineTimeControl; - case DeclineReason.rated: - return context.l10n.challengeDeclineRated; - case DeclineReason.casual: - return context.l10n.challengeDeclineCasual; - case DeclineReason.standard: - return context.l10n.challengeDeclineStandard; - case DeclineReason.variant: - return context.l10n.challengeDeclineVariant; - case DeclineReason.noBot: - return context.l10n.challengeDeclineNoBot; - case DeclineReason.onlyBot: - return context.l10n.challengeDeclineOnlyBot; - } -} - -typedef ChallengeUser = ({ - LightUser user, - bool? provisionalRating, - int? lagRating, -}); + onlyBot; -enum SideChoice { - random, - white, - black, + String label(AppLocalizations l10n) => switch (this) { + ChallengeDeclineReason.generic => l10n.challengeDeclineGeneric, + ChallengeDeclineReason.later => l10n.challengeDeclineLater, + ChallengeDeclineReason.tooFast => l10n.challengeDeclineTooFast, + ChallengeDeclineReason.tooSlow => l10n.challengeDeclineTooSlow, + ChallengeDeclineReason.timeControl => l10n.challengeDeclineTimeControl, + ChallengeDeclineReason.rated => l10n.challengeDeclineRated, + ChallengeDeclineReason.casual => l10n.challengeDeclineCasual, + ChallengeDeclineReason.standard => l10n.challengeDeclineStandard, + ChallengeDeclineReason.variant => l10n.challengeDeclineVariant, + ChallengeDeclineReason.noBot => l10n.challengeDeclineNoBot, + ChallengeDeclineReason.onlyBot => l10n.challengeDeclineOnlyBot, + }; } -String sideChoiceL10n(AppLocalizations l10n, SideChoice side) { - switch (side) { - case SideChoice.white: - return l10n.white; - case SideChoice.black: - return l10n.black; - case SideChoice.random: - return l10n.randomColor; - } -} +typedef ChallengeUser = ({LightUser user, int? rating, bool? provisionalRating, int? lagRating}); extension ChallengeExtension on Pick { ChallengeDirection asChallengeDirectionOrThrow() { @@ -203,9 +190,9 @@ extension ChallengeExtension on Pick { } if (value is String) { switch (value) { - case 'outward': + case 'outward' || 'out': return ChallengeDirection.outward; - case 'inward': + case 'inward' || 'in': return ChallengeDirection.inward; default: throw PickException( @@ -213,9 +200,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeDirection", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeDirection"); } ChallengeDirection? asChallengeDirectionOrNull() { @@ -250,9 +235,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeStatus", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeStatus"); } ChallengeStatus? asChallengeStatusOrNull() { @@ -316,9 +299,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to SideChoice", - ); + throw PickException("value $value at $debugParsingExit can't be casted to SideChoice"); } SideChoice? asSideChoiceOrNull() { @@ -330,47 +311,27 @@ extension ChallengeExtension on Pick { } } - DeclineReason asDeclineReasonOrThrow() { + ChallengeDeclineReason asDeclineReasonOrThrow() { final value = this.required().value; - if (value is DeclineReason) { + if (value is ChallengeDeclineReason) { return value; } if (value is String) { - switch (value) { - case 'generic': - return DeclineReason.generic; - case 'later': - return DeclineReason.later; - case 'tooFast': - return DeclineReason.tooFast; - case 'tooSlow': - return DeclineReason.tooSlow; - case 'timeControl': - return DeclineReason.timeControl; - case 'rated': - return DeclineReason.rated; - case 'casual': - return DeclineReason.casual; - case 'standard': - return DeclineReason.standard; - case 'variant': - return DeclineReason.variant; - case 'noBot': - return DeclineReason.noBot; - case 'onlyBot': - return DeclineReason.onlyBot; - default: - throw PickException( - "value $value at $debugParsingExit can't be casted to DeclineReason: invalid string.", - ); - } + return ChallengeDeclineReason.values.firstWhere( + (element) => element.name.toLowerCase() == value, + orElse: + () => + throw PickException( + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", + ), + ); } throw PickException( - "value $value at $debugParsingExit can't be casted to DeclineReason", + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason", ); } - DeclineReason? asDeclineReasonOrNull() { + ChallengeDeclineReason? asDeclineReasonOrNull() { if (value == null) return null; try { return asDeclineReasonOrThrow(); @@ -382,46 +343,39 @@ extension ChallengeExtension on Pick { Challenge _challengeFromPick(RequiredPick pick) { return Challenge( - socketVersion: pick('socketVersion').asIntOrThrow(), + socketVersion: pick('socketVersion').asIntOrNull(), id: pick('id').asChallengeIdOrThrow(), gameFullId: pick('fullId').asGameFullIdOrNull(), status: pick('status').asChallengeStatusOrThrow(), variant: pick('variant').asVariantOrThrow(), speed: pick('speed').asSpeedOrThrow(), - timeControl: - pick('timeControl', 'type').asChallengeTimeControlTypeOrThrow(), - clock: pick('timeControl').letOrThrow( - (clockPick) { - final time = clockPick('limit').asDurationFromSecondsOrNull(); - final increment = clockPick('increment').asDurationFromSecondsOrNull(); - return time != null && increment != null - ? (time: time, increment: increment) - : null; - }, - ), + timeControl: pick('timeControl', 'type').asChallengeTimeControlTypeOrThrow(), + clock: pick('timeControl').letOrThrow((clockPick) { + final time = clockPick('limit').asDurationFromSecondsOrNull(); + final increment = clockPick('increment').asDurationFromSecondsOrNull(); + return time != null && increment != null ? (time: time, increment: increment) : null; + }), days: pick('timeControl', 'daysPerTurn').asIntOrNull(), rated: pick('rated').asBoolOrThrow(), sideChoice: pick('color').asSideChoiceOrThrow(), - challenger: pick('challenger').letOrNull( - (challengerPick) { - final challengerUser = pick('challenger').asLightUserOrThrow(); - return ( - user: challengerUser, - provisionalRating: challengerPick('provisional').asBoolOrNull(), - lagRating: challengerPick('lag').asIntOrNull(), - ); - }, - ), - destUser: pick('destUser').letOrNull( - (destPick) { - final destUser = pick('destUser').asLightUserOrThrow(); - return ( - user: destUser, - provisionalRating: destPick('provisional').asBoolOrNull(), - lagRating: destPick('lag').asIntOrNull(), - ); - }, - ), + challenger: pick('challenger').letOrNull((challengerPick) { + final challengerUser = pick('challenger').asLightUserOrThrow(); + return ( + user: challengerUser, + rating: challengerPick('rating').asIntOrNull(), + provisionalRating: challengerPick('provisional').asBoolOrNull(), + lagRating: challengerPick('lag').asIntOrNull(), + ); + }), + destUser: pick('destUser').letOrNull((destPick) { + final destUser = pick('destUser').asLightUserOrThrow(); + return ( + user: destUser, + rating: destPick('rating').asIntOrNull(), + provisionalRating: destPick('provisional').asBoolOrNull(), + lagRating: destPick('lag').asIntOrNull(), + ); + }), initialFen: pick('initialFen').asStringOrNull(), direction: pick('direction').asChallengeDirectionOrNull(), declineReason: pick('declineReasonKey').asDeclineReasonOrNull(), diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index e0abd038e1..dbbe66db28 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -1,84 +1,73 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_preferences.freezed.dart'; part 'challenge_preferences.g.dart'; -@Riverpod(keepAlive: true) -class ChallengePreferences extends _$ChallengePreferences { - static String _prefKey(AuthSessionState? session) => - 'preferences.game_setup.${session?.user.id ?? '**anon**'}'; +@riverpod +class ChallengePreferences extends _$ChallengePreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + PrefCategory get prefCategory => PrefCategory.challenge; @override - ChallengePreferencesState build() { - final session = ref.watch(authSessionProvider); - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey(session)); - return stored != null - ? ChallengePreferencesState.fromJson( - jsonDecode(stored) as Map, - ) - : ChallengePreferencesState.defaults; + ChallengePrefs defaults({LightUser? user}) => ChallengePrefs.defaults; + + @override + ChallengePrefs fromJson(Map json) => ChallengePrefs.fromJson(json); + + @override + ChallengePrefs build() { + return fetch(); } Future setVariant(Variant variant) { - return _save(state.copyWith(variant: variant)); + return save(state.copyWith(variant: variant)); } Future setTimeControl(ChallengeTimeControlType timeControl) { - return _save(state.copyWith(timeControl: timeControl)); + return save(state.copyWith(timeControl: timeControl)); } Future setClock(Duration time, Duration increment) { - return _save(state.copyWith(clock: (time: time, increment: increment))); + return save(state.copyWith(clock: (time: time, increment: increment))); } Future setDays(int days) { - return _save(state.copyWith(days: days)); + return save(state.copyWith(days: days)); } Future setSideChoice(SideChoice sideChoice) { - return _save(state.copyWith(sideChoice: sideChoice)); + return save(state.copyWith(sideChoice: sideChoice)); } Future setRated(bool rated) { - return _save(state.copyWith(rated: rated)); - } - - Future _save(ChallengePreferencesState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - final session = ref.read(authSessionProvider); - await prefs.setString( - _prefKey(session), - jsonEncode(newState.toJson()), - ); - state = newState; + return save(state.copyWith(rated: rated)); } } @Freezed(fromJson: true, toJson: true) -class ChallengePreferencesState with _$ChallengePreferencesState { - const ChallengePreferencesState._(); +class ChallengePrefs with _$ChallengePrefs implements Serializable { + const ChallengePrefs._(); - const factory ChallengePreferencesState({ + const factory ChallengePrefs({ required Variant variant, required ChallengeTimeControlType timeControl, required ({Duration time, Duration increment}) clock, required int days, required bool rated, required SideChoice sideChoice, - }) = _ChallengeSetup; + }) = _ChallengePrefs; - static const defaults = ChallengePreferencesState( + static const defaults = ChallengePrefs( variant: Variant.standard, timeControl: ChallengeTimeControlType.clock, clock: (time: Duration(minutes: 10), increment: Duration.zero), @@ -87,33 +76,29 @@ class ChallengePreferencesState with _$ChallengePreferencesState { sideChoice: SideChoice.random, ); - Speed get speed => timeControl == ChallengeTimeControlType.clock - ? Speed.fromTimeIncrement( - TimeIncrement( - clock.time.inSeconds, - clock.increment.inSeconds, - ), - ) - : Speed.correspondence; + Speed get speed => + timeControl == ChallengeTimeControlType.clock + ? Speed.fromTimeIncrement(TimeIncrement(clock.time.inSeconds, clock.increment.inSeconds)) + : Speed.correspondence; ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { return ChallengeRequest( destUser: destUser, variant: variant, timeControl: timeControl, - clock: clock, - days: days, + clock: timeControl == ChallengeTimeControlType.clock ? clock : null, + days: timeControl == ChallengeTimeControlType.correspondence ? days : null, rated: rated, sideChoice: sideChoice, initialFen: initialFen, ); } - factory ChallengePreferencesState.fromJson(Map json) { + factory ChallengePrefs.fromJson(Map json) { try { - return _$ChallengePreferencesStateFromJson(json); + return _$ChallengePrefsFromJson(json); } catch (_) { - return ChallengePreferencesState.defaults; + return ChallengePrefs.defaults; } } } diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index d0cf8e653c..7510bae576 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,14 +1,22 @@ +import 'dart:async'; + import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenge_repository.g.dart'; -typedef ChallengesList = ({ - IList inward, - IList outward, -}); +@Riverpod(keepAlive: true) +ChallengeRepository challengeRepository(Ref ref) { + return ChallengeRepository(ref.read(lichessClientProvider)); +} + +typedef ChallengesList = ({IList inward, IList outward}); class ChallengeRepository { const ChallengeRepository(this.client); @@ -31,10 +39,7 @@ class ChallengeRepository { Future show(ChallengeId id) async { final uri = Uri(path: '/api/challenge/$id/show'); - return client.readJson( - uri, - mapper: Challenge.fromServerJson, - ); + return client.readJson(uri, mapper: Challenge.fromServerJson); } Future create(ChallengeRequest challenge) async { @@ -51,22 +56,16 @@ class ChallengeRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to accept challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to accept challenge: ${response.statusCode}', uri); } } - Future decline(ChallengeId id) async { + Future decline(ChallengeId id, {ChallengeDeclineReason? reason}) async { final uri = Uri(path: '/api/challenge/$id/decline'); - final response = await client.post(uri); + final response = await client.post(uri, body: reason != null ? {'reason': reason.name} : null); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to decline challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to decline challenge: ${response.statusCode}', uri); } } @@ -75,10 +74,7 @@ class ChallengeRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to cancel challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to cancel challenge: ${response.statusCode}', uri); } } } diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart new file mode 100644 index 0000000000..29d7b1c68a --- /dev/null +++ b/lib/src/model/challenge/challenge_service.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:stream_transform/stream_transform.dart'; + +part 'challenge_service.g.dart'; + +@Riverpod(keepAlive: true) +ChallengeService challengeService(Ref ref) { + final service = ChallengeService(ref); + ref.onDispose(() => service.dispose()); + return service; +} + +/// A service that listens to challenge events and shows notifications. +class ChallengeService { + ChallengeService(this.ref); + + final Ref ref; + + ChallengesList? _current; + ChallengesList? _previous; + + StreamSubscription? _socketSubscription; + + /// The stream of challenge events that are received from the server. + static Stream get stream => + socketGlobalStream.map((event) { + if (event.topic != 'challenges') return null; + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + return (inward: inward.lock, outward: outward.lock); + }).whereNotNull(); + + /// Start listening to challenge events from the server. + void start() { + _socketSubscription = stream.listen(_onSocketEvent); + } + + void _onSocketEvent(ChallengesList current) { + _previous = _current; + _current = current; + + _sendNotifications(); + } + + Future _sendNotifications() async { + final notificationService = ref.read(notificationServiceProvider); + + final Iterable prevInwardIds = + _previous?.inward.map((challenge) => challenge.id) ?? []; + final Iterable currentInwardIds = + _current?.inward.map((challenge) => challenge.id) ?? []; + + // challenges that were canceled by challenger or expired + await Future.wait( + prevInwardIds + .whereNot((challengeId) => currentInwardIds.contains(challengeId)) + .map((id) async => await notificationService.cancel(id.value.hashCode)), + ); + + // new incoming challenges + await Future.wait( + _current?.inward.whereNot((challenge) => prevInwardIds.contains(challenge.id)).map(( + challenge, + ) async { + return await notificationService.show(ChallengeNotification(challenge)); + }) ?? + >[], + ); + } + + /// Stop listening to challenge events from the server. + void dispose() { + _socketSubscription?.cancel(); + } + + /// Handle a local notification response when the app is in the foreground. + Future onNotificationResponse(String? actionid, Challenge challenge) async { + final challengeId = challenge.id; + + switch (actionid) { + case 'accept': + final repo = ref.read(challengeRepositoryProvider); + await repo.accept(challengeId); + + final fullId = await repo.show(challengeId).then((challenge) => challenge.gameFullId); + + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; + + final rootNavState = Navigator.of(context, rootNavigator: true); + if (rootNavState.canPop()) { + rootNavState.popUntil((route) => route.isFirst); + } + + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) => GameScreen(initialGameId: fullId), + ); + + case 'decline': + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; + showAdaptiveActionSheet( + context: context, + actions: + ChallengeDeclineReason.values + .map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + onPressed: (_) { + final repo = ref.read(challengeRepositoryProvider); + repo.decline(challengeId, reason: reason); + }, + ), + ) + .toList(), + ); + + case null: + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + pushPlatformRoute( + context, + builder: (BuildContext context) => const ChallengeRequestsScreen(), + ); + } + } +} diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart new file mode 100644 index 0000000000..8fa8c495c7 --- /dev/null +++ b/lib/src/model/challenge/challenges.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenges.g.dart'; + +@riverpod +class Challenges extends _$Challenges { + StreamSubscription? _subscription; + + @override + Future build() async { + _subscription = ChallengeService.stream.listen((list) => state = AsyncValue.data(list)); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + final session = ref.watch(authSessionProvider); + if (session == null) { + return Future.value(( + inward: const IList.empty(), + outward: const IList.empty(), + )); + } + + return ref.read(challengeRepositoryProvider).list(); + } +} diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart new file mode 100644 index 0000000000..02e75bfcf0 --- /dev/null +++ b/lib/src/model/clock/chess_clock.dart @@ -0,0 +1,183 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; + +const _emergencyDelay = Duration(seconds: 20); +const _tickDelay = Duration(milliseconds: 100); + +/// A chess clock. +class ChessClock { + ChessClock({ + required Duration whiteTime, + required Duration blackTime, + this.emergencyThreshold, + this.onFlag, + this.onEmergency, + }) : _whiteTime = ValueNotifier(whiteTime), + _blackTime = ValueNotifier(blackTime), + _activeSide = Side.white; + + /// The threshold at which the clock will call [onEmergency] if provided. + final Duration? emergencyThreshold; + + /// Callback when the clock reaches zero. + VoidCallback? onFlag; + + /// Called when one clock timers reaches the emergency threshold. + final void Function(Side activeSide)? onEmergency; + + Timer? _timer; + Timer? _startDelayTimer; + DateTime? _lastStarted; + final _stopwatch = clock.stopwatch(); + bool _shouldPlayEmergencyFeedback = true; + DateTime? _nextEmergency; + + final ValueNotifier _whiteTime; + final ValueNotifier _blackTime; + Side _activeSide; + + bool get isRunning { + return _lastStarted != null; + } + + /// Returns the current white time. + ValueListenable get whiteTime => _whiteTime; + + /// Returns the current black time. + ValueListenable get blackTime => _blackTime; + + /// Returns the current active time. + ValueListenable get activeTime => _activeTime; + + /// Returns the current active side. + Side get activeSide => _activeSide; + + /// Sets the time for either side. + void setTimes({Duration? whiteTime, Duration? blackTime}) { + if (whiteTime != null) { + _whiteTime.value = whiteTime; + } + if (blackTime != null) { + _blackTime.value = blackTime; + } + } + + /// Sets the time for the given side. + void setTime(Side side, Duration time) { + if (side == Side.white) { + _whiteTime.value = time; + } else { + _blackTime.value = time; + } + } + + /// Increments the time for either side. + void incTimes({Duration? whiteInc, Duration? blackInc}) { + if (whiteInc != null) { + _whiteTime.value += whiteInc; + } + if (blackInc != null) { + _blackTime.value += blackInc; + } + } + + /// Increments the time for the given side. + void incTime(Side side, Duration increment) { + if (side == Side.white) { + _whiteTime.value += increment; + } else { + _blackTime.value += increment; + } + } + + /// Starts the clock and switch to the given side. + /// + /// Trying to start an already running clock on the same side is a no-op. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + /// + /// Returns the think time of the active side before switching or `null` if the clock is not running. + Duration? startSide(Side side, {Duration? delay}) { + if (isRunning && _activeSide == side) { + return _thinkTime; + } + _activeSide = side; + final thinkTime = _thinkTime; + start(delay: delay); + return thinkTime; + } + + /// Starts the clock. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + void start({Duration? delay}) { + _lastStarted = clock.now().add(delay ?? Duration.zero); + _startDelayTimer?.cancel(); + _startDelayTimer = Timer(delay ?? Duration.zero, _scheduleTick); + } + + /// Pauses the clock. + /// + /// Returns the current think time for the active side. + Duration stop() { + _stopwatch.stop(); + _startDelayTimer?.cancel(); + _timer?.cancel(); + final thinkTime = _thinkTime ?? Duration.zero; + _lastStarted = null; + return thinkTime; + } + + void dispose() { + _timer?.cancel(); + _startDelayTimer?.cancel(); + _whiteTime.dispose(); + _blackTime.dispose(); + } + + /// Returns the current think time for the active side. + Duration? get _thinkTime { + if (_lastStarted == null) { + return null; + } + return clock.now().difference(_lastStarted!); + } + + ValueNotifier get _activeTime { + return activeSide == Side.white ? _whiteTime : _blackTime; + } + + void _scheduleTick() { + _stopwatch.reset(); + _stopwatch.start(); + _timer?.cancel(); + _timer = Timer(_tickDelay, _tick); + } + + void _tick() { + final newTime = _activeTime.value - _stopwatch.elapsed; + _activeTime.value = newTime < Duration.zero ? Duration.zero : newTime; + _checkEmergency(); + if (_activeTime.value == Duration.zero) { + onFlag?.call(); + } + _scheduleTick(); + } + + void _checkEmergency() { + final timeLeft = _activeTime.value; + if (emergencyThreshold != null && + timeLeft <= emergencyThreshold! && + _shouldPlayEmergencyFeedback && + (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { + _shouldPlayEmergencyFeedback = false; + _nextEmergency = clock.now().add(_emergencyDelay); + onEmergency?.call(_activeSide); + } else if (emergencyThreshold != null && timeLeft > emergencyThreshold! * 1.5) { + _shouldPlayEmergencyFeedback = true; + } + } +} diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart deleted file mode 100644 index 5dea10a871..0000000000 --- a/lib/src/model/clock/clock_controller.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'clock_controller.freezed.dart'; -part 'clock_controller.g.dart'; - -@riverpod -class ClockController extends _$ClockController { - @override - ClockState build() { - return ClockState.fromOptions( - const ClockOptions(time: Duration(minutes: 10), increment: Duration.zero), - ); - } - - void onTap(ClockPlayerType playerType) { - final started = state.started; - if (playerType == ClockPlayerType.top) { - state = state.copyWith( - started: true, - currentPlayer: ClockPlayerType.bottom, - playerTopMoves: started ? state.playerTopMoves + 1 : 0, - ); - } else { - state = state.copyWith( - started: true, - currentPlayer: ClockPlayerType.top, - playerBottomMoves: started ? state.playerBottomMoves + 1 : 0, - ); - } - ref.read(soundServiceProvider).play(Sound.clock); - } - - void updateDuration(ClockPlayerType playerType, Duration duration) { - if (state.loser != null || state.currentPlayer == null || state.paused) { - return; - } - - if (playerType == ClockPlayerType.top) { - state = state.copyWith(playerTopTime: duration + state.options.increment); - } else { - state = - state.copyWith(playerBottomTime: duration + state.options.increment); - } - } - - void updateOptions(TimeIncrement timeIncrement) => - state = ClockState.fromTimeIncrement(timeIncrement); - - void setLoser(ClockPlayerType playerType) => - state = state.copyWith(currentPlayer: null, loser: playerType); - - void reset() => state = ClockState.fromOptions(state.options); - - void pause() => state = state.copyWith(paused: true); - - void resume() => state = state.copyWith(paused: false); -} - -enum ClockPlayerType { top, bottom } - -@freezed -class ClockOptions with _$ClockOptions { - const ClockOptions._(); - - const factory ClockOptions({ - required Duration time, - required Duration increment, - }) = _ClockOptions; -} - -@freezed -class ClockState with _$ClockState { - const ClockState._(); - - const factory ClockState({ - required int id, - required ClockOptions options, - required Duration playerTopTime, - required Duration playerBottomTime, - ClockPlayerType? currentPlayer, - ClockPlayerType? loser, - @Default(false) bool started, - @Default(false) bool paused, - @Default(0) int playerTopMoves, - @Default(0) int playerBottomMoves, - }) = _ClockState; - - factory ClockState.fromTimeIncrement(TimeIncrement timeIncrement) { - final options = ClockOptions( - time: Duration(seconds: timeIncrement.time), - increment: Duration(seconds: timeIncrement.increment), - ); - - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - options: options, - playerTopTime: options.time, - playerBottomTime: options.time, - ); - } - - factory ClockState.fromOptions(ClockOptions options) { - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - options: options, - playerTopTime: options.time, - playerBottomTime: options.time, - ); - } - - Duration getDuration(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopTime : playerBottomTime; - - int getMovesCount(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopMoves : playerBottomMoves; - - bool isPlayersTurn(ClockPlayerType playerType) => - currentPlayer == playerType || (currentPlayer == null && loser == null); - - bool isPlayersMoveAllowed(ClockPlayerType playerType) => - isPlayersTurn(playerType) && !paused; - - bool isActivePlayer(ClockPlayerType playerType) => - currentPlayer == playerType && !paused; - - bool isLoser(ClockPlayerType playerType) => loser == playerType; -} diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart new file mode 100644 index 0000000000..4f559511df --- /dev/null +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -0,0 +1,197 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'clock_tool_controller.freezed.dart'; +part 'clock_tool_controller.g.dart'; + +@riverpod +class ClockToolController extends _$ClockToolController { + late final ChessClock _clock; + + @override + ClockState build() { + const time = Duration(minutes: 10); + const increment = Duration.zero; + const options = ClockOptions( + whiteTime: time, + blackTime: time, + whiteIncrement: increment, + blackIncrement: increment, + ); + _clock = ChessClock(whiteTime: time, blackTime: time, onFlag: _onFlagged); + + ref.onDispose(() { + _clock.dispose(); + }); + + return ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + ); + } + + void _onFlagged() { + _clock.stop(); + state = state.copyWith(flagged: _clock.activeSide); + } + + void onTap(Side playerType) { + final started = state.started; + if (playerType == Side.white) { + state = state.copyWith( + started: true, + activeSide: Side.black, + whiteMoves: started ? state.whiteMoves + 1 : 0, + ); + } else { + state = state.copyWith( + started: true, + activeSide: Side.white, + blackMoves: started ? state.blackMoves + 1 : 0, + ); + } + ref.read(soundServiceProvider).play(Sound.clock); + _clock.startSide(playerType.opposite); + _clock.incTime( + playerType, + playerType == Side.white ? state.options.whiteIncrement : state.options.blackIncrement, + ); + } + + void updateDuration(Side playerType, Duration duration) { + if (state.flagged != null || state.paused) { + return; + } + + _clock.setTimes( + whiteTime: playerType == Side.white ? duration + state.options.whiteIncrement : null, + blackTime: playerType == Side.black ? duration + state.options.blackIncrement : null, + ); + } + + void updateOptions(TimeIncrement timeIncrement) { + final options = ClockOptions.fromTimeIncrement(timeIncrement); + _clock.setTimes(whiteTime: options.whiteTime, blackTime: options.blackTime); + state = state.copyWith( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + ); + } + + void updateOptionsCustom(TimeIncrement clock, Side player) { + final options = ClockOptions( + whiteTime: player == Side.white ? Duration(seconds: clock.time) : state.options.whiteTime, + blackTime: player == Side.black ? Duration(seconds: clock.time) : state.options.blackTime, + whiteIncrement: + player == Side.white ? Duration(seconds: clock.increment) : state.options.whiteIncrement, + blackIncrement: + player == Side.black ? Duration(seconds: clock.increment) : state.options.blackIncrement, + ); + _clock.setTimes(whiteTime: options.whiteTime, blackTime: options.blackTime); + state = ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: state.activeSide, + ); + } + + void setBottomPlayer(Side playerType) => state = state.copyWith(bottomPlayer: playerType); + + void reset() { + _clock.setTimes(whiteTime: state.options.whiteTime, blackTime: state.options.whiteTime); + state = state.copyWith( + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + flagged: null, + started: false, + paused: false, + whiteMoves: 0, + blackMoves: 0, + ); + } + + void start() { + _clock.start(); + state = state.copyWith(started: true); + } + + void pause() { + _clock.stop(); + state = state.copyWith(paused: true); + } + + void resume() { + _clock.start(); + state = state.copyWith(paused: false); + } +} + +@freezed +class ClockOptions with _$ClockOptions { + const ClockOptions._(); + + const factory ClockOptions({ + required Duration whiteTime, + required Duration blackTime, + required Duration whiteIncrement, + required Duration blackIncrement, + }) = _ClockOptions; + + factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => ClockOptions( + whiteTime: Duration(seconds: timeIncrement.time), + blackTime: Duration(seconds: timeIncrement.time), + whiteIncrement: Duration(seconds: timeIncrement.increment), + blackIncrement: Duration(seconds: timeIncrement.increment), + ); + + factory ClockOptions.fromSeparateTimeIncrements( + TimeIncrement playerTop, + TimeIncrement playerBottom, + ) => ClockOptions( + whiteTime: Duration(seconds: playerTop.time), + blackTime: Duration(seconds: playerBottom.time), + whiteIncrement: Duration(seconds: playerTop.increment), + blackIncrement: Duration(seconds: playerBottom.increment), + ); +} + +@freezed +class ClockState with _$ClockState { + const ClockState._(); + + const factory ClockState({ + required ClockOptions options, + required ValueListenable whiteTime, + required ValueListenable blackTime, + required Side activeSide, + @Default(Side.white) Side bottomPlayer, + Side? flagged, + @Default(false) bool started, + @Default(false) bool paused, + @Default(0) int whiteMoves, + @Default(0) int blackMoves, + }) = _ClockState; + + ValueListenable getDuration(Side playerType) => + playerType == Side.white ? whiteTime : blackTime; + + int getMovesCount(Side playerType) => playerType == Side.white ? whiteMoves : blackMoves; + + bool isPlayersTurn(Side playerType) => started && activeSide == playerType && flagged == null; + + bool isPlayersMoveAllowed(Side playerType) => isPlayersTurn(playerType) && !paused; + + bool isActivePlayer(Side playerType) => isPlayersTurn(playerType) && !paused; + + bool isFlagged(Side playerType) => flagged == playerType; +} diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index a327310f1c..a26b8b3789 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -15,13 +15,9 @@ typedef UCIMove = String; @Freezed(fromJson: true, toJson: true) class SanMove with _$SanMove { const SanMove._(); - const factory SanMove( - String san, - @MoveConverter() Move move, - ) = _SanMove; + const factory SanMove(String san, @MoveConverter() Move move) = _SanMove; - factory SanMove.fromJson(Map json) => - _$SanMoveFromJson(json); + factory SanMove.fromJson(Map json) => _$SanMoveFromJson(json); bool get isCheck => san.contains('+'); bool get isCapture => san.contains('x'); @@ -32,19 +28,22 @@ class MoveConverter implements JsonConverter { // assume we are serializing only valid uci strings @override - Move fromJson(String json) => Move.fromUci(json)!; + Move fromJson(String json) => Move.parse(json)!; @override String toJson(Move object) => object.uci; } /// Alternative castling uci notations. -const altCastles = { - 'e1a1': 'e1c1', - 'e1h1': 'e1g1', - 'e8a8': 'e8c8', - 'e8h8': 'e8g8', -}; +const altCastles = {'e1a1': 'e1c1', 'e1h1': 'e1g1', 'e8a8': 'e8c8', 'e8h8': 'e8g8'}; + +/// Returns `true` if the move is a pawn promotion move that is not yet promoted. +bool isPromotionPawnMove(Position position, NormalMove move) { + return move.promotion == null && + position.board.roleAt(move.from) == Role.pawn && + ((move.to.rank == Rank.first && position.turn == Side.black) || + (move.to.rank == Rank.eighth && position.turn == Side.white)); +} /// Set of supported variants for reading a game (not playing). const ISet readSupportedVariants = ISetConst({ @@ -117,6 +116,9 @@ enum Variant { case Variant.standard: return Chess.initial; case Variant.chess960: + throw ArgumentError( + 'Chess960 has not single initial position, use randomChess960Position() instead.', + ); case Variant.fromPosition: throw ArgumentError('This variant has no defined initial position!'); case Variant.antichess: @@ -169,24 +171,16 @@ sealed class Opening { @Freezed(fromJson: true, toJson: true) class LightOpening with _$LightOpening implements Opening { const LightOpening._(); - const factory LightOpening({ - required String eco, - required String name, - }) = _LightOpening; + const factory LightOpening({required String eco, required String name}) = _LightOpening; - factory LightOpening.fromJson(Map json) => - _$LightOpeningFromJson(json); + factory LightOpening.fromJson(Map json) => _$LightOpeningFromJson(json); } @Freezed(fromJson: true, toJson: true) class Division with _$Division { - const factory Division({ - double? middlegame, - double? endgame, - }) = _Division; + const factory Division({double? middlegame, double? endgame}) = _Division; - factory Division.fromJson(Map json) => - _$DivisionFromJson(json); + factory Division.fromJson(Map json) => _$DivisionFromJson(json); } @freezed @@ -208,7 +202,7 @@ extension ChessExtension on Pick { return value; } if (value is String) { - final move = Move.fromUci(value); + final move = Move.parse(value); if (move != null) { return move; } else { @@ -217,9 +211,7 @@ extension ChessExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Move", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Move"); } Move? asUciMoveOrNull() { @@ -240,14 +232,12 @@ extension ChessExtension on Pick { return value == 'white' ? Side.white : value == 'black' - ? Side.black - : throw PickException( - "value $value at $debugParsingExit can't be casted to Side: invalid string.", - ); + ? Side.black + : throw PickException( + "value $value at $debugParsingExit can't be casted to Side: invalid string.", + ); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Side", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Side"); } Side? asSideOrNull() { @@ -259,6 +249,19 @@ extension ChessExtension on Pick { } } + Square asSquareOrThrow() { + return Square.parse(this.required().value as String)!; + } + + Square? asSquareOrNull() { + if (value == null) return null; + try { + return asSquareOrThrow(); + } catch (_) { + return null; + } + } + Variant asVariantOrThrow() { final value = this.required().value; if (value is Variant) { @@ -275,9 +278,7 @@ extension ChessExtension on Pick { return variant; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Variant", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Variant"); } Variant? asVariantOrNull() { diff --git a/lib/src/model/common/chess960.dart b/lib/src/model/common/chess960.dart new file mode 100644 index 0000000000..6daf1bd857 --- /dev/null +++ b/lib/src/model/common/chess960.dart @@ -0,0 +1,977 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; + +final _random = Random.secure(); + +Position randomChess960Position() { + final rank8 = _positions[_random.nextInt(_positions.length)]; + + return Chess.fromSetup( + Setup.parseFen('$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1'), + ); +} + +// https://github.com/lichess-org/scalachess/blob/bd139c6dc1acdc8fff08c46e412f784d49a16578/core/src/main/scala/variant/Chess960.scala#L49 +final _positions = [ + 'bbqnnrkr', + 'bqnbnrkr', + 'bqnnrbkr', + 'bqnnrkrb', + 'qbbnnrkr', + 'qnbbnrkr', + 'qnbnrbkr', + 'qnbnrkrb', + 'qbnnbrkr', + 'qnnbbrkr', + 'qnnrbbkr', + 'qnnrbkrb', + 'qbnnrkbr', + 'qnnbrkbr', + 'qnnrkbbr', + 'qnnrkrbb', + 'bbnqnrkr', + 'bnqbnrkr', + 'bnqnrbkr', + 'bnqnrkrb', + 'nbbqnrkr', + 'nqbbnrkr', + 'nqbnrbkr', + 'nqbnrkrb', + 'nbqnbrkr', + 'nqnbbrkr', + 'nqnrbbkr', + 'nqnrbkrb', + 'nbqnrkbr', + 'nqnbrkbr', + 'nqnrkbbr', + 'nqnrkrbb', + 'bbnnqrkr', + 'bnnbqrkr', + 'bnnqrbkr', + 'bnnqrkrb', + 'nbbnqrkr', + 'nnbbqrkr', + 'nnbqrbkr', + 'nnbqrkrb', + 'nbnqbrkr', + 'nnqbbrkr', + 'nnqrbbkr', + 'nnqrbkrb', + 'nbnqrkbr', + 'nnqbrkbr', + 'nnqrkbbr', + 'nnqrkrbb', + 'bbnnrqkr', + 'bnnbrqkr', + 'bnnrqbkr', + 'bnnrqkrb', + 'nbbnrqkr', + 'nnbbrqkr', + 'nnbrqbkr', + 'nnbrqkrb', + 'nbnrbqkr', + 'nnrbbqkr', + 'nnrqbbkr', + 'nnrqbkrb', + 'nbnrqkbr', + 'nnrbqkbr', + 'nnrqkbbr', + 'nnrqkrbb', + 'bbnnrkqr', + 'bnnbrkqr', + 'bnnrkbqr', + 'bnnrkqrb', + 'nbbnrkqr', + 'nnbbrkqr', + 'nnbrkbqr', + 'nnbrkqrb', + 'nbnrbkqr', + 'nnrbbkqr', + 'nnrkbbqr', + 'nnrkbqrb', + 'nbnrkqbr', + 'nnrbkqbr', + 'nnrkqbbr', + 'nnrkqrbb', + 'bbnnrkrq', + 'bnnbrkrq', + 'bnnrkbrq', + 'bnnrkrqb', + 'nbbnrkrq', + 'nnbbrkrq', + 'nnbrkbrq', + 'nnbrkrqb', + 'nbnrbkrq', + 'nnrbbkrq', + 'nnrkbbrq', + 'nnrkbrqb', + 'nbnrkrbq', + 'nnrbkrbq', + 'nnrkrbbq', + 'nnrkrqbb', + 'bbqnrnkr', + 'bqnbrnkr', + 'bqnrnbkr', + 'bqnrnkrb', + 'qbbnrnkr', + 'qnbbrnkr', + 'qnbrnbkr', + 'qnbrnkrb', + 'qbnrbnkr', + 'qnrbbnkr', + 'qnrnbbkr', + 'qnrnbkrb', + 'qbnrnkbr', + 'qnrbnkbr', + 'qnrnkbbr', + 'qnrnkrbb', + 'bbnqrnkr', + 'bnqbrnkr', + 'bnqrnbkr', + 'bnqrnkrb', + 'nbbqrnkr', + 'nqbbrnkr', + 'nqbrnbkr', + 'nqbrnkrb', + 'nbqrbnkr', + 'nqrbbnkr', + 'nqrnbbkr', + 'nqrnbkrb', + 'nbqrnkbr', + 'nqrbnkbr', + 'nqrnkbbr', + 'nqrnkrbb', + 'bbnrqnkr', + 'bnrbqnkr', + 'bnrqnbkr', + 'bnrqnkrb', + 'nbbrqnkr', + 'nrbbqnkr', + 'nrbqnbkr', + 'nrbqnkrb', + 'nbrqbnkr', + 'nrqbbnkr', + 'nrqnbbkr', + 'nrqnbkrb', + 'nbrqnkbr', + 'nrqbnkbr', + 'nrqnkbbr', + 'nrqnkrbb', + 'bbnrnqkr', + 'bnrbnqkr', + 'bnrnqbkr', + 'bnrnqkrb', + 'nbbrnqkr', + 'nrbbnqkr', + 'nrbnqbkr', + 'nrbnqkrb', + 'nbrnbqkr', + 'nrnbbqkr', + 'nrnqbbkr', + 'nrnqbkrb', + 'nbrnqkbr', + 'nrnbqkbr', + 'nrnqkbbr', + 'nrnqkrbb', + 'bbnrnkqr', + 'bnrbnkqr', + 'bnrnkbqr', + 'bnrnkqrb', + 'nbbrnkqr', + 'nrbbnkqr', + 'nrbnkbqr', + 'nrbnkqrb', + 'nbrnbkqr', + 'nrnbbkqr', + 'nrnkbbqr', + 'nrnkbqrb', + 'nbrnkqbr', + 'nrnbkqbr', + 'nrnkqbbr', + 'nrnkqrbb', + 'bbnrnkrq', + 'bnrbnkrq', + 'bnrnkbrq', + 'bnrnkrqb', + 'nbbrnkrq', + 'nrbbnkrq', + 'nrbnkbrq', + 'nrbnkrqb', + 'nbrnbkrq', + 'nrnbbkrq', + 'nrnkbbrq', + 'nrnkbrqb', + 'nbrnkrbq', + 'nrnbkrbq', + 'nrnkrbbq', + 'nrnkrqbb', + 'bbqnrknr', + 'bqnbrknr', + 'bqnrkbnr', + 'bqnrknrb', + 'qbbnrknr', + 'qnbbrknr', + 'qnbrkbnr', + 'qnbrknrb', + 'qbnrbknr', + 'qnrbbknr', + 'qnrkbbnr', + 'qnrkbnrb', + 'qbnrknbr', + 'qnrbknbr', + 'qnrknbbr', + 'qnrknrbb', + 'bbnqrknr', + 'bnqbrknr', + 'bnqrkbnr', + 'bnqrknrb', + 'nbbqrknr', + 'nqbbrknr', + 'nqbrkbnr', + 'nqbrknrb', + 'nbqrbknr', + 'nqrbbknr', + 'nqrkbbnr', + 'nqrkbnrb', + 'nbqrknbr', + 'nqrbknbr', + 'nqrknbbr', + 'nqrknrbb', + 'bbnrqknr', + 'bnrbqknr', + 'bnrqkbnr', + 'bnrqknrb', + 'nbbrqknr', + 'nrbbqknr', + 'nrbqkbnr', + 'nrbqknrb', + 'nbrqbknr', + 'nrqbbknr', + 'nrqkbbnr', + 'nrqkbnrb', + 'nbrqknbr', + 'nrqbknbr', + 'nrqknbbr', + 'nrqknrbb', + 'bbnrkqnr', + 'bnrbkqnr', + 'bnrkqbnr', + 'bnrkqnrb', + 'nbbrkqnr', + 'nrbbkqnr', + 'nrbkqbnr', + 'nrbkqnrb', + 'nbrkbqnr', + 'nrkbbqnr', + 'nrkqbbnr', + 'nrkqbnrb', + 'nbrkqnbr', + 'nrkbqnbr', + 'nrkqnbbr', + 'nrkqnrbb', + 'bbnrknqr', + 'bnrbknqr', + 'bnrknbqr', + 'bnrknqrb', + 'nbbrknqr', + 'nrbbknqr', + 'nrbknbqr', + 'nrbknqrb', + 'nbrkbnqr', + 'nrkbbnqr', + 'nrknbbqr', + 'nrknbqrb', + 'nbrknqbr', + 'nrkbnqbr', + 'nrknqbbr', + 'nrknqrbb', + 'bbnrknrq', + 'bnrbknrq', + 'bnrknbrq', + 'bnrknrqb', + 'nbbrknrq', + 'nrbbknrq', + 'nrbknbrq', + 'nrbknrqb', + 'nbrkbnrq', + 'nrkbbnrq', + 'nrknbbrq', + 'nrknbrqb', + 'nbrknrbq', + 'nrkbnrbq', + 'nrknrbbq', + 'nrknrqbb', + 'bbqnrkrn', + 'bqnbrkrn', + 'bqnrkbrn', + 'bqnrkrnb', + 'qbbnrkrn', + 'qnbbrkrn', + 'qnbrkbrn', + 'qnbrkrnb', + 'qbnrbkrn', + 'qnrbbkrn', + 'qnrkbbrn', + 'qnrkbrnb', + 'qbnrkrbn', + 'qnrbkrbn', + 'qnrkrbbn', + 'qnrkrnbb', + 'bbnqrkrn', + 'bnqbrkrn', + 'bnqrkbrn', + 'bnqrkrnb', + 'nbbqrkrn', + 'nqbbrkrn', + 'nqbrkbrn', + 'nqbrkrnb', + 'nbqrbkrn', + 'nqrbbkrn', + 'nqrkbbrn', + 'nqrkbrnb', + 'nbqrkrbn', + 'nqrbkrbn', + 'nqrkrbbn', + 'nqrkrnbb', + 'bbnrqkrn', + 'bnrbqkrn', + 'bnrqkbrn', + 'bnrqkrnb', + 'nbbrqkrn', + 'nrbbqkrn', + 'nrbqkbrn', + 'nrbqkrnb', + 'nbrqbkrn', + 'nrqbbkrn', + 'nrqkbbrn', + 'nrqkbrnb', + 'nbrqkrbn', + 'nrqbkrbn', + 'nrqkrbbn', + 'nrqkrnbb', + 'bbnrkqrn', + 'bnrbkqrn', + 'bnrkqbrn', + 'bnrkqrnb', + 'nbbrkqrn', + 'nrbbkqrn', + 'nrbkqbrn', + 'nrbkqrnb', + 'nbrkbqrn', + 'nrkbbqrn', + 'nrkqbbrn', + 'nrkqbrnb', + 'nbrkqrbn', + 'nrkbqrbn', + 'nrkqrbbn', + 'nrkqrnbb', + 'bbnrkrqn', + 'bnrbkrqn', + 'bnrkrbqn', + 'bnrkrqnb', + 'nbbrkrqn', + 'nrbbkrqn', + 'nrbkrbqn', + 'nrbkrqnb', + 'nbrkbrqn', + 'nrkbbrqn', + 'nrkrbbqn', + 'nrkrbqnb', + 'nbrkrqbn', + 'nrkbrqbn', + 'nrkrqbbn', + 'nrkrqnbb', + 'bbnrkrnq', + 'bnrbkrnq', + 'bnrkrbnq', + 'bnrkrnqb', + 'nbbrkrnq', + 'nrbbkrnq', + 'nrbkrbnq', + 'nrbkrnqb', + 'nbrkbrnq', + 'nrkbbrnq', + 'nrkrbbnq', + 'nrkrbnqb', + 'nbrkrnbq', + 'nrkbrnbq', + 'nrkrnbbq', + 'nrkrnqbb', + 'bbqrnnkr', + 'bqrbnnkr', + 'bqrnnbkr', + 'bqrnnkrb', + 'qbbrnnkr', + 'qrbbnnkr', + 'qrbnnbkr', + 'qrbnnkrb', + 'qbrnbnkr', + 'qrnbbnkr', + 'qrnnbbkr', + 'qrnnbkrb', + 'qbrnnkbr', + 'qrnbnkbr', + 'qrnnkbbr', + 'qrnnkrbb', + 'bbrqnnkr', + 'brqbnnkr', + 'brqnnbkr', + 'brqnnkrb', + 'rbbqnnkr', + 'rqbbnnkr', + 'rqbnnbkr', + 'rqbnnkrb', + 'rbqnbnkr', + 'rqnbbnkr', + 'rqnnbbkr', + 'rqnnbkrb', + 'rbqnnkbr', + 'rqnbnkbr', + 'rqnnkbbr', + 'rqnnkrbb', + 'bbrnqnkr', + 'brnbqnkr', + 'brnqnbkr', + 'brnqnkrb', + 'rbbnqnkr', + 'rnbbqnkr', + 'rnbqnbkr', + 'rnbqnkrb', + 'rbnqbnkr', + 'rnqbbnkr', + 'rnqnbbkr', + 'rnqnbkrb', + 'rbnqnkbr', + 'rnqbnkbr', + 'rnqnkbbr', + 'rnqnkrbb', + 'bbrnnqkr', + 'brnbnqkr', + 'brnnqbkr', + 'brnnqkrb', + 'rbbnnqkr', + 'rnbbnqkr', + 'rnbnqbkr', + 'rnbnqkrb', + 'rbnnbqkr', + 'rnnbbqkr', + 'rnnqbbkr', + 'rnnqbkrb', + 'rbnnqkbr', + 'rnnbqkbr', + 'rnnqkbbr', + 'rnnqkrbb', + 'bbrnnkqr', + 'brnbnkqr', + 'brnnkbqr', + 'brnnkqrb', + 'rbbnnkqr', + 'rnbbnkqr', + 'rnbnkbqr', + 'rnbnkqrb', + 'rbnnbkqr', + 'rnnbbkqr', + 'rnnkbbqr', + 'rnnkbqrb', + 'rbnnkqbr', + 'rnnbkqbr', + 'rnnkqbbr', + 'rnnkqrbb', + 'bbrnnkrq', + 'brnbnkrq', + 'brnnkbrq', + 'brnnkrqb', + 'rbbnnkrq', + 'rnbbnkrq', + 'rnbnkbrq', + 'rnbnkrqb', + 'rbnnbkrq', + 'rnnbbkrq', + 'rnnkbbrq', + 'rnnkbrqb', + 'rbnnkrbq', + 'rnnbkrbq', + 'rnnkrbbq', + 'rnnkrqbb', + 'bbqrnknr', + 'bqrbnknr', + 'bqrnkbnr', + 'bqrnknrb', + 'qbbrnknr', + 'qrbbnknr', + 'qrbnkbnr', + 'qrbnknrb', + 'qbrnbknr', + 'qrnbbknr', + 'qrnkbbnr', + 'qrnkbnrb', + 'qbrnknbr', + 'qrnbknbr', + 'qrnknbbr', + 'qrnknrbb', + 'bbrqnknr', + 'brqbnknr', + 'brqnkbnr', + 'brqnknrb', + 'rbbqnknr', + 'rqbbnknr', + 'rqbnkbnr', + 'rqbnknrb', + 'rbqnbknr', + 'rqnbbknr', + 'rqnkbbnr', + 'rqnkbnrb', + 'rbqnknbr', + 'rqnbknbr', + 'rqnknbbr', + 'rqnknrbb', + 'bbrnqknr', + 'brnbqknr', + 'brnqkbnr', + 'brnqknrb', + 'rbbnqknr', + 'rnbbqknr', + 'rnbqkbnr', + 'rnbqknrb', + 'rbnqbknr', + 'rnqbbknr', + 'rnqkbbnr', + 'rnqkbnrb', + 'rbnqknbr', + 'rnqbknbr', + 'rnqknbbr', + 'rnqknrbb', + 'bbrnkqnr', + 'brnbkqnr', + 'brnkqbnr', + 'brnkqnrb', + 'rbbnkqnr', + 'rnbbkqnr', + 'rnbkqbnr', + 'rnbkqnrb', + 'rbnkbqnr', + 'rnkbbqnr', + 'rnkqbbnr', + 'rnkqbnrb', + 'rbnkqnbr', + 'rnkbqnbr', + 'rnkqnbbr', + 'rnkqnrbb', + 'bbrnknqr', + 'brnbknqr', + 'brnknbqr', + 'brnknqrb', + 'rbbnknqr', + 'rnbbknqr', + 'rnbknbqr', + 'rnbknqrb', + 'rbnkbnqr', + 'rnkbbnqr', + 'rnknbbqr', + 'rnknbqrb', + 'rbnknqbr', + 'rnkbnqbr', + 'rnknqbbr', + 'rnknqrbb', + 'bbrnknrq', + 'brnbknrq', + 'brnknbrq', + 'brnknrqb', + 'rbbnknrq', + 'rnbbknrq', + 'rnbknbrq', + 'rnbknrqb', + 'rbnkbnrq', + 'rnkbbnrq', + 'rnknbbrq', + 'rnknbrqb', + 'rbnknrbq', + 'rnkbnrbq', + 'rnknrbbq', + 'rnknrqbb', + 'bbqrnkrn', + 'bqrbnkrn', + 'bqrnkbrn', + 'bqrnkrnb', + 'qbbrnkrn', + 'qrbbnkrn', + 'qrbnkbrn', + 'qrbnkrnb', + 'qbrnbkrn', + 'qrnbbkrn', + 'qrnkbbrn', + 'qrnkbrnb', + 'qbrnkrbn', + 'qrnbkrbn', + 'qrnkrbbn', + 'qrnkrnbb', + 'bbrqnkrn', + 'brqbnkrn', + 'brqnkbrn', + 'brqnkrnb', + 'rbbqnkrn', + 'rqbbnkrn', + 'rqbnkbrn', + 'rqbnkrnb', + 'rbqnbkrn', + 'rqnbbkrn', + 'rqnkbbrn', + 'rqnkbrnb', + 'rbqnkrbn', + 'rqnbkrbn', + 'rqnkrbbn', + 'rqnkrnbb', + 'bbrnqkrn', + 'brnbqkrn', + 'brnqkbrn', + 'brnqkrnb', + 'rbbnqkrn', + 'rnbbqkrn', + 'rnbqkbrn', + 'rnbqkrnb', + 'rbnqbkrn', + 'rnqbbkrn', + 'rnqkbbrn', + 'rnqkbrnb', + 'rbnqkrbn', + 'rnqbkrbn', + 'rnqkrbbn', + 'rnqkrnbb', + 'bbrnkqrn', + 'brnbkqrn', + 'brnkqbrn', + 'brnkqrnb', + 'rbbnkqrn', + 'rnbbkqrn', + 'rnbkqbrn', + 'rnbkqrnb', + 'rbnkbqrn', + 'rnkbbqrn', + 'rnkqbbrn', + 'rnkqbrnb', + 'rbnkqrbn', + 'rnkbqrbn', + 'rnkqrbbn', + 'rnkqrnbb', + 'bbrnkrqn', + 'brnbkrqn', + 'brnkrbqn', + 'brnkrqnb', + 'rbbnkrqn', + 'rnbbkrqn', + 'rnbkrbqn', + 'rnbkrqnb', + 'rbnkbrqn', + 'rnkbbrqn', + 'rnkrbbqn', + 'rnkrbqnb', + 'rbnkrqbn', + 'rnkbrqbn', + 'rnkrqbbn', + 'rnkrqnbb', + 'bbrnkrnq', + 'brnbkrnq', + 'brnkrbnq', + 'brnkrnqb', + 'rbbnkrnq', + 'rnbbkrnq', + 'rnbkrbnq', + 'rnbkrnqb', + 'rbnkbrnq', + 'rnkbbrnq', + 'rnkrbbnq', + 'rnkrbnqb', + 'rbnkrnbq', + 'rnkbrnbq', + 'rnkrnbbq', + 'rnkrnqbb', + 'bbqrknnr', + 'bqrbknnr', + 'bqrknbnr', + 'bqrknnrb', + 'qbbrknnr', + 'qrbbknnr', + 'qrbknbnr', + 'qrbknnrb', + 'qbrkbnnr', + 'qrkbbnnr', + 'qrknbbnr', + 'qrknbnrb', + 'qbrknnbr', + 'qrkbnnbr', + 'qrknnbbr', + 'qrknnrbb', + 'bbrqknnr', + 'brqbknnr', + 'brqknbnr', + 'brqknnrb', + 'rbbqknnr', + 'rqbbknnr', + 'rqbknbnr', + 'rqbknnrb', + 'rbqkbnnr', + 'rqkbbnnr', + 'rqknbbnr', + 'rqknbnrb', + 'rbqknnbr', + 'rqkbnnbr', + 'rqknnbbr', + 'rqknnrbb', + 'bbrkqnnr', + 'brkbqnnr', + 'brkqnbnr', + 'brkqnnrb', + 'rbbkqnnr', + 'rkbbqnnr', + 'rkbqnbnr', + 'rkbqnnrb', + 'rbkqbnnr', + 'rkqbbnnr', + 'rkqnbbnr', + 'rkqnbnrb', + 'rbkqnnbr', + 'rkqbnnbr', + 'rkqnnbbr', + 'rkqnnrbb', + 'bbrknqnr', + 'brkbnqnr', + 'brknqbnr', + 'brknqnrb', + 'rbbknqnr', + 'rkbbnqnr', + 'rkbnqbnr', + 'rkbnqnrb', + 'rbknbqnr', + 'rknbbqnr', + 'rknqbbnr', + 'rknqbnrb', + 'rbknqnbr', + 'rknbqnbr', + 'rknqnbbr', + 'rknqnrbb', + 'bbrknnqr', + 'brkbnnqr', + 'brknnbqr', + 'brknnqrb', + 'rbbknnqr', + 'rkbbnnqr', + 'rkbnnbqr', + 'rkbnnqrb', + 'rbknbnqr', + 'rknbbnqr', + 'rknnbbqr', + 'rknnbqrb', + 'rbknnqbr', + 'rknbnqbr', + 'rknnqbbr', + 'rknnqrbb', + 'bbrknnrq', + 'brkbnnrq', + 'brknnbrq', + 'brknnrqb', + 'rbbknnrq', + 'rkbbnnrq', + 'rkbnnbrq', + 'rkbnnrqb', + 'rbknbnrq', + 'rknbbnrq', + 'rknnbbrq', + 'rknnbrqb', + 'rbknnrbq', + 'rknbnrbq', + 'rknnrbbq', + 'rknnrqbb', + 'bbqrknrn', + 'bqrbknrn', + 'bqrknbrn', + 'bqrknrnb', + 'qbbrknrn', + 'qrbbknrn', + 'qrbknbrn', + 'qrbknrnb', + 'qbrkbnrn', + 'qrkbbnrn', + 'qrknbbrn', + 'qrknbrnb', + 'qbrknrbn', + 'qrkbnrbn', + 'qrknrbbn', + 'qrknrnbb', + 'bbrqknrn', + 'brqbknrn', + 'brqknbrn', + 'brqknrnb', + 'rbbqknrn', + 'rqbbknrn', + 'rqbknbrn', + 'rqbknrnb', + 'rbqkbnrn', + 'rqkbbnrn', + 'rqknbbrn', + 'rqknbrnb', + 'rbqknrbn', + 'rqkbnrbn', + 'rqknrbbn', + 'rqknrnbb', + 'bbrkqnrn', + 'brkbqnrn', + 'brkqnbrn', + 'brkqnrnb', + 'rbbkqnrn', + 'rkbbqnrn', + 'rkbqnbrn', + 'rkbqnrnb', + 'rbkqbnrn', + 'rkqbbnrn', + 'rkqnbbrn', + 'rkqnbrnb', + 'rbkqnrbn', + 'rkqbnrbn', + 'rkqnrbbn', + 'rkqnrnbb', + 'bbrknqrn', + 'brkbnqrn', + 'brknqbrn', + 'brknqrnb', + 'rbbknqrn', + 'rkbbnqrn', + 'rkbnqbrn', + 'rkbnqrnb', + 'rbknbqrn', + 'rknbbqrn', + 'rknqbbrn', + 'rknqbrnb', + 'rbknqrbn', + 'rknbqrbn', + 'rknqrbbn', + 'rknqrnbb', + 'bbrknrqn', + 'brkbnrqn', + 'brknrbqn', + 'brknrqnb', + 'rbbknrqn', + 'rkbbnrqn', + 'rkbnrbqn', + 'rkbnrqnb', + 'rbknbrqn', + 'rknbbrqn', + 'rknrbbqn', + 'rknrbqnb', + 'rbknrqbn', + 'rknbrqbn', + 'rknrqbbn', + 'rknrqnbb', + 'bbrknrnq', + 'brkbnrnq', + 'brknrbnq', + 'brknrnqb', + 'rbbknrnq', + 'rkbbnrnq', + 'rkbnrbnq', + 'rkbnrnqb', + 'rbknbrnq', + 'rknbbrnq', + 'rknrbbnq', + 'rknrbnqb', + 'rbknrnbq', + 'rknbrnbq', + 'rknrnbbq', + 'rknrnqbb', + 'bbqrkrnn', + 'bqrbkrnn', + 'bqrkrbnn', + 'bqrkrnnb', + 'qbbrkrnn', + 'qrbbkrnn', + 'qrbkrbnn', + 'qrbkrnnb', + 'qbrkbrnn', + 'qrkbbrnn', + 'qrkrbbnn', + 'qrkrbnnb', + 'qbrkrnbn', + 'qrkbrnbn', + 'qrkrnbbn', + 'qrkrnnbb', + 'bbrqkrnn', + 'brqbkrnn', + 'brqkrbnn', + 'brqkrnnb', + 'rbbqkrnn', + 'rqbbkrnn', + 'rqbkrbnn', + 'rqbkrnnb', + 'rbqkbrnn', + 'rqkbbrnn', + 'rqkrbbnn', + 'rqkrbnnb', + 'rbqkrnbn', + 'rqkbrnbn', + 'rqkrnbbn', + 'rqkrnnbb', + 'bbrkqrnn', + 'brkbqrnn', + 'brkqrbnn', + 'brkqrnnb', + 'rbbkqrnn', + 'rkbbqrnn', + 'rkbqrbnn', + 'rkbqrnnb', + 'rbkqbrnn', + 'rkqbbrnn', + 'rkqrbbnn', + 'rkqrbnnb', + 'rbkqrnbn', + 'rkqbrnbn', + 'rkqrnbbn', + 'rkqrnnbb', + 'bbrkrqnn', + 'brkbrqnn', + 'brkrqbnn', + 'brkrqnnb', + 'rbbkrqnn', + 'rkbbrqnn', + 'rkbrqbnn', + 'rkbrqnnb', + 'rbkrbqnn', + 'rkrbbqnn', + 'rkrqbbnn', + 'rkrqbnnb', + 'rbkrqnbn', + 'rkrbqnbn', + 'rkrqnbbn', + 'rkrqnnbb', + 'bbrkrnqn', + 'brkbrnqn', + 'brkrnbqn', + 'brkrnqnb', + 'rbbkrnqn', + 'rkbbrnqn', + 'rkbrnbqn', + 'rkbrnqnb', + 'rbkrbnqn', + 'rkrbbnqn', + 'rkrnbbqn', + 'rkrnbqnb', + 'rbkrnqbn', + 'rkrbnqbn', + 'rkrnqbbn', + 'rkrnqnbb', + 'bbrkrnnq', + 'brkbrnnq', + 'brkrnbnq', + 'brkrnnqb', + 'rbbkrnnq', + 'rkbbrnnq', + 'rkbrnbnq', + 'rkbrnnqb', + 'rbkrbnnq', + 'rkrbbnnq', + 'rkrnbbnq', + 'rkrnbnqb', + 'rbkrnnbq', + 'rkrbnnbq', + 'rkrnnbbq', + 'rkrnnqbb', +]; diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 429e24cd99..a1ef6181d0 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -1,8 +1,11 @@ import 'dart:math' as math; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -28,8 +31,15 @@ class ExternalEval with _$ExternalEval implements Eval { ({String name, String comment})? judgment, }) = _ExternalEval; - factory ExternalEval.fromJson(Map json) => - _$ExternalEvalFromJson(json); + factory ExternalEval.fromPgnEval(PgnEvaluation eval) { + return ExternalEval( + cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, + mate: eval.mate, + depth: eval.depth, + ); + } + + factory ExternalEval.fromJson(Map json) => _$ExternalEvalFromJson(json); @override String get evalString => _evalString(cp, mate); @@ -69,7 +79,7 @@ class ClientEval with _$ClientEval implements Eval { required int nodes, required IList pvs, required int millis, - required int maxDepth, + required Duration searchTime, int? cp, int? mate, }) = _ClientEval; @@ -79,14 +89,14 @@ class ClientEval with _$ClientEval implements Eval { Move? get bestMove { final uci = pvs.firstOrNull?.moves.firstOrNull; if (uci == null) return null; - return Move.fromUci(uci); + return Move.parse(uci); } IList get bestMoves { return pvs .where((e) => e.moves.isNotEmpty) .map((e) => e._firstMoveWithWinningChances(position.turn)) - .whereNotNull() + .nonNulls .sorted((a, b) => b.winningChances.compareTo(a.winningChances)) .toIList(); } @@ -109,11 +119,7 @@ class ClientEval with _$ClientEval implements Eval { @freezed class PvData with _$PvData { const PvData._(); - const factory PvData({ - required IList moves, - int? mate, - int? cp, - }) = _PvData; + const factory PvData({required IList moves, int? mate, int? cp}) = _PvData; String get evalString => _evalString(cp, mate); @@ -132,7 +138,7 @@ class PvData with _$PvData { final List res = []; for (final uciMove in moves.sublist(0, math.min(12, moves.length))) { // assume uciMove string is valid as it comes from stockfish - final move = Move.fromUci(uciMove)!; + final move = Move.parse(uciMove)!; if (pos.isLegal(move)) { final (newPos, san) = pos.makeSanUnchecked(move); res.add(san); @@ -145,25 +151,85 @@ class PvData with _$PvData { } MoveWithWinningChances? _firstMoveWithWinningChances(Side sideToMove) { - final uciMove = (moves.isNotEmpty) ? Move.fromUci(moves.first) : null; + final uciMove = (moves.isNotEmpty) ? Move.parse(moves.first) : null; return (uciMove != null) - ? ( - move: uciMove, - winningChances: - _toPov(sideToMove, _toWhiteWinningChances(cp, mate)), - ) + ? (move: uciMove, winningChances: _toPov(sideToMove, _toWhiteWinningChances(cp, mate))) : null; } } typedef MoveWithWinningChances = ({Move move, double winningChances}); +ISet computeBestMoveShapes( + IList moves, + Side sideToMove, + PieceAssets pieceAssets, +) { + // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move + // (assume moves are ordered by their winning chances, so index==0 is the best move) + double scaleArrowAgainstBestMove(int index) { + const minScale = 0.15; + const maxScale = 1.0; + const winningDiffScaleFactor = 2.5; + + final bestMove = moves[0]; + final winningDiffComparedToBestMove = bestMove.winningChances - moves[index].winningChances; + // Force minimum scale if the best move is significantly better than this move + if (winningDiffComparedToBestMove > 0.3) { + return minScale; + } + return clampDouble( + math.max(minScale, maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove), + 0, + 1, + ); + } + + return ISet( + moves + .mapIndexed((i, m) { + final move = m.move; + // Same colors as in the Web UI with a slightly different opacity + // The best move has a different color than the other moves + final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); + switch (move) { + case NormalMove(from: _, to: _, promotion: final promRole): + return [ + Arrow( + color: color, + orig: move.from, + dest: move.to, + scale: scaleArrowAgainstBestMove(i), + ), + if (promRole != null) + PieceShape( + color: color, + orig: move.to, + pieceAssets: pieceAssets, + piece: Piece(color: sideToMove, role: promRole), + ), + ]; + case DropMove(role: final role, to: _): + return [ + PieceShape( + color: color, + orig: move.to, + pieceAssets: pieceAssets, + opacity: 0.5, + piece: Piece(color: sideToMove, role: role), + ), + ]; + } + }) + .expand((e) => e), + ); +} + double cpToPawns(int cp) => cp / 100; int cpFromPawns(double pawns) => (pawns * 100).round(); -double cpWinningChances(int cp) => - _rawWinningChances(math.min(math.max(-1000, cp), 1000)); +double cpWinningChances(int cp) => _rawWinningChances(math.min(math.max(-1000, cp), 1000)); double mateWinningChances(int mate) { final cp = (21 - math.min(10, mate.abs())) * 100; diff --git a/lib/src/model/common/game.dart b/lib/src/model/common/game.dart new file mode 100644 index 0000000000..f9dfefeb92 --- /dev/null +++ b/lib/src/model/common/game.dart @@ -0,0 +1,14 @@ +import 'package:lichess_mobile/l10n/l10n.dart'; + +/// Represents the choice of a side as a player: white, black or random. +enum SideChoice { + white, + random, + black; + + String label(AppLocalizations l10n) => switch (this) { + SideChoice.white => l10n.white, + SideChoice.random => l10n.randomColor, + SideChoice.black => l10n.black, + }; +} diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 4ab5fb6edb..f7cbe93049 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -8,6 +8,8 @@ extension type const StringId(String value) { bool startsWith(String prefix) => value.startsWith(prefix); } +extension type const IntId(int value) {} + extension type const GameAnyId._(String value) implements StringId { GameAnyId(this.value) : assert(value.length == 8 || value.length == 12); GameId get gameId => GameId(value.substring(0, 8)); @@ -47,21 +49,33 @@ extension type const UserId(String value) implements StringId { UserId.fromJson(dynamic json) : this(json as String); } -extension type const ChallengeId(String value) implements StringId {} +extension type const ChallengeId(String value) implements StringId { + ChallengeId.fromJson(dynamic json) : this(json as String); +} + +extension type const BroadcastTournamentId(String value) implements StringId {} extension type const BroadcastRoundId(String value) implements StringId {} extension type const BroadcastGameId(String value) implements StringId {} +extension type const StudyId(String value) implements StringId { + StudyId.fromJson(dynamic json) : this(json as String); +} + +extension type const StudyChapterId(String value) implements StringId { + StudyChapterId.fromJson(dynamic json) : this(json as String); +} + +extension type const FideId(int value) implements IntId {} + extension IDPick on Pick { UserId asUserIdOrThrow() { final value = required().value; if (value is String) { return UserId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to UserId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to UserId"); } UserId? asUserIdOrNull() { @@ -78,9 +92,7 @@ extension IDPick on Pick { if (value is String) { return GameId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameId"); } GameId? asGameIdOrNull() { @@ -97,9 +109,7 @@ extension IDPick on Pick { if (value is String) { return GameFullId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameId"); } GameFullId? asGameFullIdOrNull() { @@ -116,9 +126,7 @@ extension IDPick on Pick { if (value is String) { return PuzzleId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to PuzzleId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to PuzzleId"); } PuzzleId? asPuzzleIdOrNull() { @@ -135,9 +143,7 @@ extension IDPick on Pick { if (value is String) { return ChallengeId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeId"); } ChallengeId? asChallengeIdOrNull() { @@ -149,17 +155,34 @@ extension IDPick on Pick { } } - BroadcastRoundId asBroadcastRoundIdOrThrow() { + BroadcastTournamentId asBroadcastTournamentIdOrThrow() { final value = required().value; if (value is String) { - return BroadcastRoundId(value); + return BroadcastTournamentId(value); } throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastRoundId", + "value $value at $debugParsingExit can't be casted to BroadcastTournamentId", ); } - BroadcastRoundId? asBroadcastRoundIddOrNull() { + BroadcastTournamentId? asBroadcastTournamentIdOrNull() { + if (value == null) return null; + try { + return asBroadcastTournamentIdOrThrow(); + } catch (_) { + return null; + } + } + + BroadcastRoundId asBroadcastRoundIdOrThrow() { + final value = required().value; + if (value is String) { + return BroadcastRoundId(value); + } + throw PickException("value $value at $debugParsingExit can't be casted to BroadcastRoundId"); + } + + BroadcastRoundId? asBroadcastRoundIdOrNull() { if (value == null) return null; try { return asBroadcastRoundIdOrThrow(); @@ -173,12 +196,10 @@ extension IDPick on Pick { if (value is String) { return BroadcastGameId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastRoundId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to BroadcastGameId"); } - BroadcastGameId? asBroadcastGameIddOrNull() { + BroadcastGameId? asBroadcastGameIdOrNull() { if (value == null) return null; try { return asBroadcastGameIdOrThrow(); @@ -186,4 +207,29 @@ extension IDPick on Pick { return null; } } + + StudyId asStudyIdOrThrow() { + final value = required().value; + if (value is String) { + return StudyId(value); + } + throw PickException("value $value at $debugParsingExit can't be casted to StudyId"); + } + + FideId asFideIdOrThrow() { + final value = required().value; + if (value is int && value != 0) { + return FideId(value); + } + throw PickException("value $value at $debugParsingExit can't be casted to FideId"); + } + + FideId? asFideIdOrNull() { + if (value == null) return null; + try { + return asFideIdOrThrow(); + } catch (_) { + return null; + } + } } diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index 8916702bac..5aed5f5540 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -5,9 +5,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:logging/logging.dart'; part 'node.freezed.dart'; +final _logger = Logger('Node'); + /// A node in a game tree. /// /// The tree is implemented with a linked list of nodes, using mutable [List] of @@ -16,11 +19,7 @@ part 'node.freezed.dart'; /// It should not be directly used in a riverpod state, because it is mutable. /// It can be converted into an immutable [ViewNode], using the [view] getter. abstract class Node { - Node({ - required this.position, - this.eval, - this.opening, - }); + Node({required this.position, this.eval, this.opening}); final Position position; @@ -130,7 +129,7 @@ abstract class Node { return null; } - /// Updates all nodes. + /// Recursively applies [update] to all nodes of the tree. void updateAll(void Function(Node node) update) { update(this); for (final child in children) { @@ -138,22 +137,46 @@ abstract class Node { } } + void merge(Node other) { + if (other.eval != null) { + eval = other.eval; + } + if (other.opening != null) { + opening = other.opening; + } + for (final otherChild in other.children) { + final child = childById(otherChild.id); + if (child != null) { + child.merge(otherChild); + } else { + addChild(otherChild); + } + } + } + /// Adds a new node at the given path and returns the new path. /// /// Returns a tuple of the new path and whether the node was added. /// Returns null if the node at path does not exist. /// /// If the node already exists, it is not added again. + /// + /// If [prepend] is true, the new node is added at the beginning of the children. + /// If [replace] is true, the children of the existing node are replaced. (UciPath?, bool) addNodeAt( UciPath path, Branch newNode, { bool prepend = false, + bool replace = false, }) { final newPath = path + newNode.id; final node = nodeAtOrNull(path); if (node != null) { final existing = nodeAtOrNull(newPath) != null; if (!existing) { + if (replace) { + node.children.clear(); + } if (prepend) { node.prependChild(newNode); } else { @@ -167,17 +190,11 @@ abstract class Node { } /// Adds a list of nodes at the given path and returns the new path. - UciPath? addNodesAt( - UciPath path, - Iterable newNodes, { - bool prepend = false, - }) { + UciPath? addNodesAt(UciPath path, Iterable newNodes, {bool prepend = false}) { final node = newNodes.elementAtOrNull(0); if (node == null) return path; final (newPath, _) = addNodeAt(path, node, prepend: prepend); - return newPath != null - ? addNodesAt(newPath, newNodes.skip(1), prepend: prepend) - : null; + return newPath != null ? addNodesAt(newPath, newNodes.skip(1), prepend: prepend) : null; } /// Adds a new node with that [Move] at the given path. @@ -186,31 +203,39 @@ abstract class Node { /// Returns null if the node at path does not exist. /// /// If the node already exists, it is not added again. + /// + /// If [prepend] is true, the new node is added at the beginning of the children. + /// If [replace] is true, the children of the existing node are replaced. (UciPath?, bool) addMoveAt( UciPath path, Move move, { bool prepend = false, + bool replace = false, + Duration? clock, }) { final pos = nodeAt(path).position; - final isKingMove = - move is NormalMove && pos.board.roleAt(move.from) == Role.king; - final convertedMove = - isKingMove ? convertAltCastlingMove(move) ?? move : move; + + final potentialAltCastlingMove = + move is NormalMove && + pos.board.roleAt(move.from) == Role.king && + pos.board.roleAt(move.to) != Role.rook; + + final convertedMove = potentialAltCastlingMove ? convertAltCastlingMove(move) ?? move : move; + final (newPos, newSan) = pos.makeSan(convertedMove); final newNode = Branch( sanMove: SanMove(newSan, convertedMove), position: newPos, + comments: (clock != null) ? [PgnComment(clock: clock)] : null, ); - return addNodeAt(path, newNode, prepend: prepend); + return addNodeAt(path, newNode, prepend: prepend, replace: replace); } /// The function `convertAltCastlingMove` checks if a move is an alternative /// castling move and converts it to the corresponding standard castling move if so. Move? convertAltCastlingMove(Move move) { return altCastles.containsValue(move.uci) - ? Move.fromUci( - altCastles.entries.firstWhere((e) => e.value == move.uci).key, - ) + ? Move.parse(altCastles.entries.firstWhere((e) => e.value == move.uci).key) : move; } @@ -219,22 +244,6 @@ abstract class Node { parentAt(path).children.removeWhere((child) => child.id == path.last); } - /// Hides the variation from the node at the given path. - void hideVariationAt(UciPath path) { - final nodes = nodesOn(path).toList(); - for (int i = nodes.length - 2; i >= 0; i--) { - final node = nodes[i + 1]; - final parent = nodes[i]; - if (node is Branch && parent.children.length > 1) { - for (final child in parent.children) { - if (child.id == node.id) { - child.isHidden = true; - } - } - } - } - } - /// Promotes the node at the given path. void promoteAt(UciPath path, {required bool toMainline}) { final nodes = nodesOn(path).toList(); @@ -289,51 +298,35 @@ abstract class Node { /// Export the tree to a PGN string. /// /// Optionally, headers and initial game comments can be provided. - String makePgn([ - IMap? headers, - IList? rootComments, - ]) { + String makePgn([IMap? headers, IList? rootComments]) { final pgnNode = PgnNode(); - final List<({Node from, PgnNode to})> stack = [ - (from: this, to: pgnNode), - ]; + final List<({Node from, PgnNode to})> stack = [(from: this, to: pgnNode)]; while (stack.isNotEmpty) { final frame = stack.removeLast(); - for (int childIdx = 0; - childIdx < frame.from.children.length; - childIdx++) { + for (int childIdx = 0; childIdx < frame.from.children.length; childIdx++) { final childFrom = frame.from.children[childIdx]; final childTo = PgnChildNode( PgnNodeData( san: childFrom.sanMove.san, - startingComments: childFrom.startingComments - ?.map((c) => c.makeComment()) - .toList(), + startingComments: childFrom.startingComments?.map((c) => c.makeComment()).toList(), comments: - (childFrom.lichessAnalysisComments ?? childFrom.comments)?.map( - (c) { - final eval = childFrom.eval; - final pgnEval = eval?.cp != null - ? PgnEvaluation.pawns( - pawns: cpToPawns(eval!.cp!), - depth: eval.depth, - ) - : eval?.mate != null - ? PgnEvaluation.mate( - mate: eval!.mate, - depth: eval.depth, - ) - : c.eval; - return PgnComment( - text: c.text, - shapes: c.shapes, - clock: c.clock, - emt: c.emt, - eval: pgnEval, - ).makeComment(); - }, - ).toList(), + (childFrom.lichessAnalysisComments ?? childFrom.comments)?.map((c) { + final eval = childFrom.eval; + final pgnEval = + eval?.cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(eval!.cp!), depth: eval.depth) + : eval?.mate != null + ? PgnEvaluation.mate(mate: eval!.mate, depth: eval.depth) + : c.eval; + return PgnComment( + text: c.text, + shapes: c.shapes, + clock: c.clock, + emt: c.emt, + eval: pgnEval, + ).makeComment(); + }).toList(), nags: childFrom.nags, ), ); @@ -361,7 +354,8 @@ class Branch extends Node { super.eval, super.opening, required this.sanMove, - this.isHidden = false, + this.isComputerVariation = false, + this.isCollapsed = false, this.lichessAnalysisComments, // below are fields from dartchess [PgnNodeData] this.startingComments, @@ -369,8 +363,11 @@ class Branch extends Node { this.nags, }); + /// Whether this branch is from a variation generated by lichess computer analysis. + final bool isComputerVariation; + /// Whether the branch should be hidden in the tree view. - bool isHidden; + bool isCollapsed; /// The id of the branch, using a concise notation of associated move. UciCharPair get id => UciCharPair.fromMove(sanMove.move); @@ -394,22 +391,77 @@ class Branch extends Node { @override ViewBranch get view => ViewBranch( - position: position, - sanMove: sanMove, - eval: eval, - opening: opening, - children: IList(children.map((child) => child.view)), - isHidden: isHidden, - lichessAnalysisComments: lichessAnalysisComments?.lock, - startingComments: startingComments?.lock, - comments: comments?.lock, - nags: nags?.lock, - ); + position: position, + sanMove: sanMove, + eval: eval, + opening: opening, + children: IList(children.map((child) => child.view)), + isComputerVariation: isComputerVariation, + isCollapsed: isCollapsed, + lichessAnalysisComments: lichessAnalysisComments?.lock, + startingComments: startingComments?.lock, + comments: comments?.lock, + nags: nags?.lock, + ); /// Gets the branch at the given path @override Branch branchAt(UciPath path) => nodeAt(path) as Branch; + @override + void merge(Node other) { + if (other is Branch) { + other.lichessAnalysisComments?.forEach((c) { + if (lichessAnalysisComments == null) { + lichessAnalysisComments = [c]; + } else { + final existing = lichessAnalysisComments?.firstWhereOrNull((e) => e.text == c.text); + if (existing == null) { + lichessAnalysisComments?.add(c); + } + } + }); + other.startingComments?.forEach((c) { + if (startingComments == null) { + startingComments = [c]; + } else { + final existing = startingComments?.firstWhereOrNull((e) => e.text == c.text); + if (existing == null) { + startingComments?.add(c); + } + } + }); + other.comments?.forEach((c) { + if (comments == null) { + comments = [c]; + } else { + final existing = comments?.firstWhereOrNull((e) => e.text == c.text); + if (existing == null) { + comments?.add(c); + } + } + }); + if (other.nags != null) { + nags = other.nags; + } + } + super.merge(other); + } + + /// Gets the first available clock from the comments. + Duration? get clock { + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.clock != null, + ); + return clockComment?.clock; + } + + /// Gets the first available external eval from the comments. + ExternalEval? get externalEval { + final comment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull((c) => c.eval != null); + return comment?.eval != null ? ExternalEval.fromPgnEval(comment!.eval!) : null; + } + @override String toString() { return 'Branch(id: $id, fen: ${position.fen}, sanMove: $sanMove, eval: $eval, children: $children)'; @@ -420,35 +472,27 @@ class Branch extends Node { /// /// Represents the initial position, where no move has been played yet. class Root extends Node { - Root({ - required super.position, - super.eval, - }); + Root({required super.position, super.eval}); @override ViewRoot get view => ViewRoot( - position: position, - eval: eval, - children: IList(children.map((child) => child.view)), - ); + position: position, + eval: eval, + children: IList(children.map((child) => child.view)), + ); /// Creates a flat game tree from a PGN string. /// /// Assumes that the PGN string is valid and that the moves are legal. factory Root.fromPgnMoves(String pgn) { Position position = Chess.initial; - final root = Root( - position: position, - ); + final root = Root(position: position); Node current = root; final moves = pgn.split(' '); for (final san in moves) { final move = position.parseSan(san); position = position.playUnchecked(move!); - final nextNode = Branch( - sanMove: SanMove(san, move), - position: position, - ); + final nextNode = Branch(sanMove: SanMove(san, move), position: position); current.addChild(nextNode); current = nextNode; } @@ -465,19 +509,15 @@ class Root extends Node { bool hideVariations = false, void Function(Root root, Branch branch, bool isMainline)? onVisitNode, }) { - final root = Root( - position: PgnGame.startingPosition(game.headers), - ); + final root = Root(position: PgnGame.startingPosition(game.headers)); - final List<({PgnNode from, Node to})> stack = [ - (from: game.moves, to: root), + final List<({PgnNode from, Node to, int nesting})> stack = [ + (from: game.moves, to: root, nesting: 1), ]; while (stack.isNotEmpty) { final frame = stack.removeLast(); - for (int childIdx = 0; - childIdx < frame.from.children.length; - childIdx++) { + for (int childIdx = 0; childIdx < frame.from.children.length; childIdx++) { final childFrom = frame.from.children[childIdx]; final move = frame.to.position.parseSan(childFrom.data.san); if (move != null) { @@ -488,22 +528,32 @@ class Root extends Node { final branch = Branch( sanMove: SanMove(childFrom.data.san, move), position: newPos, - isHidden: hideVariations && childIdx > 0, - lichessAnalysisComments: - isLichessAnalysis ? comments?.toList() : null, - startingComments: isLichessAnalysis - ? null - : childFrom.data.startingComments - ?.map(PgnComment.fromPgn) - .toList(), + isCollapsed: frame.nesting > 2 || hideVariations && childIdx > 0, + isComputerVariation: isLichessAnalysis && childIdx > 0, + lichessAnalysisComments: isLichessAnalysis ? comments?.toList() : null, + startingComments: + isLichessAnalysis + ? null + : childFrom.data.startingComments?.map(PgnComment.fromPgn).toList(), comments: isLichessAnalysis ? null : comments?.toList(), nags: childFrom.data.nags, ); frame.to.addChild(branch); - stack.add((from: childFrom, to: branch)); + stack.add(( + from: childFrom, + to: branch, + nesting: + frame.from.children.length == 1 || + // mainline continuation + (childIdx == 0 && frame.nesting == 1) + ? frame.nesting + : frame.nesting + 1, + )); onVisitNode?.call(root, branch, isMainline); + } else { + _logger.warning('Invalid move: ${childFrom.data.san}, on position: ${frame.to.position}'); } } } @@ -513,6 +563,8 @@ class Root extends Node { /// An immutable view of a [Node]. abstract class ViewNode { + const ViewNode(); + UciCharPair? get id; SanMove? get sanMove; Position get position; @@ -523,11 +575,20 @@ abstract class ViewNode { IList? get comments; IList? get lichessAnalysisComments; IList? get nags; + + Iterable get mainline sync* { + ViewNode current = this; + while (current.children.isNotEmpty) { + final child = current.children.first; + yield child; + current = child; + } + } } /// An immutable view of a [Root] node. @freezed -class ViewRoot with _$ViewRoot implements ViewNode { +class ViewRoot extends ViewNode with _$ViewRoot { const ViewRoot._(); const factory ViewRoot({ required Position position, @@ -559,7 +620,7 @@ class ViewRoot with _$ViewRoot implements ViewNode { /// An immutable view of a [Branch] node. @freezed -class ViewBranch with _$ViewBranch implements ViewNode { +class ViewBranch extends ViewNode with _$ViewBranch { const ViewBranch._(); const factory ViewBranch({ @@ -567,7 +628,8 @@ class ViewBranch with _$ViewBranch implements ViewNode { required Position position, Opening? opening, required IList children, - @Default(false) bool isHidden, + @Default(false) bool isCollapsed, + required bool isComputerVariation, ClientEval? eval, IList? lichessAnalysisComments, IList? startingComments, @@ -575,27 +637,32 @@ class ViewBranch with _$ViewBranch implements ViewNode { IList? nags, }) = _ViewBranch; + /// The text comments of this branch. + Iterable get textComments { + return [ + ...lichessAnalysisComments ?? IList(const []), + ...comments ?? IList(const []), + ].where((t) => t.text?.isNotEmpty == true).map((c) => c.text!); + } + /// Has at least one non empty starting comment text. bool get hasStartingTextComment => startingComments?.any((c) => c.text?.isNotEmpty == true) == true; /// Has at least one non empty comment text. - bool get hasTextComment => - comments?.any((c) => c.text?.isNotEmpty == true) == true; - - /// Has at least one non empty lichess analysis comment text. - bool get hasLichessAnalysisTextComment => - lichessAnalysisComments?.any((c) => c.text?.isNotEmpty == true) == true; + bool get hasTextComment => textComments.isNotEmpty; Duration? get clock { - final clockComment = (lichessAnalysisComments ?? comments) - ?.firstWhereOrNull((c) => c.clock != null); + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.clock != null, + ); return clockComment?.clock; } Duration? get elapsedMoveTime { - final clockComment = (lichessAnalysisComments ?? comments) - ?.firstWhereOrNull((c) => c.emt != null); + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.emt != null, + ); return clockComment?.emt; } diff --git a/lib/src/model/common/perf.dart b/lib/src/model/common/perf.dart index 5df2cbe0e5..8b297cad48 100644 --- a/lib/src/model/common/perf.dart +++ b/lib/src/model/common/perf.dart @@ -73,11 +73,11 @@ enum Perf { static final IMap nameMap = IMap(Perf.values.asNameMap()); } -String _titleKey(String title) => - title.toLowerCase().replaceAll(RegExp('[ -_]'), ''); +String _titleKey(String title) => title.toLowerCase().replaceAll(RegExp('[ -_]'), ''); -final IMap _lowerCaseTitleMap = - Perf.nameMap.map((key, value) => MapEntry(_titleKey(value.title), value)); +final IMap _lowerCaseTitleMap = Perf.nameMap.map( + (key, value) => MapEntry(_titleKey(value.title), value), +); extension PerfExtension on Pick { Perf asPerfOrThrow() { @@ -104,9 +104,7 @@ extension PerfExtension on Pick { return perf; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Perf", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Perf"); } Perf? asPerfOrNull() { diff --git a/lib/src/model/common/preloaded_data.dart b/lib/src/model/common/preloaded_data.dart new file mode 100644 index 0000000000..da0531202a --- /dev/null +++ b/lib/src/model/common/preloaded_data.dart @@ -0,0 +1,60 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/session_storage.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/utils/system.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'preloaded_data.g.dart'; + +typedef PreloadedData = + ({ + PackageInfo packageInfo, + BaseDeviceInfo deviceInfo, + AuthSessionState? userSession, + String sri, + int engineMaxMemoryInMb, + }); + +@Riverpod(keepAlive: true) +Future preloadedData(Ref ref) async { + final sessionStorage = ref.watch(sessionStorageProvider); + + final pInfo = await PackageInfo.fromPlatform(); + final deviceInfo = await DeviceInfoPlugin().deviceInfo; + + // Generate a socket random identifier and store it for the app lifetime + String? storedSri; + try { + storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); + if (storedSri == null) { + final sri = genRandomString(12); + await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); + storedSri = sri; + } + } on PlatformException catch (_) { + // Clear all secure storage if an error occurs because it probably means the key has + // been lost + await SecureStorage.instance.deleteAll(); + } + + final sri = storedSri ?? genRandomString(12); + + final userSession = await sessionStorage.read(); + + final physicalMemory = await System.instance.getTotalRam() ?? 256.0; + final engineMaxMemory = (physicalMemory / 10).ceil(); + + return ( + packageInfo: pInfo, + deviceInfo: deviceInfo, + userSession: userSession, + sri: sri, + engineMaxMemoryInMb: engineMaxMemory, + ); +} diff --git a/lib/src/model/common/service/move_feedback.dart b/lib/src/model/common/service/move_feedback.dart index f441b7abc1..ff11f5d50b 100644 --- a/lib/src/model/common/service/move_feedback.dart +++ b/lib/src/model/common/service/move_feedback.dart @@ -1,4 +1,5 @@ import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -6,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'move_feedback.g.dart'; @Riverpod(keepAlive: true) -MoveFeedbackService moveFeedbackService(MoveFeedbackServiceRef ref) { +MoveFeedbackService moveFeedbackService(Ref ref) { final soundService = ref.watch(soundServiceProvider); return MoveFeedbackService(soundService, ref); } @@ -15,7 +16,7 @@ class MoveFeedbackService { MoveFeedbackService(this._soundService, this._ref); final SoundService _soundService; - final MoveFeedbackServiceRef _ref; + final Ref _ref; void moveFeedback({bool check = false}) { _soundService.play(Sound.move); diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index 875de4a4ea..c7da648e31 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -1,7 +1,12 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sound_effect/sound_effect.dart'; @@ -9,20 +14,13 @@ part 'sound_service.g.dart'; final _soundEffectPlugin = SoundEffect(); +final _logger = Logger('SoundService'); + // Must match name of files in assets/sounds/standard -enum Sound { - move, - capture, - lowTime, - dong, - error, - confirmation, - puzzleStormEnd, - clock, -} +enum Sound { move, capture, lowTime, dong, error, confirmation, puzzleStormEnd, clock } @Riverpod(keepAlive: true) -SoundService soundService(SoundServiceRef ref) { +SoundService soundService(Ref ref) { final service = SoundService(ref); ref.onDispose(() => service.release()); return service; @@ -33,14 +31,9 @@ final _extension = defaultTargetPlatform == TargetPlatform.iOS ? 'aifc' : 'mp3'; const Set _emtpySet = {}; /// Loads all sounds of the given [SoundTheme]. -Future _loadAllSounds( - SoundTheme soundTheme, { - Set excluded = _emtpySet, -}) async { +Future _loadAllSounds(SoundTheme soundTheme, {Set excluded = _emtpySet}) async { await Future.wait( - Sound.values - .where((s) => !excluded.contains(s)) - .map((sound) => _loadSound(soundTheme, sound)), + Sound.values.where((s) => !excluded.contains(s)).map((sound) => _loadSound(soundTheme, sound)), ); } @@ -64,14 +57,27 @@ Future _loadSound(SoundTheme theme, Sound sound) async { class SoundService { SoundService(this._ref); - final SoundServiceRef _ref; + final Ref _ref; - /// Initialize the sound service with the given sound theme. + /// Initialize the sound service. /// /// This will load the sounds from assets and make them ready to be played. - Future initialize(SoundTheme theme) async { - await _soundEffectPlugin.initialize(); - await _loadAllSounds(theme); + /// This should be called once when the app starts. + static Future initialize() async { + try { + final stored = LichessBinding.instance.sharedPreferences.getString( + PrefCategory.general.storageKey, + ); + final theme = + (stored != null + ? GeneralPrefs.fromJson(jsonDecode(stored) as Map) + : GeneralPrefs.defaults) + .soundTheme; + await _soundEffectPlugin.initialize(); + await _loadAllSounds(theme); + } catch (e) { + _logger.warning('Failed to initialize sound service: $e'); + } } /// Play the given sound if sound is enabled. @@ -89,10 +95,7 @@ class SoundService { /// This will release the previous sounds and load the new ones. /// /// If [playSound] is true, a move sound will be played. - Future changeTheme( - SoundTheme theme, { - bool playSound = false, - }) async { + Future changeTheme(SoundTheme theme, {bool playSound = false}) async { await _soundEffectPlugin.release(); await _soundEffectPlugin.initialize(); await _loadSound(theme, Sound.move); diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart index 58a503ae86..cb258fee08 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/model/common/socket.dart @@ -1,665 +1,6 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' as math; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/utils/device_info.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; -import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; part 'socket.freezed.dart'; -part 'socket.g.dart'; - -const kSRIStorageKey = 'socket_random_identifier'; -const kDefaultSocketRoute = '/socket/v5'; - -const _kDefaultConnectTimeout = Duration(seconds: 10); -const _kPingDelay = Duration(milliseconds: 2500); -const _kPingMaxLag = Duration(seconds: 9); -const _kAutoReconnectDelay = Duration(milliseconds: 3500); -const _kResendAckDelay = Duration(milliseconds: 1500); -const _kIdleTimeout = Duration(seconds: 2); -const _kDisconnectOnBackgroundTimeout = Duration(minutes: 5); - -final _logger = Logger('Socket'); - -/// Set of topics that are allowed to be broadcasted to the global stream. -const _globalSocketStreamAllowedTopics = { - 'n', - 'message', -}; - -final _globalStreamController = StreamController.broadcast(); - -/// The global socket broadcast stream. -/// -/// Only a subset of topics are allowed to be broadcasted to the global stream: -/// - 'n' (number of players and games currently on the server) -/// - 'message' -final socketGlobalStream = _globalStreamController.stream; - -/// Creates a WebSocket URI for the lichess server. -Uri lichessWSUri( - String unencodedPath, [ - Map? queryParameters, -]) => - kLichessWSHost.startsWith('localhost') - ? Uri( - scheme: 'ws', - host: kLichessWSHost.split(':')[0], - port: int.parse(kLichessWSHost.split(':')[1]), - path: unencodedPath, - queryParameters: queryParameters, - ) - : Uri( - scheme: 'wss', - host: kLichessWSHost, - path: unencodedPath, - queryParameters: queryParameters, - ); - -/// A lichess WebSocket client. -/// -/// Handles authentication: -/// - adds the following headers on connect: -/// - Authorization header when a token has been stored, -/// - User-Agent header -/// -/// Handles low-level ping/pong protocol, message acks, and automatic reconnections. -class SocketClient { - SocketClient( - this.route, { - required this.channelFactory, - required this.getSession, - required this.packageInfo, - required this.deviceInfo, - required this.sri, - this.onStreamListen, - this.onStreamCancel, - this.pingDelay = _kPingDelay, - this.pingMaxLag = _kPingMaxLag, - this.autoReconnectDelay = _kAutoReconnectDelay, - this.resendAckDelay = _kResendAckDelay, - }); - - final WebSocketChannelFactory channelFactory; - - final AuthSessionState? Function() getSession; - - final PackageInfo packageInfo; - - final BaseDeviceInfo deviceInfo; - - /// The Socket Random Identifier. - final String sri; - - /// The route to connect to. - final Uri route; - - /// The delay between the next ping after receiving a pong. - final Duration pingDelay; - - /// The maximum lag before considering the connection as lost. - final Duration pingMaxLag; - - /// The delay before reconnecting after a connection failure. - final Duration autoReconnectDelay; - - /// The delay before resending an ack. - final Duration resendAckDelay; - - /// Called when the first listener is added to the socket stream. - final VoidCallback? onStreamListen; - - /// Called when the last listener is removed from the socket stream. - final VoidCallback? onStreamCancel; - - late final StreamController _streamController = - StreamController.broadcast( - onListen: onStreamListen, - onCancel: onStreamCancel, - ); - - late final StreamController _socketOpenController = - StreamController.broadcast(); - - Completer _firstConnection = Completer(); - - Timer? _pingTimer; - Timer? _reconnectTimer; - Timer? _ackResendTimer; - int _pongCount = 0; - DateTime _lastPing = DateTime.now(); - - final _averageLag = ValueNotifier(Duration.zero); - - StreamSubscription? _socketStreamSubscription; - - /// The list of acknowledgeable messages. - final List<(DateTime, int, Map)> _acks = []; - - /// The current number of connections attempted. - int nbConnectionAttempts = 0; - - /// The current number of successful connections. - int nbConnectionSuccess = 0; - - /// The current ack id. Incremented for each ack. - int _ackId = 1; - - /// The current WebSocket channel. - WebSocketChannel? _channel; - - /// Gets the current WebSocket sink - WebSocketSink? get _sink => _channel?.sink; - - /// The socket events broadcast stream. - Stream get stream => _streamController.stream; - - /// The stream that emits each time the socket is (re)connected. - Stream get connectedStream => _socketOpenController.stream; - - /// The average lag computed from ping/pong protocol. - /// - /// A duration of zero means the socket is not connected. - ValueListenable get averageLag => _averageLag; - - /// Whether the socket is actively trying to connect or is connected. - bool get isActive => nbConnectionAttempts > 0; - - /// Whether the socket is connected. - bool get isConnected => averageLag.value != Duration.zero; - - /// Whether the client is disposed. If true the client cannot be reconnected, or - /// be listened to. - bool isDisposed = false; - - /// A [Future] that completes when the first connection is established. - Future get firstConnection => _firstConnection.future; - - /// Connect or reconnect the WebSocket. - Future connect() async { - if (isDisposed) { - throw StateError('SocketClient is disposed, cannot connect.'); - } - - _disconnect(); - _pongCount = 0; - _reconnectTimer?.cancel(); - _ackResendTimer?.cancel(); - _ackResendTimer = Timer.periodic(resendAckDelay, (_) => _resendAcks()); - - final session = getSession(); - final uri = lichessWSUri(route.path); - final Map headers = session != null - ? { - 'Authorization': 'Bearer ${signBearerToken(session.token)}', - } - : {}; - WebSocket.userAgent = - makeUserAgent(packageInfo, deviceInfo, sri, session?.user); - - _logger.info('Creating WebSocket connection to $route'); - - nbConnectionAttempts++; - - try { - final channel = await channelFactory.create( - uri.toString(), - headers: headers, - timeout: _kDefaultConnectTimeout, - ); - - _channel = channel; - - _socketStreamSubscription?.cancel(); - _socketStreamSubscription = channel.stream.map((raw) { - if (raw == '0') { - return SocketEvent.pong; - } - return SocketEvent.fromJson( - jsonDecode(raw as String) as Map, - ); - }).listen(_handleEvent); - - _logger.fine('WebSocket connection to $route established.'); - - nbConnectionSuccess++; - - if (nbConnectionSuccess == 1) { - _firstConnection.complete(); - } - - _averageLag.value = Duration.zero; - _sendPing(); - _schedulePing(pingDelay); - - if (_socketOpenController.hasListener) { - _socketOpenController.add(null); - } - _resendAcks(); - } catch (error) { - _logger.severe('WebSocket connection failed: $error', error); - _averageLag.value = Duration.zero; - _scheduleReconnect(autoReconnectDelay); - } - } - - /// Sends a message to the websocket. - void send( - String topic, - Object? data, { - bool? ackable, - bool? withLag, - }) { - Map message; - - if (ackable == true) { - final ackId = _ackId++; - message = { - 't': topic, - 'd': { - if (data != null && data is Map) ...data, - 'a': ackId, - if (withLag == true) 'l': _averageLag.value.inMilliseconds, - }, - }; - _acks.add((DateTime.now(), ackId, message)); - } else { - message = { - 't': topic, - if (data != null && data is Map) - 'd': { - ...data, - if (withLag == true) 'l': _averageLag.value.inMilliseconds, - } - else if (data != null) - 'd': data, - }; - } - - _sink?.add(jsonEncode(message)); - } - - /// Closes the WebSocket connection and disposes the client. - /// - /// The [SocketPool] will call this method when the client is no longer needed. - void _dispose() { - _socketStreamSubscription?.cancel(); - _pingTimer?.cancel(); - _reconnectTimer?.cancel(); - _ackResendTimer?.cancel(); - _streamController.close(); - _averageLag.dispose(); - isDisposed = true; - _disconnect(); - } - - /// Closes the WebSocket connection when temporarily not needed (by default - /// this is when we open another one). - /// - /// The connection can be reopend later by calling [connect]. This will reset - /// the [firstConnection] future and the [nbConnectionAttempts] and [nbConnectionSuccess] counters. - Future close() { - nbConnectionAttempts = 0; - nbConnectionSuccess = 0; - _firstConnection = Completer(); - return _disconnect(); - } - - /// Disconnects websocket connection. - /// - /// Returns a [Future] that completes when the connection is closed. - Future _disconnect() { - final future = _sink?.close().then((_) { - _logger.fine('WebSocket connection to $route was properly closed.'); - if (isDisposed) { - return; - } - _averageLag.value = Duration.zero; - }).catchError((Object? error) { - _logger.warning( - 'WebSocket connection to $route could not be closed: $error', - error, - ); - if (isDisposed) { - return; - } - _averageLag.value = Duration.zero; - }) ?? - Future.value(); - _channel = null; - _socketStreamSubscription?.cancel(); - _pingTimer?.cancel(); - _reconnectTimer?.cancel(); - _ackResendTimer?.cancel(); - - return future; - } - - void _handleEvent(SocketEvent event) { - switch (event.topic) { - case '_pong': - case 'n': - _handlePong(pingDelay); - case 'ack': - _onServerAck(event); - } - - if (event != SocketEvent.pong && event.topic != 'ack') { - if (_streamController.hasListener) { - _streamController.add(event); - } - if (_globalStreamController.hasListener && - _globalSocketStreamAllowedTopics.contains(event.topic)) { - _globalStreamController.add(event); - } - } - } - - void _schedulePing(Duration delay) { - _pingTimer?.cancel(); - _pingTimer = Timer(delay, _sendPing); - } - - /// Sends a ping to the server. - void _sendPing() { - _sink?.add( - _pongCount % 10 == 2 - ? jsonEncode({ - 't': 'p', - 'l': (_averageLag.value.inMilliseconds * 0.1).round(), - }) - : 'p', - ); - _lastPing = DateTime.now(); - _scheduleReconnect(pingMaxLag); - } - - void _handlePong(Duration pingDelay) { - _reconnectTimer?.cancel(); - if (_pongCount == 0) { - _logger.fine('Ping/pong protocol for $route established.'); - } - _schedulePing(pingDelay); - _pongCount++; - final currentLag = Duration( - milliseconds: - math.min(DateTime.now().difference(_lastPing).inMilliseconds, 10000), - ); - - // Average first 4 pings, then switch to decaying average. - final mix = _pongCount > 4 ? 0.1 : 1 / _pongCount; - _averageLag.value += (currentLag - _averageLag.value) * mix; - } - - void _scheduleReconnect(Duration delay) { - _reconnectTimer?.cancel(); - _reconnectTimer = Timer(delay, () { - _averageLag.value = Duration.zero; - _logger.fine('Reconnecting WebSocket.'); - connect(); - }); - } - - void _onServerAck(SocketEvent event) { - if (event.data is! int) { - return; - } - _acks.removeWhere((rec) => rec.$2 == event.data); - } - - void _resendAcks() { - final resendCutoff = - DateTime.now().subtract(const Duration(milliseconds: 2500)); - for (final (at, _, ack) in _acks) { - if (at.isBefore(resendCutoff)) { - _sink?.add(jsonEncode(ack)); - } - } - } -} - -/// Service that manages a pool of socket clients. -/// -/// The pool is used to manage multiple socket connections to different routes. -/// It ensures that only one connection is active at a time, and that a client -/// created for a route other than the lichess default socket route is disposed -/// when it becomes idle. -/// -/// A client for the default route is created upon initialization and is never -/// disposed. -/// The pool is responsible for creating and disposing other clients and that -/// there is always an active client. -/// When a requested client is disposed, the pool will automatically reconnect -/// the default client. -class SocketPool { - SocketPool( - this._ref, { - this.idleTimeout = _kIdleTimeout, - }) { - // Create a default socket client. This one is never disposed. - final client = SocketClient( - _currentRoute, - sri: _ref.read(sriProvider), - channelFactory: _ref.read(webSocketChannelFactoryProvider), - getSession: () => _ref.read(authSessionProvider), - packageInfo: _ref.read(packageInfoProvider), - deviceInfo: _ref.read(deviceInfoProvider), - pingDelay: const Duration(seconds: 25), - ); - - client.averageLag.addListener(() { - if (_currentRoute == client.route) { - _averageLag.value = client.averageLag.value; - } - }); - - _pool[_currentRoute] = client; - } - - final SocketPoolRef _ref; - - /// The delay before closing the socket if idle (no subscription). - final Duration idleTimeout; - - final _averageLag = ValueNotifier(Duration.zero); - - /// The average lag computed from ping/pong protocol of the current active route. - /// - /// A duration of zero means the socket is not connected. - ValueListenable get averageLag => _averageLag; - - /// The current socket route. - Uri _currentRoute = Uri(path: kDefaultSocketRoute); - - /// The current socket client. - SocketClient get currentClient => _pool[_currentRoute]!; - - /// The socket clients pool. - final Map _pool = {}; - final Map _disposeTimers = {}; - - /// Opens a socket connection to the given [route]. - /// - /// It will use an existing connection if it is already active, unless - /// [forceReconnect] is set to true. - /// Any other active connection will be closed. - SocketClient open( - Uri route, { - bool? forceReconnect, - }) { - _currentRoute = route; - - if (_pool[route] == null) { - _pool[route] = SocketClient( - route, - channelFactory: _ref.read(webSocketChannelFactoryProvider), - getSession: () => _ref.read(authSessionProvider), - packageInfo: _ref.read(packageInfoProvider), - deviceInfo: _ref.read(deviceInfoProvider), - sri: _ref.read(sriProvider), - onStreamListen: () { - _disposeTimers[route]?.cancel(); - }, - onStreamCancel: () { - // Schedule the socket to be closed if idle, after a short delay to - // avoid unnecessary reconnections. - _disposeTimers[route]?.cancel(); - _disposeTimers[route] = Timer(idleTimeout, () { - _logger.fine('Disposing idle socket on $route.'); - _pool[route]?._dispose(); - _pool.remove(route); - // if during the idle time no new socket is requested, we reconnect - // the default socket - if (route == _currentRoute) { - _currentRoute = Uri(path: kDefaultSocketRoute); - if (!currentClient.isActive) { - currentClient.connect(); - } - } - }); - }, - ); - } - - final client = _pool[route]!; - - client.averageLag.addListener(() { - if (_currentRoute == client.route) { - _averageLag.value = client.averageLag.value; - } - }); - - // ensure there is only one active connection - _pool.forEach((k, c) { - if (k != route) { - c.close(); - } - }); - - if (forceReconnect == true || !client.isActive) { - client.connect(); - } - - return client; - } - - /// Disposes the pool and all its clients. - void dispose() { - _averageLag.dispose(); - _pool.forEach((_, c) => c._dispose()); - } -} - -@Riverpod(keepAlive: true) -SocketPool socketPool(SocketPoolRef ref) { - final pool = SocketPool(ref); - Timer? closeInBackgroundTimer; - - final appLifecycleListener = AppLifecycleListener( - onHide: () { - closeInBackgroundTimer?.cancel(); - closeInBackgroundTimer = Timer( - _kDisconnectOnBackgroundTimeout, - () { - _logger.info( - 'App is in background for ${_kDisconnectOnBackgroundTimeout.inMinutes}m, closing socket.', - ); - pool.currentClient.close(); - }, - ); - }, - onShow: () { - closeInBackgroundTimer?.cancel(); - if (!pool.currentClient.isActive) { - pool.currentClient.connect(); - } - }, - ); - - ref.onDispose(() { - pool.dispose(); - closeInBackgroundTimer?.cancel(); - appLifecycleListener.dispose(); - }); - - return pool; -} - -/// Socket Random Identifier. -@Riverpod(keepAlive: true) -String sri(SriRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.sri; -} - -/// Average lag computed from WebSocket ping/pong protocol. -@riverpod -class AverageLag extends _$AverageLag { - @override - Duration build() { - final listenable = ref.watch(socketPoolProvider).averageLag; - - listenable.addListener(_listener); - - ref.onDispose(() { - listenable.removeListener(_listener); - }); - - return listenable.value; - } - - void _listener() { - final newLag = ref.read(socketPoolProvider).averageLag.value; - if (state != newLag) { - state = newLag; - } - } -} - -@Riverpod(keepAlive: true) -WebSocketChannelFactory webSocketChannelFactory( - WebSocketChannelFactoryRef ref, -) { - return const WebSocketChannelFactory(); -} - -/// A factory to create a [WebSocketChannel]. -/// -/// This is useful to be able to mock the [WebSocketChannel] in tests. -class WebSocketChannelFactory { - const WebSocketChannelFactory(); - - /// Creates a [WebSocketChannel] from the given [url]. - /// - /// Throws a [TimeoutException] if the connection takes too long. - /// Throws a [SocketException] if the connection fails. - Future create( - String url, { - Map? headers, - Duration timeout = const Duration(seconds: 10), - }) async { - final socket = - await WebSocket.connect(url, headers: headers).timeout(timeout); - - return IOWebSocketChannel(socket); - } -} /// A socket event. @freezed @@ -680,10 +21,7 @@ class SocketEvent with _$SocketEvent { factory SocketEvent.fromJson(Map json) { if (json['t'] == null) { if (json['v'] != null) { - return SocketEvent( - topic: '_version', - version: json['v'] as int, - ); + return SocketEvent(topic: '_version', version: json['v'] as int); } else { assert(false, 'Unsupported socket event json: $json'); return pong; @@ -693,16 +31,9 @@ class SocketEvent with _$SocketEvent { if (topic == 'n') { return SocketEvent( topic: topic, - data: { - 'nbPlayers': json['d'] as int, - 'nbGames': json['r'] as int, - }, + data: {'nbPlayers': json['d'] as int, 'nbGames': json['r'] as int}, ); } - return SocketEvent( - topic: topic, - data: json['d'], - version: json['v'] as int?, - ); + return SocketEvent(topic: topic, data: json['d'], version: json['v'] as int?); } } diff --git a/lib/src/model/common/speed.dart b/lib/src/model/common/speed.dart index b608fdf9aa..b3656c12f2 100644 --- a/lib/src/model/common/speed.dart +++ b/lib/src/model/common/speed.dart @@ -47,9 +47,7 @@ extension SpeedExtension on Pick { final speed = Speed.nameMap[value]; if (speed != null) return speed; } - throw PickException( - "value $value at $debugParsingExit can't be casted to Speed", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Speed"); } Speed? asSpeedOrNull() { diff --git a/lib/src/model/common/time_increment.dart b/lib/src/model/common/time_increment.dart index 10e47608a6..adc4558ae7 100644 --- a/lib/src/model/common/time_increment.dart +++ b/lib/src/model/common/time_increment.dart @@ -5,8 +5,12 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; /// A pair of time and increment in seconds used as game clock @immutable class TimeIncrement { - const TimeIncrement(this.time, this.increment) - : assert(time >= 0 && increment >= 0); + const TimeIncrement(this.time, this.increment) : assert(time >= 0 && increment >= 0); + + TimeIncrement.fromDurations(Duration time, Duration increment) + : time = time.inSeconds, + increment = increment.inSeconds, + assert(time >= Duration.zero && increment >= Duration.zero); /// Clock initial time in seconds final int time; @@ -15,13 +19,10 @@ class TimeIncrement { final int increment; TimeIncrement.fromJson(Map json) - : time = json['time'] as int, - increment = json['increment'] as int; + : time = json['time'] as int, + increment = json['increment'] as int; - Map toJson() => { - 'time': time, - 'increment': increment, - }; + Map toJson() => {'time': time, 'increment': increment}; /// Returns the estimated duration of the game, with increment * 40 added to /// the initial time. diff --git a/lib/src/model/common/uci.dart b/lib/src/model/common/uci.dart index 8dadf811e7..85185f268c 100644 --- a/lib/src/model/common/uci.dart +++ b/lib/src/model/common/uci.dart @@ -24,7 +24,7 @@ class UciCharPair with _$UciCharPair { /// /// Throws an [ArgumentError] if the move is invalid. factory UciCharPair.fromUci(String uci) { - final move = Move.fromUci(uci); + final move = Move.parse(uci); if (move == null) { throw ArgumentError('Invalid uci $uci'); } @@ -32,43 +32,25 @@ class UciCharPair with _$UciCharPair { } factory UciCharPair.fromMove(Move move) => switch (move) { - NormalMove(from: final f, to: final t, promotion: final p) => - UciCharPair( - String.fromCharCode(35 + f), - String.fromCharCode( - p != null - ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + squareFile(t) - : 35 + t, - ), - ), - DropMove(to: final t, role: final r) => UciCharPair( - String.fromCharCode(35 + t), - String.fromCharCode(35 + 64 + 8 * 5 + _dropRoles.indexOf(r)), - ), - }; - - factory UciCharPair.fromJson(Map json) => - _$UciCharPairFromJson(json); + NormalMove(from: final f, to: final t, promotion: final p) => UciCharPair( + String.fromCharCode(35 + f), + String.fromCharCode(p != null ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file : 35 + t), + ), + DropMove(to: final t, role: final r) => UciCharPair( + String.fromCharCode(35 + t), + String.fromCharCode(35 + 64 + 8 * 5 + _dropRoles.indexOf(r)), + ), + }; + + factory UciCharPair.fromJson(Map json) => _$UciCharPairFromJson(json); @override String toString() => '$a$b'; } -const _promotionRoles = [ - Role.queen, - Role.rook, - Role.bishop, - Role.knight, - Role.king, -]; - -const _dropRoles = [ - Role.queen, - Role.rook, - Role.bishop, - Role.knight, - Role.pawn, -]; +const _promotionRoles = [Role.queen, Role.rook, Role.bishop, Role.knight, Role.king]; + +const _dropRoles = [Role.queen, Role.rook, Role.bishop, Role.knight, Role.pawn]; /// Compact representation of a path to a game node made from concatenated /// UciCharPair strings. @@ -90,6 +72,8 @@ class UciPath with _$UciPath { return UciPath(path.toString()); } + factory UciPath.join(UciPath a, UciPath b) => UciPath(a.value + b.value); + /// Creates a UciPath from a list of UCI moves. /// /// Throws an [ArgumentError] if any of the moves is invalid. @@ -109,22 +93,17 @@ class UciPath with _$UciPath { int get size => value.length ~/ 2; - UciCharPair? get head => - value.isEmpty ? null : UciCharPair(value[0], value[1]); + UciCharPair? get head => value.isEmpty ? null : UciCharPair(value[0], value[1]); - UciCharPair? get last => value.isEmpty - ? null - : UciCharPair(value[value.length - 2], value[value.length - 1]); + UciCharPair? get last => + value.isEmpty ? null : UciCharPair(value[value.length - 2], value[value.length - 1]); - UciPath get tail => - value.isEmpty ? UciPath.empty : UciPath(value.substring(2)); + UciPath get tail => value.isEmpty ? UciPath.empty : UciPath(value.substring(2)); - UciPath get penultimate => value.isEmpty - ? UciPath.empty - : UciPath(value.substring(0, value.length - 2)); + UciPath get penultimate => + value.isEmpty ? UciPath.empty : UciPath(value.substring(0, value.length - 2)); bool get isEmpty => value.isEmpty; - factory UciPath.fromJson(Map json) => - _$UciPathFromJson(json); + factory UciPath.fromJson(Map json) => _$UciPathFromJson(json); } diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart new file mode 100644 index 0000000000..94dbbbc228 --- /dev/null +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'coordinate_training_controller.freezed.dart'; +part 'coordinate_training_controller.g.dart'; + +enum Guess { correct, incorrect } + +@riverpod +class CoordinateTrainingController extends _$CoordinateTrainingController { + final _random = Random(DateTime.now().millisecondsSinceEpoch); + + final _stopwatch = Stopwatch(); + + Timer? _updateTimer; + + @override + CoordinateTrainingState build() { + ref.onDispose(() { + _updateTimer?.cancel(); + }); + final sideChoice = ref.watch( + coordinateTrainingPreferencesProvider.select((value) => value.sideChoice), + ); + return CoordinateTrainingState(orientation: _getOrientation(sideChoice)); + } + + void startTraining(Duration? timeLimit) { + final currentCoord = _randomCoord(); + state = state.copyWith( + currentCoord: currentCoord, + nextCoord: _randomCoord(previous: currentCoord), + score: 0, + timeLimit: timeLimit, + elapsed: Duration.zero, + lastGuess: null, + ); + + _updateTimer?.cancel(); + _stopwatch.reset(); + _stopwatch.start(); + _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + if (state.timeLimit != null && _stopwatch.elapsed > state.timeLimit!) { + _finishTraining(); + } else { + state = state.copyWith(elapsed: _stopwatch.elapsed); + } + }); + } + + void _finishTraining() { + // TODO save score in local storage here (and display high score and/or average score in UI) + final orientation = _getOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); + _updateTimer?.cancel(); + state = CoordinateTrainingState( + lastGuess: state.lastGuess, + lastScore: state.score, + orientation: orientation, + ); + } + + void abortTraining() { + final orientation = _getOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); + _updateTimer?.cancel(); + state = CoordinateTrainingState(orientation: orientation); + } + + Side _getOrientation(SideChoice choice) => switch (choice) { + SideChoice.white => Side.white, + SideChoice.black => Side.black, + SideChoice.random => _randomSide(), + }; + + /// Generate a random side + Side _randomSide() => Side.values[Random().nextInt(Side.values.length)]; + + /// Generate a random square that is not the same as the [previous] square + Square _randomCoord({Square? previous}) { + while (true) { + final square = Square.values[_random.nextInt(Square.values.length)]; + if (square != previous) { + return square; + } + } + } + + void guessCoordinate(Square coord) { + final correctGuess = coord == state.currentCoord; + + if (correctGuess) { + state = state.copyWith( + currentCoord: state.nextCoord, + nextCoord: _randomCoord(previous: state.nextCoord), + score: state.score + 1, + ); + } + + state = state.copyWith(lastGuess: correctGuess ? Guess.correct : Guess.incorrect); + } +} + +@freezed +class CoordinateTrainingState with _$CoordinateTrainingState { + const CoordinateTrainingState._(); + + const factory CoordinateTrainingState({ + @Default(null) Square? currentCoord, + @Default(null) Square? nextCoord, + @Default(0) int score, + @Default(null) Duration? timeLimit, + @Default(null) Duration? elapsed, + @Default(null) Guess? lastGuess, + required Side orientation, + @Default(null) int? lastScore, + }) = _CoordinateTrainingState; + + bool get trainingActive => elapsed != null; + + double? get timeFractionElapsed => + (elapsed != null && timeLimit != null) + ? elapsed!.inMilliseconds / timeLimit!.inMilliseconds + : null; +} diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart new file mode 100644 index 0000000000..09273ce082 --- /dev/null +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'coordinate_training_preferences.freezed.dart'; +part 'coordinate_training_preferences.g.dart'; + +@riverpod +class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.coordinateTraining; + + // ignore: avoid_public_notifier_properties + @override + CoordinateTrainingPrefs get defaults => CoordinateTrainingPrefs.defaults; + + @override + CoordinateTrainingPrefs fromJson(Map json) { + return CoordinateTrainingPrefs.fromJson(json); + } + + @override + CoordinateTrainingPrefs build() { + return fetch(); + } + + Future setShowCoordinates(bool showCoordinates) { + return save(state.copyWith(showCoordinates: showCoordinates)); + } + + Future setShowPieces(bool showPieces) { + return save(state.copyWith(showPieces: showPieces)); + } + + Future setMode(TrainingMode mode) { + return save(state.copyWith(mode: mode)); + } + + Future setTimeChoice(TimeChoice timeChoice) { + return save(state.copyWith(timeChoice: timeChoice)); + } + + Future setSideChoice(SideChoice sideChoice) { + return save(state.copyWith(sideChoice: sideChoice)); + } +} + +enum TimeChoice { + thirtySeconds(Duration(seconds: 30)), + unlimited(null); + + const TimeChoice(this.duration); + + final Duration? duration; + + // TODO l10n + Widget label(AppLocalizations l10n) { + switch (this) { + case TimeChoice.thirtySeconds: + return const Text('30s'); + case TimeChoice.unlimited: + return const Icon(Icons.all_inclusive); + } + } +} + +enum TrainingMode { + findSquare, + nameSquare; + + // TODO l10n + String label(AppLocalizations l10n) { + switch (this) { + case TrainingMode.findSquare: + return 'Find Square'; + case TrainingMode.nameSquare: + return 'Name Square'; + } + } +} + +@Freezed(fromJson: true, toJson: true) +class CoordinateTrainingPrefs with _$CoordinateTrainingPrefs implements Serializable { + const CoordinateTrainingPrefs._(); + + const factory CoordinateTrainingPrefs({ + required bool showCoordinates, + required bool showPieces, + required TrainingMode mode, + required TimeChoice timeChoice, + required SideChoice sideChoice, + }) = _CoordinateTrainingPrefs; + + static const defaults = CoordinateTrainingPrefs( + showCoordinates: false, + showPieces: true, + mode: TrainingMode.findSquare, + timeChoice: TimeChoice.thirtySeconds, + sideChoice: SideChoice.random, + ); + + factory CoordinateTrainingPrefs.fromJson(Map json) { + return _$CoordinateTrainingPrefsFromJson(json); + } +} diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index 36fc0d1e56..fe1a259177 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -12,32 +14,27 @@ import 'offline_correspondence_game.dart'; part 'correspondence_game_storage.g.dart'; @Riverpod(keepAlive: true) -CorrespondenceGameStorage correspondenceGameStorage( - CorrespondenceGameStorageRef ref, -) { - final db = ref.watch(databaseProvider); +Future correspondenceGameStorage(Ref ref) async { + final db = await ref.watch(databaseProvider.future); return CorrespondenceGameStorage(db, ref); } @riverpod -Future> - offlineOngoingCorrespondenceGames( - OfflineOngoingCorrespondenceGamesRef ref, +Future> offlineOngoingCorrespondenceGames( + Ref ref, ) async { final session = ref.watch(authSessionProvider); // cannot use ref.watch because it would create a circular dependency // as we invalidate this provider in the storage save and delete methods - final storage = ref.read(correspondenceGameStorageProvider); + final storage = await ref.read(correspondenceGameStorageProvider.future); final data = await storage.fetchOngoingGames(session?.user.id); - return data.sort( - (a, b) { - final aIsMyTurn = a.$2.isMyTurn; - final bIsMyTurn = b.$2.isMyTurn; - if (aIsMyTurn && !bIsMyTurn) return -1; - if (!aIsMyTurn && bIsMyTurn) return 1; - return b.$1.compareTo(a.$1); - }, - ); + return data.sort((a, b) { + final aIsMyTurn = a.$2.isMyTurn; + final bIsMyTurn = b.$2.isMyTurn; + if (aIsMyTurn && !bIsMyTurn) return -1; + if (!aIsMyTurn && bIsMyTurn) return 1; + return b.$1.compareTo(a.$1); + }); } const kCorrespondenceStorageTable = 'correspondence_game'; @@ -47,19 +44,14 @@ const kCorrespondenceStorageAnonId = '**anonymous**'; class CorrespondenceGameStorage { const CorrespondenceGameStorage(this._db, this.ref); final Database _db; - final CorrespondenceGameStorageRef ref; + final Ref ref; /// Fetches all ongoing correspondence games, sorted by time left. - Future> fetchOngoingGames( - UserId? userId, - ) async { + Future> fetchOngoingGames(UserId? userId) async { final list = await _db.query( kCorrespondenceStorageTable, where: 'userId = ? AND data LIKE ?', - whereArgs: [ - '${userId ?? kCorrespondenceStorageAnonId}', - '%"status":"started"%', - ], + whereArgs: ['${userId ?? kCorrespondenceStorageAnonId}', '%"status":"started"%'], ); return _decodeGames(list).sort((a, b) { @@ -76,36 +68,30 @@ class CorrespondenceGameStorage { } /// Fetches all correspondence games with a registered move. - Future> - fetchGamesWithRegisteredMove(UserId? userId) async { - final sqlVersion = await ref.read(sqliteVersionProvider.future); - if (sqlVersion != null && sqlVersion >= 338000) { + Future> fetchGamesWithRegisteredMove( + UserId? userId, + ) async { + try { final list = await _db.query( kCorrespondenceStorageTable, where: "json_extract(data, '\$.registeredMoveAtPgn') IS NOT NULL", ); return _decodeGames(list); - } - - final list = await _db.query( - kCorrespondenceStorageTable, - // where: "json_extract(data, '\$.registeredMoveAtPgn') IS NOT NULL", - where: 'userId = ? AND data LIKE ?', - whereArgs: [ - '${userId ?? kCorrespondenceStorageAnonId}', - '%status":"started"%', - ], - ); + } catch (e) { + final list = await _db.query( + kCorrespondenceStorageTable, + where: 'userId = ? AND data LIKE ?', + whereArgs: ['${userId ?? kCorrespondenceStorageAnonId}', '%status":"started"%'], + ); - return _decodeGames(list).where((e) { - final (_, game) = e; - return game.registeredMoveAtPgn != null; - }).toIList(); + return _decodeGames(list).where((e) { + final (_, game) = e; + return game.registeredMoveAtPgn != null; + }).toIList(); + } } - Future fetch({ - required GameId gameId, - }) async { + Future fetch({required GameId gameId}) async { final list = await _db.query( kCorrespondenceStorageTable, where: 'gameId = ?', @@ -127,17 +113,17 @@ class CorrespondenceGameStorage { } Future save(OfflineCorrespondenceGame game) async { - await _db.insert( - kCorrespondenceStorageTable, - { + try { + await _db.insert(kCorrespondenceStorageTable, { 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, 'gameId': game.id.toString(), 'lastModified': DateTime.now().toIso8601String(), 'data': jsonEncode(game.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - ref.invalidate(offlineOngoingCorrespondenceGamesProvider); + }, conflictAlgorithm: ConflictAlgorithm.replace); + ref.invalidate(offlineOngoingCorrespondenceGamesProvider); + } catch (e) { + debugPrint('[CorrespondenceGameStorage] failed to save game: $e'); + } } Future delete(GameId gameId) async { @@ -149,9 +135,7 @@ class CorrespondenceGameStorage { ref.invalidate(offlineOngoingCorrespondenceGamesProvider); } - IList<(DateTime, OfflineCorrespondenceGame)> _decodeGames( - List> list, - ) { + IList<(DateTime, OfflineCorrespondenceGame)> _decodeGames(List> list) { return list.map((e) { final lmString = e['lastModified'] as String?; final raw = e['data'] as String?; diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 21d1d6d25f..f8fa995758 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -4,10 +4,11 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; @@ -15,25 +16,45 @@ import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_g import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'correspondence_service.g.dart'; @Riverpod(keepAlive: true) -CorrespondenceService correspondenceService(CorrespondenceServiceRef ref) { - return CorrespondenceService( - Logger('CorrespondenceService'), - ref: ref, - ); +CorrespondenceService correspondenceService(Ref ref) { + return CorrespondenceService(Logger('CorrespondenceService'), ref: ref); } +/// Services that manages correspondence games. class CorrespondenceService { CorrespondenceService(this._log, {required this.ref}); - final CorrespondenceServiceRef ref; + final Ref ref; final Logger _log; + /// Handles a notification response that caused the app to open. + Future onNotificationResponse(GameFullId fullId) async { + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) return; + + final rootNavState = Navigator.of(context, rootNavigator: true); + if (rootNavState.canPop()) { + rootNavState.popUntil((route) => route.isFirst); + } + + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => GameScreen(initialGameId: fullId), + ); + } + /// Syncs offline correspondence games with the server. Future syncGames() async { if (_session == null) { @@ -44,8 +65,7 @@ class CorrespondenceService { await playRegisteredMoves(); - final storedOngoingGames = - await _storage.fetchOngoingGames(_session?.user.id); + final storedOngoingGames = await (await _storage).fetchOngoingGames(_session?.user.id); ref.withClient((client) async { try { @@ -60,7 +80,7 @@ class CorrespondenceService { _log.info( 'Deleting correspondence game ${sg.$2.id} because it is not present on the server anymore', ); - _storage.delete(sg.$2.id); + (await _storage).delete(sg.$2.id); } } @@ -87,17 +107,13 @@ class CorrespondenceService { Future playRegisteredMoves() async { _log.info('Playing registered correspondence moves...'); - final games = - await _storage.fetchGamesWithRegisteredMove(_session?.user.id).then( - (games) => games.map((e) => e.$2).toList(), - ); + final games = await (await _storage) + .fetchGamesWithRegisteredMove(_session?.user.id) + .then((games) => games.map((e) => e.$2).toList()); WebSocket.userAgent = ref.read(userAgentProvider); - final Map wsHeaders = _session != null - ? { - 'Authorization': 'Bearer ${signBearerToken(_session!.token)}', - } - : {}; + final Map wsHeaders = + _session != null ? {'Authorization': 'Bearer ${signBearerToken(_session!.token)}'} : {}; int movesPlayed = 0; @@ -109,14 +125,14 @@ class CorrespondenceService { WebSocket? socket; StreamSubscription? streamSubscription; try { - socket = await WebSocket.connect(uri.toString(), headers: wsHeaders) - .timeout(const Duration(seconds: 5)); + socket = await WebSocket.connect( + uri.toString(), + headers: wsHeaders, + ).timeout(const Duration(seconds: 5)); - final eventStream = socket.where((e) => e != '0').map( - (e) => SocketEvent.fromJson( - jsonDecode(e as String) as Map, - ), - ); + final eventStream = socket + .where((e) => e != '0') + .map((e) => SocketEvent.fromJson(jsonDecode(e as String) as Map)); final Completer gameCompleter = Completer(); final Completer movePlayedCompleter = Completer(); @@ -124,14 +140,11 @@ class CorrespondenceService { streamSubscription = eventStream.listen((event) { switch (event.topic) { case 'full': - final playableGame = GameFullEvent.fromJson( - event.data as Map, - ).game; + final playableGame = GameFullEvent.fromJson(event.data as Map).game; gameCompleter.complete(playableGame); case 'move': - final moveEvent = - MoveEvent.fromJson(event.data as Map); + final moveEvent = MoveEvent.fromJson(event.data as Map); // move acknowledged if (moveEvent.uci == gameToSync.registeredMoveAtPgn!.$2.uci) { movesPlayed++; @@ -149,31 +162,21 @@ class CorrespondenceService { socket.add( jsonEncode({ 't': 'move', - 'd': { - 'u': gameToSync.registeredMoveAtPgn!.$2.uci, - }, + 'd': {'u': gameToSync.registeredMoveAtPgn!.$2.uci}, }), ); await movePlayedCompleter.future.timeout(const Duration(seconds: 3)); - ref.read(correspondenceGameStorageProvider).save( - gameToSync.copyWith( - registeredMoveAtPgn: null, - ), - ); + (await ref.read( + correspondenceGameStorageProvider.future, + )).save(gameToSync.copyWith(registeredMoveAtPgn: null)); } else { - _log.info( - 'Cannot play game ${gameToSync.id} move because its state has changed', - ); + _log.info('Cannot play game ${gameToSync.id} move because its state has changed'); updateGame(gameToSync.fullId, playableGame); } } catch (e, s) { - _log.severe( - 'Failed to sync correspondence game ${gameToSync.id}', - e, - s, - ); + _log.severe('Failed to sync correspondence game ${gameToSync.id}', e, s); } finally { streamSubscription?.cancel(); socket?.close(); @@ -183,32 +186,49 @@ class CorrespondenceService { return movesPlayed; } + /// Handles a game update event from the server. + Future onServerUpdateEvent( + GameFullId fullId, + PlayableGame game, { + required bool fromBackground, + }) async { + if (!fromBackground) { + // opponent just played, invalidate ongoing games + if (game.sideToMove == game.youAre) { + ref.invalidate(ongoingGamesProvider); + } + } + + await updateGame(fullId, game); + } + + /// Updates a stored correspondence game. Future updateGame(GameFullId fullId, PlayableGame game) async { - return ref.read(correspondenceGameStorageProvider).save( - OfflineCorrespondenceGame( - id: game.id, - fullId: fullId, - meta: game.meta, - rated: game.meta.rated, - steps: game.steps, - initialFen: game.initialFen, - status: game.status, - variant: game.meta.variant, - speed: game.meta.speed, - perf: game.meta.perf, - white: game.white, - black: game.black, - youAre: game.youAre!, - daysPerTurn: game.meta.daysPerTurn, - clock: game.correspondenceClock, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - ), - ); + return (await ref.read(correspondenceGameStorageProvider.future)).save( + OfflineCorrespondenceGame( + id: game.id, + fullId: fullId, + meta: game.meta, + rated: game.meta.rated, + steps: game.steps, + initialFen: game.initialFen, + status: game.status, + variant: game.meta.variant, + speed: game.meta.speed, + perf: game.meta.perf, + white: game.white, + black: game.black, + youAre: game.youAre!, + daysPerTurn: game.meta.daysPerTurn, + clock: game.correspondenceClock, + winner: game.winner, + isThreefoldRepetition: game.isThreefoldRepetition, + ), + ); } AuthSessionState? get _session => ref.read(authSessionProvider); - CorrespondenceGameStorage get _storage => - ref.read(correspondenceGameStorageProvider); + Future get _storage => + ref.read(correspondenceGameStorageProvider.future); } diff --git a/lib/src/model/correspondence/offline_correspondence_game.dart b/lib/src/model/correspondence/offline_correspondence_game.dart index 1c8fc0cf5a..83a1d6a354 100644 --- a/lib/src/model/correspondence/offline_correspondence_game.dart +++ b/lib/src/model/correspondence/offline_correspondence_game.dart @@ -25,8 +25,7 @@ class OfflineCorrespondenceGame required GameId id, required GameFullId fullId, required GameMeta meta, - @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) - required IList steps, + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, CorrespondenceClockData? clock, String? initialFen, required bool rated, @@ -61,8 +60,7 @@ class OfflineCorrespondenceGame bool get isMyTurn => sideToMove == youAre; - Duration? myTimeLeft(DateTime lastModifiedTime) => - estimatedTimeLeft(youAre, lastModifiedTime); + Duration? myTimeLeft(DateTime lastModifiedTime) => estimatedTimeLeft(youAre, lastModifiedTime); Duration? estimatedTimeLeft(Side side, DateTime lastModifiedTime) { final timeLeft = side == Side.white ? clock?.white : clock?.black; @@ -72,7 +70,6 @@ class OfflineCorrespondenceGame return null; } - bool get isPlayerTurn => lastPosition.turn == youAre; bool get playable => status.value < GameStatus.aborted.value; bool get playing => status.value > GameStatus.started.value; bool get finished => status.value >= GameStatus.mate.value; diff --git a/lib/src/model/engine/engine.dart b/lib/src/model/engine/engine.dart index ef8163ba28..9833f06658 100644 --- a/lib/src/model/engine/engine.dart +++ b/lib/src/model/engine/engine.dart @@ -7,14 +7,7 @@ import 'package:stockfish/stockfish.dart'; import 'uci_protocol.dart'; import 'work.dart'; -enum EngineState { - initial, - loading, - idle, - computing, - error, - disposed, -} +enum EngineState { initial, loading, idle, computing, error, disposed } abstract class Engine { ValueListenable get state; @@ -44,36 +37,36 @@ class StockfishEngine implements Engine { @override Stream start(Work work) { - _log.info( - 'engine start at ply ${work.ply} and path ${work.path}', - ); + _log.info('engine start at ply ${work.ply} and path ${work.path}'); _protocol.compute(work); if (_stockfish == null) { - stockfishAsync().then((stockfish) { - _state.value = EngineState.loading; - _stockfish = stockfish; - _stdoutSubscription = stockfish.stdout.listen((line) { - _protocol.received(line); - }); - stockfish.state.addListener(_stockfishStateListener); - _protocol.isComputing.addListener(() { - if (_protocol.isComputing.value) { - _state.value = EngineState.computing; - } else { - _state.value = EngineState.idle; - } - }); - _protocol.connected((String cmd) { - stockfish.stdin = cmd; - }); - _protocol.engineName.then((name) { - _name = name; - }); - }).catchError((Object e, StackTrace s) { - _log.severe('error loading stockfish', e, s); - _state.value = EngineState.error; - }); + stockfishAsync() + .then((stockfish) { + _state.value = EngineState.loading; + _stockfish = stockfish; + _stdoutSubscription = stockfish.stdout.listen((line) { + _protocol.received(line); + }); + stockfish.state.addListener(_stockfishStateListener); + _protocol.isComputing.addListener(() { + if (_protocol.isComputing.value) { + _state.value = EngineState.computing; + } else { + _state.value = EngineState.idle; + } + }); + _protocol.connected((String cmd) { + stockfish.stdin = cmd; + }); + _protocol.engineName.then((name) { + _name = name; + }); + }) + .catchError((Object e, StackTrace s) { + _log.severe('error loading stockfish', e, s); + _state.value = EngineState.error; + }); } return _protocol.evalStream.where((e) => e.$1 == work); @@ -100,8 +93,7 @@ class StockfishEngine implements Engine { @override Future dispose() { _log.fine('disposing engine'); - if (_stockfish == null || - _stockfish?.state.value == StockfishState.disposed) { + if (_stockfish == null || _stockfish?.state.value == StockfishState.disposed) { return Future.value(); } _stdoutSubscription?.cancel(); diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 1721654562..63673738eb 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -5,10 +5,11 @@ import 'dart:math'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -19,20 +20,16 @@ import 'work.dart'; part 'evaluation_service.g.dart'; part 'evaluation_service.freezed.dart'; -const kMaxEngineDepth = 22; final maxEngineCores = max(Platform.numberOfProcessors - 1, 1); -final defaultEngineCores = - min((Platform.numberOfProcessors / 2).ceil(), maxEngineCores); +final defaultEngineCores = min((Platform.numberOfProcessors / 2).ceil(), maxEngineCores); -const engineSupportedVariants = { - Variant.standard, - Variant.chess960, -}; +const engineSupportedVariants = {Variant.standard, Variant.chess960, Variant.fromPosition}; +/// A service to evaluate chess positions using an engine. class EvaluationService { EvaluationService(this.ref, {required this.maxMemory}); - final EvaluationServiceRef ref; + final Ref ref; final int maxMemory; @@ -42,17 +39,18 @@ class EvaluationService { EvaluationOptions _options = EvaluationOptions( multiPv: 1, cores: defaultEngineCores, + searchTime: const Duration(seconds: 10), ); - static const _defaultState = - (engineName: 'Stockfish', state: EngineState.initial, eval: null); + static const _defaultState = (engineName: 'Stockfish', state: EngineState.initial, eval: null); - final ValueNotifier _state = - ValueNotifier(_defaultState); + final ValueNotifier _state = ValueNotifier(_defaultState); ValueListenable get state => _state; /// Initialize the engine with the given context and options. /// + /// If the engine is already initialized, it is disposed first. + /// /// An optional [engineFactory] can be provided, it defaults to Stockfish. /// /// If [options] is not provided, the default options are used. @@ -63,9 +61,11 @@ class EvaluationService { Engine Function() engineFactory = StockfishEngine.new, EvaluationOptions? options, }) async { + if (_context != null || _engine != null) { + await disposeEngine(); + } _context = context; if (options != null) _options = options; - await (_engine?.dispose() ?? Future.value()); _engine = engineFactory.call(); _engine!.state.addListener(() { debugPrint('Engine state: ${_engine?.state.value}'); @@ -77,22 +77,34 @@ class EvaluationService { _state.value = ( engineName: _engine!.name, state: _engine!.state.value, - eval: _state.value.eval + eval: _state.value.eval, ); } }); } + /// Ensure the engine is initialized with the given context and options. + Future ensureEngineInitialized( + EvaluationContext context, { + Engine Function() engineFactory = StockfishEngine.new, + EvaluationOptions? options, + }) async { + if (_engine == null || _context != context) { + await initEngine(context, engineFactory: engineFactory, options: options); + } + } + void setOptions(EvaluationOptions options) { stop(); _options = options; } - void disposeEngine() { - _engine?.dispose().then((_) { - _engine = null; - }); - _context = null; + Future disposeEngine() { + return _engine?.dispose().then((_) { + _engine = null; + _context = null; + }) ?? + Future.value(); } /// Start the engine evaluation with the given [path] and [steps]. @@ -127,17 +139,16 @@ class EvaluationService { variant: context.variant, threads: _options.cores, hashSize: maxMemory, - maxDepth: kMaxEngineDepth, + searchTime: _options.searchTime, multiPv: _options.multiPv, path: path, initialPosition: context.initialPosition, steps: IList(steps), ); - // cancel evaluation if we already have a cached eval at max depth - final cachedEval = - work.steps.isEmpty ? initialPositionEval : work.evalCache; - if (cachedEval != null && cachedEval.depth >= kMaxEngineDepth) { + // cancel evaluation if we already have a cached eval at max search time + final cachedEval = work.steps.isEmpty ? initialPositionEval : work.evalCache; + if (cachedEval != null && cachedEval.searchTime >= _options.searchTime) { _state.value = ( engineName: _state.value.engineName, state: _state.value.state, @@ -146,19 +157,14 @@ class EvaluationService { return null; } - final evalStream = engine.start(work).throttle( - const Duration(milliseconds: 200), - trailing: true, - ); + final evalStream = engine + .start(work) + .throttle(const Duration(milliseconds: 200), trailing: true); evalStream.forEach((t) { final (work, eval) = t; if (shouldEmit(work)) { - _state.value = ( - engineName: _state.value.engineName, - state: _state.value.state, - eval: eval, - ); + _state.value = (engineName: _state.value.engineName, state: _state.value.state, eval: eval); } }); @@ -171,11 +177,8 @@ class EvaluationService { } @Riverpod(keepAlive: true) -EvaluationService evaluationService(EvaluationServiceRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - final maxMemory = - ref.read(appInitializationProvider).requireValue.engineMaxMemoryInMb; +EvaluationService evaluationService(Ref ref) { + final maxMemory = ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb; final service = EvaluationService(ref, maxMemory: maxMemory); ref.onDispose(() { @@ -184,11 +187,7 @@ EvaluationService evaluationService(EvaluationServiceRef ref) { return service; } -typedef EngineEvaluationState = ({ - String engineName, - EngineState state, - ClientEval? eval -}); +typedef EngineEvaluationState = ({String engineName, EngineState state, ClientEval? eval}); /// A provider that holds the state of the engine and the current evaluation. @riverpod @@ -216,10 +215,8 @@ class EngineEvaluation extends _$EngineEvaluation { @freezed class EvaluationContext with _$EvaluationContext { - const factory EvaluationContext({ - required Variant variant, - required Position initialPosition, - }) = _EvaluationContext; + const factory EvaluationContext({required Variant variant, required Position initialPosition}) = + _EvaluationContext; } @freezed @@ -227,5 +224,6 @@ class EvaluationOptions with _$EvaluationOptions { const factory EvaluationOptions({ required int multiPv, required int cores, + required Duration searchTime, }) = _EvaluationOptions; } diff --git a/lib/src/model/engine/uci_protocol.dart b/lib/src/model/engine/uci_protocol.dart index 549867eeb4..5692f8f40f 100644 --- a/lib/src/model/engine/uci_protocol.dart +++ b/lib/src/model/engine/uci_protocol.dart @@ -13,12 +13,7 @@ const minDepth = 6; const maxPlies = 245; class UCIProtocol { - UCIProtocol() - : _options = { - 'Threads': '1', - 'Hash': '16', - 'MultiPV': '1', - }; + UCIProtocol() : _options = {'Threads': '1', 'Hash': '16', 'MultiPV': '1'}; final _log = Logger('UCIProtocol'); final Map _options; @@ -95,9 +90,7 @@ class UCIProtocol { _work = null; _swapWork(); return; - } else if (_work != null && - _stopRequested != true && - parts.first == 'info') { + } else if (_work != null && _stopRequested != true && parts.first == 'info') { int depth = 0; int nodes = 0; int multiPv = 1; @@ -120,8 +113,7 @@ class UCIProtocol { isMate = parts[++i] == 'mate'; povEv = int.parse(parts[++i]); if (i + 1 < parts.length && - (parts[i + 1] == 'lowerbound' || - parts[i + 1] == 'upperbound')) { + (parts[i + 1] == 'lowerbound' || parts[i + 1] == 'upperbound')) { evalType = parts[++i]; } case 'pv': @@ -142,16 +134,12 @@ class UCIProtocol { // However non-primary pvs may only have an upperbound. if (evalType != null && multiPv == 1) return; - final pvData = PvData( - moves: IList(moves), - cp: isMate ? null : ev, - mate: isMate ? ev : null, - ); + final pvData = PvData(moves: IList(moves), cp: isMate ? null : ev, mate: isMate ? ev : null); if (multiPv == 1) { _currentEval = ClientEval( position: _work!.position, - maxDepth: _work!.maxDepth, + searchTime: Duration(milliseconds: elapsedMs), depth: depth, nodes: nodes, cp: isMate ? null : ev, @@ -169,14 +157,7 @@ class UCIProtocol { if (multiPv == _expectedPvs && _currentEval != null) { _evalController.sink.add((_work!, _currentEval!)); - // Depth limits are nice in the user interface, but in clearly decided - // positions the usual depth limits are reached very quickly due to - // pruning. Therefore not using `go depth ${_work.maxDepth}` and - // manually ensuring Stockfish gets to spend a minimum amount of - // time/nodes on each position. - if (depth >= _work!.maxDepth && - elapsedMs > 8000 && - nodes > 4000 * math.exp(_work!.maxDepth * 0.3)) { + if (elapsedMs > _work!.searchTime.inMilliseconds) { _stop(); } } @@ -213,17 +194,11 @@ class UCIProtocol { _work!.initialPosition.fen, 'moves', ..._work!.steps.map( - (s) => _work!.variant == Variant.chess960 - ? s.sanMove.move.uci - : s.castleSafeUCI, + (s) => _work!.variant == Variant.chess960 ? s.sanMove.move.uci : s.castleSafeUCI, ), ].join(' '), ); - _sendAndLog( - _work!.maxDepth >= 99 - ? 'go depth $maxPlies' // 'go infinite' would not finish even if entire tree search completed - : 'go movetime 60000', - ); + _sendAndLog('go movetime ${_work!.searchTime.inMilliseconds}'); _isComputing.value = true; } else { _isComputing.value = false; diff --git a/lib/src/model/engine/work.dart b/lib/src/model/engine/work.dart index 553c9e1677..8cab1baa06 100644 --- a/lib/src/model/engine/work.dart +++ b/lib/src/model/engine/work.dart @@ -20,7 +20,7 @@ class Work with _$Work { required int threads, int? hashSize, required UciPath path, - required int maxDepth, + required Duration searchTime, required int multiPv, bool? threatMode, required Position initialPosition, @@ -40,18 +40,11 @@ class Work with _$Work { class Step with _$Step { const Step._(); - const factory Step({ - required Position position, - required SanMove sanMove, - ClientEval? eval, - }) = _Step; + const factory Step({required Position position, required SanMove sanMove, ClientEval? eval}) = + _Step; factory Step.fromNode(Branch node) { - return Step( - position: node.position, - sanMove: node.sanMove, - eval: node.eval, - ); + return Step(position: node.position, sanMove: node.sanMove, eval: node.eval); } /// Stockfish in chess960 mode always needs a "king takes rook" UCI notation. @@ -69,9 +62,4 @@ class Step with _$Step { } } -const _castleMoves = { - 'e1c1': 'e1a1', - 'e1g1': 'e1h1', - 'e8c8': 'e8a8', - 'e8g8': 'e8h8', -}; +const _castleMoves = {'e1c1': 'e1a1', 'e1g1': 'e1h1', 'e8c8': 'e8a8', 'e8g8': 'e8h8'}; diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 99ca23a623..d62aca0bae 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -19,10 +19,7 @@ import 'player.dart'; part 'archived_game.freezed.dart'; part 'archived_game.g.dart'; -typedef ClockData = ({ - Duration initial, - Duration increment, -}); +typedef ClockData = ({Duration initial, Duration increment}); /// A lichess game exported from the API. /// @@ -32,9 +29,7 @@ typedef ClockData = ({ /// See also [PlayableGame] for a game owned by the current user and that can be /// played unless finished. @Freezed(fromJson: true, toJson: true) -class ArchivedGame - with _$ArchivedGame, BaseGame, IndexableSteps - implements BaseGame { +class ArchivedGame with _$ArchivedGame, BaseGame, IndexableSteps implements BaseGame { const ArchivedGame._(); @Assert('steps.isNotEmpty') @@ -43,10 +38,11 @@ class ArchivedGame required GameMeta meta, // TODO refactor to not include this field required LightArchivedGame data, - @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) - required IList steps, + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, String? initialFen, required GameStatus status, + @JsonKey(defaultValue: GameSource.unknown, unknownEnumValue: GameSource.unknown) + required GameSource source, Side? winner, /// The point of view of the current player or null if spectating. @@ -67,20 +63,21 @@ class ArchivedGame } /// Create an archived game from a local storage JSON. - factory ArchivedGame.fromJson(Map json) => - _$ArchivedGameFromJson(json); + factory ArchivedGame.fromJson(Map json) => _$ArchivedGameFromJson(json); /// Player point of view. Null if spectating. - Player? get me => youAre == null - ? null - : youAre == Side.white + Player? get me => + youAre == null + ? null + : youAre == Side.white ? white : black; /// Opponent point of view. Null if spectating. - Player? get opponent => youAre == null - ? null - : youAre == Side.white + Player? get opponent => + youAre == null + ? null + : youAre == Side.white ? black : white; } @@ -113,6 +110,7 @@ class LightArchivedGame with _$LightArchivedGame { required Player white, required Player black, required Variant variant, + GameSource? source, LightOpening? opening, String? lastFen, @MoveConverter() Move? lastMove, @@ -128,10 +126,7 @@ class LightArchivedGame with _$LightArchivedGame { _$LightArchivedGameFromJson(json); String get clockDisplay { - return TimeIncrement( - clock?.initial.inSeconds ?? 0, - clock?.increment.inSeconds ?? 0, - ).display; + return TimeIncrement(clock?.initial.inSeconds ?? 0, clock?.increment.inSeconds ?? 0).display; } } @@ -144,10 +139,7 @@ IList? gameEvalsFromPick(RequiredPick pick) { bestMove: p0('best').asStringOrNull(), variation: p0('variation').asStringOrNull(), judgment: p0('judgment').letOrNull( - (j) => ( - name: j('name').asStringOrThrow(), - comment: j('comment').asStringOrThrow(), - ), + (j) => (name: j('name').asStringOrThrow(), comment: j('comment').asStringOrThrow()), ), ), ) @@ -156,9 +148,9 @@ IList? gameEvalsFromPick(RequiredPick pick) { ArchivedGame _archivedGameFromPick(RequiredPick pick) { final data = _lightArchivedGameFromPick(pick); - final clocks = pick('clocks').asListOrNull( - (p0) => Duration(milliseconds: p0.asIntOrThrow() * 10), - ); + final clocks = pick( + 'clocks', + ).asListOrNull((p0) => Duration(milliseconds: p0.asIntOrThrow() * 10)); final division = pick('division').letOrNull(_divisionFromPick); final initialFen = pick('initialFen').asStringOrNull(); @@ -171,17 +163,21 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { speed: data.speed, perf: data.perf, rated: data.rated, - clock: data.clock != null - ? ( - initial: data.clock!.initial, - increment: data.clock!.increment, - emergency: null, - moreTime: null - ) - : null, + clock: + data.clock != null + ? ( + initial: data.clock!.initial, + increment: data.clock!.increment, + emergency: null, + moreTime: null, + ) + : null, opening: data.opening, division: division, ), + source: pick( + 'source', + ).letOrThrow((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), data: data, status: data.status, winner: data.winner, @@ -192,10 +188,10 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { steps: pick('moves').letOrThrow((it) { final moves = it.asStringOrThrow().split(' '); // assume lichess always send initialFen with fromPosition and chess960 - Position position = (data.variant == Variant.fromPosition || - data.variant == Variant.chess960) - ? Chess.fromSetup(Setup.parseFen(initialFen!)) - : data.variant.initialPosition; + Position position = + (data.variant == Variant.fromPosition || data.variant == Variant.chess960) + ? Chess.fromSetup(Setup.parseFen(initialFen!)) + : data.variant.initialPosition; int index = 0; final List steps = [GameStep(position: position)]; Duration? clock = data.clock?.initial; @@ -227,6 +223,9 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { return LightArchivedGame( id: pick('id').asGameIdOrThrow(), fullId: pick('fullId').asGameFullIdOrNull(), + source: pick( + 'source', + ).letOrNull((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), rated: pick('rated').asBoolOrThrow(), speed: pick('speed').asSpeedOrThrow(), perf: pick('perf').asPerfOrThrow(), @@ -245,10 +244,7 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { } LightOpening _openingFromPick(RequiredPick pick) { - return LightOpening( - eco: pick('eco').asStringOrThrow(), - name: pick('name').asStringOrThrow(), - ); + return LightOpening(eco: pick('eco').asStringOrThrow(), name: pick('name').asStringOrThrow()); } ClockData _clockDataFromPick(RequiredPick pick) { @@ -259,15 +255,33 @@ ClockData _clockDataFromPick(RequiredPick pick) { } Player _playerFromUserGamePick(RequiredPick pick) { + final originalName = pick('name').asStringOrNull(); return Player( user: pick('user').asLightUserOrNull(), - rating: pick('rating').asIntOrNull(), + name: _removeRatingFromName(originalName), + rating: pick('rating').asIntOrNull() ?? _extractRatingFromName(originalName), ratingDiff: pick('ratingDiff').asIntOrNull(), aiLevel: pick('aiLevel').asIntOrNull(), analysis: pick('analysis').letOrNull(_playerAnalysisFromPick), ); } +int? _extractRatingFromName(String? name) { + if (name == null) return null; + final regex = RegExp(r'\((\d+)\)'); + final match = regex.firstMatch(name); + if (match != null) { + return int.tryParse(match.group(1)!); + } + return null; +} + +String? _removeRatingFromName(String? name) { + if (name == null) return null; + final regex = RegExp(r'\s*\(\d+\)\s*'); + return name.replaceAll(regex, ''); +} + PlayerAnalysis _playerAnalysisFromPick(RequiredPick pick) { return PlayerAnalysis( inaccuracies: pick('inaccuracy').asIntOrThrow(), diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index d9e711e7d5..108a73fd36 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -24,8 +25,7 @@ class ChatController extends _$ChatController { @override Future build(GameFullId id) async { - _socketClient = - ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); + _socketClient = ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); _subscription?.cancel(); _subscription = _socketClient.stream.listen(_handleSocketEvent); @@ -37,9 +37,7 @@ class ChatController extends _$ChatController { final messages = await _socketClient.stream .firstWhere((event) => event.topic == 'full') .then( - (event) => pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(), + (event) => pick(event.data, 'chat', 'lines').asListOrNull(_messageFromPick)?.toIList(), ); final readMessagesCount = await _getReadMessagesCount(); @@ -52,10 +50,7 @@ class ChatController extends _$ChatController { /// Sends a message to the chat. void sendMessage(String message) { - _socketClient.send( - 'talk', - message, - ); + _socketClient.send('talk', message); } /// Resets the unread messages count to 0 and saves the number of read messages. @@ -63,13 +58,11 @@ class ChatController extends _$ChatController { if (state.hasValue) { await _setReadMessagesCount(state.requireValue.messages.length); } - state = state.whenData( - (s) => s.copyWith(unreadMessages: 0), - ); + state = state.whenData((s) => s.copyWith(unreadMessages: 0)); } Future _getReadMessagesCount() async { - final db = ref.read(databaseProvider); + final db = await ref.read(databaseProvider.future); final result = await db.query( _tableName, columns: ['nbRead'], @@ -80,35 +73,25 @@ class ChatController extends _$ChatController { } Future _setReadMessagesCount(int count) async { - final db = ref.read(databaseProvider); - await db.insert( - _tableName, - { - 'id': _storeKey(id), - 'lastModified': DateTime.now().toIso8601String(), - 'nbRead': count, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + final db = await ref.read(databaseProvider.future); + await db.insert(_tableName, { + 'id': _storeKey(id), + 'lastModified': DateTime.now().toIso8601String(), + 'nbRead': count, + }, conflictAlgorithm: ConflictAlgorithm.replace); } Future _setMessages(IList messages) async { final readMessagesCount = await _getReadMessagesCount(); state = state.whenData( - (s) => s.copyWith( - messages: messages, - unreadMessages: messages.length - readMessagesCount, - ), + (s) => s.copyWith(messages: messages, unreadMessages: messages.length - readMessagesCount), ); } void _addMessage(Message message) { state = state.whenData( - (s) => s.copyWith( - messages: s.messages.add(message), - unreadMessages: s.unreadMessages + 1, - ), + (s) => s.copyWith(messages: s.messages.add(message), unreadMessages: s.unreadMessages + 1), ); } @@ -116,22 +99,14 @@ class ChatController extends _$ChatController { if (!state.hasValue) return; if (event.topic == 'full') { - final messages = pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(); + final messages = pick(event.data, 'chat', 'lines').asListOrNull(_messageFromPick)?.toIList(); if (messages != null) { _setMessages(messages); } } else if (event.topic == 'message') { final data = event.data as Map; - final message = data['t'] as String; - final username = data['u'] as String?; - _addMessage( - ( - message: message, - username: username, - ), - ); + final message = _messageFromPick(RequiredPick(data)); + _addMessage(message); } } } @@ -140,17 +115,56 @@ class ChatController extends _$ChatController { class ChatState with _$ChatState { const ChatState._(); - const factory ChatState({ - required IList messages, - required int unreadMessages, - }) = _ChatState; + const factory ChatState({required IList messages, required int unreadMessages}) = + _ChatState; } -typedef Message = ({String? username, String message}); +typedef Message = ({String? username, String message, bool troll, bool deleted}); Message _messageFromPick(RequiredPick pick) { return ( message: pick('t').asStringOrThrow(), username: pick('u').asStringOrNull(), + troll: pick('r').asBoolOrNull() ?? false, + deleted: pick('d').asBoolOrNull() ?? false, ); } + +bool isSpam(Message message) { + return spamRegex.hasMatch(message.message) || followMeRegex.hasMatch(message.message); +} + +final RegExp spamRegex = RegExp( + [ + 'xcamweb.com', + '(^|[^i])chess-bot', + 'chess-cheat', + 'coolteenbitch', + 'letcafa.webcam', + 'tinyurl.com/', + 'wooga.info/', + 'bit.ly/', + 'wbt.link/', + 'eb.by/', + '001.rs/', + 'shr.name/', + 'u.to/', + '.3-a.net', + '.ssl443.org', + '.ns02.us', + '.myftp.info', + '.flinkup.com', + '.serveusers.com', + 'badoogirls.com', + 'hide.su', + 'wyon.de', + 'sexdatingcz.club', + 'qps.ru', + 'tiny.cc/', + 'trasderk.blogspot.com', + 't.ly/', + 'shorturl.at/', + ].map((url) => url.replaceAll('.', '\\.').replaceAll('/', '\\/')).join('|'), +); + +final followMeRegex = RegExp('follow me|join my team', caseSensitive: false); diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 3f0d78458a..08866bf79d 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -8,11 +8,11 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'game_status.dart'; import 'material_diff.dart'; @@ -78,16 +78,11 @@ abstract mixin class BaseGame { for (var i = 1; i < steps.length; i++) { final step = steps[i]; final eval = evals?.elementAtOrNull(i - 1); - final pgnEval = eval?.cp != null - ? PgnEvaluation.pawns( - pawns: cpToPawns(eval!.cp!), - depth: eval.depth, - ) - : eval?.mate != null - ? PgnEvaluation.mate( - mate: eval!.mate, - depth: eval.depth, - ) + final pgnEval = + eval?.cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(eval!.cp!), depth: eval.depth) + : eval?.mate != null + ? PgnEvaluation.mate(mate: eval!.mate, depth: eval.depth) : null; final clock = clocks?.elementAtOrNull(i - 1); Duration? emt; @@ -106,17 +101,11 @@ abstract mixin class BaseGame { } } - final comment = eval != null || clock != null - ? PgnComment( - text: eval?.judgment?.comment, - clock: clock, - emt: emt, - eval: pgnEval, - ) - : null; - final nag = eval?.judgment != null - ? _judgmentNameToNag(eval!.judgment!.name) - : null; + final comment = + eval != null || clock != null + ? PgnComment(text: eval?.judgment?.comment, clock: clock, emt: emt, eval: pgnEval) + : null; + final nag = eval?.judgment != null ? _judgmentNameToNag(eval!.judgment!.name) : null; final nextNode = Branch( sanMove: step.sanMove!, position: step.position, @@ -134,10 +123,7 @@ abstract mixin class BaseGame { for (final san in moves) { final move = position.parseSan(san); position = position.playUnchecked(move!); - final child = Branch( - sanMove: SanMove(san, move), - position: position, - ); + final child = Branch(sanMove: SanMove(san, move), position: position); variationNode.addChild(child); variationNode = child; } @@ -149,11 +135,11 @@ abstract mixin class BaseGame { } int? _judgmentNameToNag(String name) => switch (name) { - 'Inaccuracy' => 6, - 'Mistake' => 2, - 'Blunder' => 4, - String() => null, - }; + 'Inaccuracy' => 6, + 'Mistake' => 2, + 'Blunder' => 4, + String() => null, + }; String makePgn() { final node = makeTree(); @@ -162,33 +148,31 @@ abstract mixin class BaseGame { 'Event': '${meta.rated ? 'Rated' : ''} ${meta.perf.title} game', 'Site': lichessUri('/$id').toString(), 'Date': _dateFormat.format(meta.createdAt), - 'White': white.user?.name ?? - (white.aiLevel != null - ? 'Stockfish level ${white.aiLevel}' - : 'Anonymous'), - 'Black': black.user?.name ?? - (black.aiLevel != null - ? 'Stockfish level ${black.aiLevel}' - : 'Anonymous'), - 'Result': status.value >= GameStatus.mate.value - ? winner == null - ? '½-½' - : winner == Side.white + 'White': + white.user?.name ?? + white.name ?? + (white.aiLevel != null ? 'Stockfish level ${white.aiLevel}' : 'Anonymous'), + 'Black': + black.user?.name ?? + black.name ?? + (black.aiLevel != null ? 'Stockfish level ${black.aiLevel}' : 'Anonymous'), + 'Result': + status.value >= GameStatus.mate.value + ? winner == null + ? '½-½' + : winner == Side.white ? '1-0' : '0-1' - : '*', + : '*', if (white.rating != null) 'WhiteElo': white.rating!.toString(), if (black.rating != null) 'BlackElo': black.rating!.toString(), if (white.ratingDiff != null) - 'WhiteRatingDiff': - '${white.ratingDiff! > 0 ? '+' : ''}${white.ratingDiff!}', + 'WhiteRatingDiff': '${white.ratingDiff! > 0 ? '+' : ''}${white.ratingDiff!}', if (black.ratingDiff != null) - 'BlackRatingDiff': - '${black.ratingDiff! > 0 ? '+' : ''}${black.ratingDiff!}', + 'BlackRatingDiff': '${black.ratingDiff! > 0 ? '+' : ''}${black.ratingDiff!}', 'Variant': meta.variant.label, if (meta.clock != null) - 'TimeControl': - '${meta.clock!.initial.inSeconds}+${meta.clock!.increment.inSeconds}', + 'TimeControl': '${meta.clock!.initial.inSeconds}+${meta.clock!.increment.inSeconds}', if (initialFen != null) 'FEN': initialFen!, if (meta.opening != null) 'ECO': meta.opening!.eco, if (meta.opening != null) 'Opening': meta.opening!.name, @@ -200,13 +184,9 @@ abstract mixin class BaseGame { /// A mixin that provides methods to access game data at a specific step. mixin IndexableSteps on BaseGame { - String get sanMoves => steps - .where((e) => e.sanMove != null) - .map((e) => e.sanMove!.san) - .join(' '); + String get sanMoves => steps.where((e) => e.sanMove != null).map((e) => e.sanMove!.san).join(' '); - MaterialDiffSide? materialDiffAt(int cursor, Side side) => - steps[cursor].diff?.bySide(side); + MaterialDiffSide? materialDiffAt(int cursor, Side side) => steps[cursor].diff?.bySide(side); GameStep stepAt(int cursor) => steps[cursor]; @@ -218,11 +198,9 @@ mixin IndexableSteps on BaseGame { Position positionAt(int cursor) => steps[cursor].position; - Duration? archivedWhiteClockAt(int cursor) => - steps[cursor].archivedWhiteClock; + Duration? archivedWhiteClockAt(int cursor) => steps[cursor].archivedWhiteClock; - Duration? archivedBlackClockAt(int cursor) => - steps[cursor].archivedBlackClock; + Duration? archivedBlackClockAt(int cursor) => steps[cursor].archivedBlackClock; Move? get lastMove { return steps.last.sanMove?.move; @@ -236,8 +214,7 @@ mixin IndexableSteps on BaseGame { int get lastPly => steps.last.position.ply; - MaterialDiffSide? lastMaterialDiffAt(Side side) => - steps.last.diff?.bySide(side); + MaterialDiffSide? lastMaterialDiffAt(Side side) => steps.last.diff?.bySide(side); } enum GameSource { @@ -268,17 +245,17 @@ enum GameRule { } @freezed -class GamePrefs with _$GamePrefs { - const GamePrefs._(); +class ServerGamePrefs with _$ServerGamePrefs { + const ServerGamePrefs._(); - const factory GamePrefs({ + const factory ServerGamePrefs({ required bool showRatings, required bool enablePremove, required AutoQueen autoQueen, required bool confirmResign, required bool submitMove, required Zen zenMode, - }) = _GamePrefs; + }) = _ServerGamePrefs; } @Freezed(fromJson: true, toJson: true) @@ -301,7 +278,8 @@ class GameMeta with _$GameMeta { /// Time added to the clock by the "add more time" button. Duration? moreTime, - })? clock, + })? + clock, int? daysPerTurn, int? startedAtTurn, ISet? rules, @@ -313,25 +291,13 @@ class GameMeta with _$GameMeta { Division? division, }) = _GameMeta; - factory GameMeta.fromJson(Map json) => - _$GameMetaFromJson(json); -} - -@freezed -class PlayableClockData with _$PlayableClockData { - const factory PlayableClockData({ - required bool running, - required Duration white, - required Duration black, - }) = _PlayableClockData; + factory GameMeta.fromJson(Map json) => _$GameMetaFromJson(json); } @Freezed(fromJson: true, toJson: true) class CorrespondenceClockData with _$CorrespondenceClockData { - const factory CorrespondenceClockData({ - required Duration white, - required Duration black, - }) = _CorrespondenceClockData; + const factory CorrespondenceClockData({required Duration white, required Duration black}) = + _CorrespondenceClockData; factory CorrespondenceClockData.fromJson(Map json) => _$CorrespondenceClockDataFromJson(json); @@ -384,7 +350,7 @@ IList stepsFromJson(String json) { if (uci == null || san == null) { break; } - final move = Move.fromUci(uci)!; + final move = Move.parse(uci)!; position = position.playUnchecked(move); steps.add( GameStep( diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index bb1f543e5b..dc29a0444c 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1,19 +1,19 @@ import 'dart:async'; import 'package:async/async.dart'; -import 'package:chessground/chessground.dart' as cg; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -29,6 +29,8 @@ import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:logging/logging.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -41,6 +43,7 @@ part 'game_controller.g.dart'; class GameController extends _$GameController { final _logger = Logger('GameController'); + AppLifecycleListener? _appLifecycleListener; StreamSubscription? _socketSubscription; /// Periodic timer when the opponent has left the game, to display the countdown @@ -60,20 +63,16 @@ class GameController extends _$GameController { /// Last socket version received int? _socketEventVersion; - /// Last move time - DateTime? _lastMoveTime; + static Uri gameSocketUri(GameFullId gameFullId) => Uri(path: '/play/$gameFullId/v6'); - late SocketClient _socketClient; - - static Uri gameSocketUri(GameFullId gameFullId) => - Uri(path: '/play/$gameFullId/v6'); + ChessClock? _clock; + late final SocketClient _socketClient; @override Future build(GameFullId gameFullId) { final socketPool = ref.watch(socketPoolProvider); - _socketClient = - socketPool.open(gameSocketUri(gameFullId), forceReconnect: true); + _socketClient = socketPool.open(gameSocketUri(gameFullId), forceReconnect: true); _socketEventVersion = null; _socketSubscription?.cancel(); _socketSubscription = _socketClient.stream.listen(_handleSocketEvent); @@ -82,40 +81,83 @@ class GameController extends _$GameController { _socketSubscription?.cancel(); _opponentLeftCountdownTimer?.cancel(); _transientMoveTimer?.cancel(); + _appLifecycleListener?.dispose(); + _clock?.dispose(); }); - return _socketClient.stream.firstWhere((e) => e.topic == 'full').then( - (event) async { - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + return _socketClient.stream.firstWhere((e) => e.topic == 'full').then((event) async { + final fullEvent = GameFullEvent.fromJson(event.data as Map); - PlayableGame game = fullEvent.game; + PlayableGame game = fullEvent.game; - if (fullEvent.game.finished) { - final result = await _getPostGameData(); - game = result.fold( - (data) => _mergePostGameData(game, data, rewriteSteps: true), - (e, s) { - _logger.warning('Could not get post game data: $e', e, s); - return game; - }); + if (fullEvent.game.finished) { + if (fullEvent.game.meta.speed == Speed.correspondence) { + ref.invalidate(ongoingGamesProvider); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, fullEvent.game); } - _socketEventVersion = fullEvent.socketEventVersion; + final result = await _getPostGameData(); + game = result.fold((data) => _mergePostGameData(game, data, rewriteSteps: true), (e, s) { + _logger.warning('Could not get post game data: $e', e, s); + return game; + }); + await _storeGame(game); + } + + _socketEventVersion = fullEvent.socketEventVersion; + + // Play "dong" sound when this is a new game and we're playing it (not spectating) + final isMyGame = game.youAre != null; + final noMovePlayed = game.steps.length == 1; + if (isMyGame && noMovePlayed && game.status == GameStatus.started) { + ref.read(soundServiceProvider).play(Sound.dong); + } - return GameState( - gameFullId: gameFullId, - game: game, - stepCursor: game.steps.length - 1, - stopClockWaitingForServerAck: false, + if (game.playable) { + _appLifecycleListener = AppLifecycleListener( + onResume: () { + // socket client should never be disposed here, but in case it is + // we can safely skip the resync + if (!_socketClient.isDisposed && _socketClient.isConnected) { + _resyncGameData(); + } + }, ); - }, - ); + + if (game.clock != null) { + _clock = ChessClock( + whiteTime: game.clock!.white, + blackTime: game.clock!.black, + emergencyThreshold: game.meta.clock?.emergency, + onEmergency: onClockEmergency, + onFlag: onFlag, + ); + if (game.clock!.running) { + final pos = game.lastPosition; + if (pos.fullmoves > 1) { + _clock!.startSide(pos.turn); + } + } + } + } + + return GameState( + gameFullId: gameFullId, + game: game, + stepCursor: game.steps.length - 1, + liveClock: _liveClock, + ); + }); } - void onUserMove(Move move, {bool? isDrop, bool? isPremove}) { + void userMove(NormalMove move, {bool? isDrop, bool? isPremove}) { final curState = state.requireValue; + if (isPromotionPawnMove(curState.game.lastPosition, move)) { + state = AsyncValue.data(curState.copyWith(promotionMove: move)); + return; + } + final (newPos, newSan) = curState.game.lastPosition.makeSan(move); final sanMove = SanMove(newSan, move); final newStep = GameStep( @@ -128,12 +170,11 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.add(newStep), - ), + game: curState.game.copyWith(steps: curState.game.steps.add(newStep)), stepCursor: curState.stepCursor + 1, - stopClockWaitingForServerAck: !shouldConfirmMove, moveToConfirm: shouldConfirmMove ? move : null, + promotionMove: null, + premove: null, ), ); @@ -143,15 +184,28 @@ class GameController extends _$GameController { _sendMoveToSocket( move, isPremove: isPremove ?? false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet - withLag: - curState.game.clock != null && curState.activeClockSide == null, + withLag: curState.game.clock != null && curState.activeClockSide == null, ); } } + void onPromotionSelection(Role? role) { + final curState = state.requireValue; + if (role == null) { + state = AsyncValue.data(curState.copyWith(promotionMove: null)); + return; + } + if (curState.promotionMove == null) { + assert(false, 'promotionMove must not be null on promotion select'); + return; + } + + final move = curState.promotionMove!.withPromotion(role); + userMove(move, isDrop: true); + } + /// Called if the player cancels the move when confirm move preference is enabled void cancelMove() { final curState = state.requireValue; @@ -161,9 +215,7 @@ class GameController extends _$GameController { } state = AsyncValue.data( curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.removeLast(), - ), + game: curState.game.copyWith(steps: curState.game.steps.removeLast()), stepCursor: curState.stepCursor - 1, moveToConfirm: null, ), @@ -179,16 +231,10 @@ class GameController extends _$GameController { return; } - state = AsyncValue.data( - curState.copyWith( - stopClockWaitingForServerAck: true, - moveToConfirm: null, - ), - ); + state = AsyncValue.data(curState.copyWith(moveToConfirm: null)); _sendMoveToSocket( moveToConfirm, isPremove: false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet withLag: curState.game.clock != null && curState.activeClockSide == null, @@ -196,23 +242,14 @@ class GameController extends _$GameController { } /// Set or unset a premove. - void setPremove(cg.Move? move) { + void setPremove(NormalMove? move) { final curState = state.requireValue; - state = AsyncValue.data( - curState.copyWith( - premove: move, - ), - ); + state = AsyncValue.data(curState.copyWith(premove: move)); } void cursorAt(int cursor) { if (state.hasValue) { - state = AsyncValue.data( - state.requireValue.copyWith( - stepCursor: cursor, - premove: null, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(stepCursor: cursor, premove: null)); final san = state.requireValue.game.stepAt(cursor).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -254,28 +291,21 @@ class GameController extends _$GameController { void toggleMoveConfirmation() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - moveConfirmSettingOverride: - !(curState.moveConfirmSettingOverride ?? true), - ), + curState.copyWith(moveConfirmSettingOverride: !(curState.moveConfirmSettingOverride ?? true)), ); } void toggleZenMode() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - zenModeGameSetting: !(curState.zenModeGameSetting ?? false), - ), + curState.copyWith(zenModeGameSetting: !(curState.zenModeGameSetting ?? false)), ); } void toggleAutoQueen() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - autoQueenSettingOverride: !(curState.autoQueenSettingOverride ?? true), - ), + curState.copyWith(autoQueenSettingOverride: !(curState.autoQueenSettingOverride ?? true)), ); } @@ -286,6 +316,15 @@ class GameController extends _$GameController { } } + /// Play a sound when the clock is about to run out + Future onClockEmergency(Side activeSide) async { + if (activeSide != state.valueOrNull?.game.youAre) return; + final shouldPlay = await ref.read(clockSoundProvider.future); + if (shouldPlay) { + ref.read(soundServiceProvider).play(Sound.lowTime); + } + } + void onFlag() { _onFlagThrottler(() { if (state.hasValue) { @@ -347,43 +386,41 @@ class GameController extends _$GameController { _socketClient.send('rematch-no', null); } - Future requestServerAnalysis() { - return state.mapOrNull( - data: (d) { - if (!d.value.game.finished) { - return Future.error( - 'Cannot request server analysis on a non finished game', - ); - } - final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis(gameFullId); - }, - ) ?? - Future.value(); - } + /// Gets the live game clock if available. + LiveGameClock? get _liveClock => + _clock != null ? (white: _clock!.whiteTime, black: _clock!.blackTime) : null; - void _sendMoveToSocket( - Move move, { - required bool isPremove, - required bool hasClock, - required bool withLag, + /// Update the internal clock on clock server event + void _updateClock({ + required Duration white, + required Duration black, + required Side? activeSide, + Duration? lag, }) { - final moveTime = hasClock - ? isPremove == true - ? Duration.zero - : _lastMoveTime != null - ? DateTime.now().difference(_lastMoveTime!) - : null - : null; + _clock?.setTimes(whiteTime: white, blackTime: black); + if (activeSide != null) { + _clock?.startSide(activeSide, delay: lag); + } else { + _clock?.stop(); + } + } + + void _sendMoveToSocket(Move move, {required bool isPremove, required bool withLag}) { + final thinkTime = _clock?.stop(); + final moveTime = + _clock != null + ? isPremove == true + ? Duration.zero + : thinkTime + : null; _socketClient.send( 'move', { 'u': move.uci, - if (moveTime != null) - 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), + if (moveTime != null) 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), }, ackable: true, - withLag: hasClock && (moveTime == null || withLag), + withLag: _clock != null && (moveTime == null || withLag), ); _transientMoveTimer = Timer(const Duration(seconds: 10), _resyncGameData); @@ -391,10 +428,9 @@ class GameController extends _$GameController { /// Move feedback while playing void _playMoveFeedback(SanMove sanMove, {bool skipAnimationDelay = false}) { - final animationDuration = - ref.read(boardPreferencesProvider).pieceAnimationDuration; + final animationDuration = ref.read(boardPreferencesProvider).pieceAnimationDuration; - final delay = animationDuration - const Duration(milliseconds: 10); + final delay = animationDuration ~/ 2; if (skipAnimationDelay || delay <= Duration.zero) { _moveFeedback(sanMove); @@ -444,9 +480,7 @@ class GameController extends _$GameController { return; } if (event.version! > currentEventVersion + 1) { - _logger.warning( - 'Event gap detected from $currentEventVersion to ${event.version}', - ); + _logger.warning('Event gap detected from $currentEventVersion to ${event.version}'); _resyncGameData(); } _socketEventVersion = event.version; @@ -475,10 +509,7 @@ class GameController extends _$GameController { _resyncGameData(); return; } - final reloadEvent = SocketEvent( - topic: data['t'] as String, - data: data['d'], - ); + final reloadEvent = SocketEvent(topic: data['t'] as String, data: data['d']); _handleSocketTopic(reloadEvent); } else { _resyncGameData(); @@ -486,28 +517,33 @@ class GameController extends _$GameController { // Full game data, received after a (re)connection to game socket case 'full': - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + final fullEvent = GameFullEvent.fromJson(event.data as Map); - if (_socketEventVersion != null && - fullEvent.socketEventVersion < _socketEventVersion!) { + if (_socketEventVersion != null && fullEvent.socketEventVersion < _socketEventVersion!) { return; } _socketEventVersion = fullEvent.socketEventVersion; - _lastMoveTime = null; state = AsyncValue.data( GameState( gameFullId: gameFullId, game: fullEvent.game, stepCursor: fullEvent.game.steps.length - 1, - stopClockWaitingForServerAck: false, + liveClock: _liveClock, // cancel the premove to avoid playing wrong premove when the full // game data is reloaded premove: null, ), ); + if (fullEvent.game.clock != null) { + _updateClock( + white: fullEvent.game.clock!.white, + black: fullEvent.game.clock!.black, + activeSide: state.requireValue.activeClockSide, + ); + } + // Move event, received after sending a move or receiving a move from the // opponent case 'move': @@ -530,7 +566,7 @@ class GameController extends _$GameController { // add opponent move if (data.ply == curState.game.lastPly + 1) { final lastPos = curState.game.lastPosition; - final move = Move.fromUci(data.uci)!; + final move = Move.parse(data.uci)!; final sanMove = SanMove(data.san, move); final newPos = lastPos.playUnchecked(move); final newStep = GameStep( @@ -540,25 +576,35 @@ class GameController extends _$GameController { ); newState = newState.copyWith( - game: newState.game.copyWith( - steps: newState.game.steps.add(newStep), - ), + game: newState.game.copyWith(steps: newState.game.steps.add(newStep)), ); if (!curState.isReplaying) { - newState = newState.copyWith( - stepCursor: newState.stepCursor + 1, - ); + newState = newState.copyWith(stepCursor: newState.stepCursor + 1); _playMoveFeedback(sanMove); } } - // TODO handle delay if (data.clock != null) { - _lastMoveTime = DateTime.now(); + final lag = + newState.game.playable && newState.game.isMyTurn + // my own clock doesn't need to be compensated for + ? Duration.zero + // server will send the lag only if it's more than 10ms + // default lag of 10ms is also used by web client + : data.clock?.lag ?? const Duration(milliseconds: 10); + + _updateClock( + white: data.clock!.white, + black: data.clock!.black, + lag: lag, + activeSide: newState.activeClockSide, + ); if (newState.game.clock != null) { + // we don't rely on these values to display the clock, but let's keep + // the game object in sync newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, @@ -569,17 +615,11 @@ class GameController extends _$GameController { black: data.clock!.black, ); } - - newState = newState.copyWith( - stopClockWaitingForServerAck: false, - ); } if (newState.game.expiration != null) { if (newState.game.steps.length > 2) { - newState = newState.copyWith.game( - expiration: null, - ); + newState = newState.copyWith.game(expiration: null); } else { newState = newState.copyWith.game( expiration: ( @@ -593,29 +633,34 @@ class GameController extends _$GameController { if (curState.game.meta.speed == Speed.correspondence) { ref.invalidate(ongoingGamesProvider); - ref - .read(correspondenceServiceProvider) - .updateGame(gameFullId, newState.game); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, newState.game); + } + + if (!curState.isReplaying && + playedSide == curState.game.youAre?.opposite && + curState.premove != null) { + scheduleMicrotask(() { + final postMovePremove = state.valueOrNull?.premove; + final postMovePosition = state.valueOrNull?.game.lastPosition; + if (postMovePremove != null && postMovePosition?.isLegal(postMovePremove) == true) { + userMove(postMovePremove, isPremove: true); + } + }); } state = AsyncValue.data(newState); // End game event case 'endData': - final endData = - GameEndEvent.fromJson(event.data as Map); + final endData = GameEndEvent.fromJson(event.data as Map); final curState = state.requireValue; GameState newState = curState.copyWith( game: curState.game.copyWith( status: endData.status, winner: endData.winner, boosted: endData.boosted, - white: curState.game.white.copyWith( - ratingDiff: endData.ratingDiff?.white, - ), - black: curState.game.black.copyWith( - ratingDiff: endData.ratingDiff?.black, - ), + white: curState.game.white.copyWith(ratingDiff: endData.ratingDiff?.white), + black: curState.game.black.copyWith(ratingDiff: endData.ratingDiff?.black), ), premove: null, ); @@ -625,6 +670,11 @@ class GameController extends _$GameController { white: endData.clock!.white, black: endData.clock!.black, ); + _updateClock( + white: endData.clock!.white, + black: endData.clock!.black, + activeSide: newState.activeClockSide, + ); } if (curState.game.lastPosition.fullmoves > 1) { @@ -635,44 +685,42 @@ class GameController extends _$GameController { if (curState.game.meta.speed == Speed.correspondence) { ref.invalidate(ongoingGamesProvider); - ref - .read(correspondenceServiceProvider) - .updateGame(gameFullId, newState.game); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, newState.game); } state = AsyncValue.data(newState); if (!newState.game.aborted) { _getPostGameData().then((result) { - result.fold((data) { - final game = _mergePostGameData(state.requireValue.game, data); - state = AsyncValue.data( - state.requireValue.copyWith(game: game), - ); - - ref - .read(gameStorageProvider) - .save(game.toArchivedGame(finishedAt: DateTime.now())); - }, (e, s) { - _logger.warning('Could not get post game data', e, s); - }); + result.fold( + (data) { + final game = _mergePostGameData(state.requireValue.game, data); + state = AsyncValue.data(state.requireValue.copyWith(game: game)); + _storeGame(game); + }, + (e, s) { + _logger.warning('Could not get post game data', e, s); + }, + ); }); } case 'clockInc': final data = event.data as Map; final side = pick(data['color']).asSideOrNull(); - final newClock = pick(data['total']) - .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); + final newClock = pick( + data['total'], + ).letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); final curState = state.requireValue; + if (side != null && newClock != null) { - final newState = side == Side.white - ? curState.copyWith.game.clock!( - white: newClock, - ) - : curState.copyWith.game.clock!( - black: newClock, - ); + _clock?.setTime(side, newClock); + + // sync game clock object even if it's not used to display the clock + final newState = + side == Side.white + ? curState.copyWith.game.clock!(white: newClock) + : curState.copyWith.game.clock!(black: newClock); state = AsyncValue.data(newState); } @@ -685,25 +733,17 @@ class GameController extends _$GameController { final opponent = curState.game.youAre?.opposite; GameState newState = curState; if (whiteOnGame != null) { - newState = newState.copyWith.game( - white: newState.game.white.setOnGame(whiteOnGame), - ); + newState = newState.copyWith.game(white: newState.game.white.setOnGame(whiteOnGame)); if (opponent == Side.white && whiteOnGame == true) { _opponentLeftCountdownTimer?.cancel(); - newState = newState.copyWith( - opponentLeftCountdown: null, - ); + newState = newState.copyWith(opponentLeftCountdown: null); } } if (blackOnGame != null) { - newState = newState.copyWith.game( - black: newState.game.black.setOnGame(blackOnGame), - ); + newState = newState.copyWith.game(black: newState.game.black.setOnGame(blackOnGame)); if (opponent == Side.black && blackOnGame == true) { _opponentLeftCountdownTimer?.cancel(); - newState = newState.copyWith( - opponentLeftCountdown: null, - ); + newState = newState.copyWith(opponentLeftCountdown: null); } } state = AsyncValue.data(newState); @@ -716,12 +756,8 @@ class GameController extends _$GameController { GameState newState = state.requireValue; final youAre = newState.game.youAre; newState = newState.copyWith.game( - white: youAre == Side.white - ? newState.game.white - : newState.game.white.setGone(isGone), - black: youAre == Side.black - ? newState.game.black - : newState.game.black.setGone(isGone), + white: youAre == Side.white ? newState.game.white : newState.game.white.setGone(isGone), + black: youAre == Side.black ? newState.game.black : newState.game.black.setGone(isGone), ); state = AsyncValue.data(newState); @@ -729,41 +765,25 @@ class GameController extends _$GameController { // before claiming victory is possible case 'goneIn': final timeLeft = Duration(seconds: event.data as int); - state = AsyncValue.data( - state.requireValue.copyWith( - opponentLeftCountdown: timeLeft, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(opponentLeftCountdown: timeLeft)); _opponentLeftCountdownTimer?.cancel(); - _opponentLeftCountdownTimer = Timer.periodic( - const Duration(seconds: 1), - (_) { - final curState = state.requireValue; - final opponentLeftCountdown = curState.opponentLeftCountdown; - if (opponentLeftCountdown == null) { - _opponentLeftCountdownTimer?.cancel(); - } else if (!curState.canShowClaimWinCountdown) { + _opponentLeftCountdownTimer = Timer.periodic(const Duration(seconds: 1), (_) { + final curState = state.requireValue; + final opponentLeftCountdown = curState.opponentLeftCountdown; + if (opponentLeftCountdown == null) { + _opponentLeftCountdownTimer?.cancel(); + } else if (!curState.canShowClaimWinCountdown) { + _opponentLeftCountdownTimer?.cancel(); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: null)); + } else { + final newTime = opponentLeftCountdown - const Duration(seconds: 1); + if (newTime <= Duration.zero) { _opponentLeftCountdownTimer?.cancel(); - state = AsyncValue.data( - curState.copyWith( - opponentLeftCountdown: null, - ), - ); - } else { - final newTime = - opponentLeftCountdown - const Duration(seconds: 1); - if (newTime <= Duration.zero) { - _opponentLeftCountdownTimer?.cancel(); - state = AsyncValue.data( - curState.copyWith(opponentLeftCountdown: null), - ); - } - state = AsyncValue.data( - curState.copyWith(opponentLeftCountdown: newTime), - ); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: null)); } - }, - ); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: newTime)); + } + }); // Event sent when a player adds or cancels a draw offer case 'drawOffer': @@ -771,9 +791,8 @@ class GameController extends _$GameController { final curState = state.requireValue; state = AsyncValue.data( curState.copyWith( - lastDrawOfferAtPly: side != null && side == curState.game.youAre - ? curState.game.lastPly - : null, + lastDrawOfferAtPly: + side != null && side == curState.game.youAre ? curState.game.lastPly : null, game: curState.game.copyWith( white: curState.game.white.copyWith( offeringDraw: side == null ? null : side == Side.white, @@ -794,12 +813,8 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( game: curState.game.copyWith( - white: curState.game.white.copyWith( - proposingTakeback: white ?? false, - ), - black: curState.game.black.copyWith( - proposingTakeback: black ?? false, - ), + white: curState.game.white.copyWith(proposingTakeback: white ?? false), + black: curState.game.black.copyWith(proposingTakeback: black ?? false), ), ), ); @@ -825,51 +840,46 @@ class GameController extends _$GameController { // sending another rematch offer, which should not happen case 'rematchTaken': final nextId = pick(event.data).asGameIdOrThrow(); - state = AsyncValue.data( - state.requireValue.copyWith.game( - rematch: nextId, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith.game(rematch: nextId)); // Event sent after a rematch is taken, to redirect to the new game case 'redirect': final data = event.data as Map; final fullId = pick(data['id']).asGameFullIdOrThrow(); - state = AsyncValue.data( - state.requireValue.copyWith( - redirectGameId: fullId, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(redirectGameId: fullId)); case 'analysisProgress': - final data = - ServerEvalEvent.fromJson(event.data as Map); + final data = ServerEvalEvent.fromJson(event.data as Map); final curState = state.requireValue; state = AsyncValue.data( curState.copyWith.game( - white: curState.game.white.copyWith( - analysis: data.analysis?.white, - ), - black: curState.game.black.copyWith( - analysis: data.analysis?.black, - ), + white: curState.game.white.copyWith(analysis: data.analysis?.white), + black: curState.game.black.copyWith(analysis: data.analysis?.black), evals: data.evals, ), ); } } + Future _storeGame(PlayableGame game) async { + if (game.finished) { + final gameStorage = await ref.read(gameStorageProvider.future); + final existing = await gameStorage.fetch(gameId: gameFullId.gameId); + final finishedAt = existing?.data.lastMoveAt ?? DateTime.now(); + await gameStorage.save(game.toArchivedGame(finishedAt: finishedAt)); + } + } + FutureResult _getPostGameData() { return Result.capture( - ref.withClient( - (client) => GameRepository(client).getGame(gameFullId.gameId), - ), + ref.withClient((client) => GameRepository(client).getGame(gameFullId.gameId)), ); } PlayableGame _mergePostGameData( PlayableGame game, ArchivedGame data, { + /// Whether to rewrite the steps with the clock data from the archived game /// /// This should not be done when the game has just finished, because we @@ -879,40 +889,36 @@ class GameController extends _$GameController { IList newSteps = game.steps; if (rewriteSteps && game.meta.clock != null && data.clocks != null) { final initialTime = game.meta.clock!.initial; - newSteps = game.steps.mapIndexed((index, element) { - if (index == 0) { - return element.copyWith( - archivedWhiteClock: initialTime, - archivedBlackClock: initialTime, - ); - } - final prevClock = index > 1 ? data.clocks![index - 2] : initialTime; - final stepClock = data.clocks![index - 1]; - return element.copyWith( - archivedWhiteClock: index.isOdd ? stepClock : prevClock, - archivedBlackClock: index.isEven ? stepClock : prevClock, - ); - }).toIList(); + newSteps = + game.steps.mapIndexed((index, element) { + if (index == 0) { + return element.copyWith( + archivedWhiteClock: initialTime, + archivedBlackClock: initialTime, + ); + } + final prevClock = index > 1 ? data.clocks![index - 2] : initialTime; + final stepClock = data.clocks![index - 1]; + return element.copyWith( + archivedWhiteClock: index.isOdd ? stepClock : prevClock, + archivedBlackClock: index.isEven ? stepClock : prevClock, + ); + }).toIList(); } return game.copyWith( steps: newSteps, clocks: data.clocks, - meta: game.meta.copyWith( - opening: data.meta.opening, - division: data.meta.division, - ), - white: game.white.copyWith( - analysis: data.white.analysis, - ), - black: game.black.copyWith( - analysis: data.black.analysis, - ), + meta: game.meta.copyWith(opening: data.meta.opening, division: data.meta.division), + white: game.white.copyWith(analysis: data.white.analysis), + black: game.black.copyWith(analysis: data.black.analysis), evals: data.evals, ); } } +typedef LiveGameClock = ({ValueListenable white, ValueListenable black}); + @freezed class GameState with _$GameState { const GameState._(); @@ -921,10 +927,15 @@ class GameState with _$GameState { required GameFullId gameFullId, required PlayableGame game, required int stepCursor, + required LiveGameClock? liveClock, int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, - required bool stopClockWaitingForServerAck, - cg.Move? premove, + + /// Promotion waiting to be selected (only if auto queen is disabled) + NormalMove? promotionMove, + + /// Premove waiting to be played + NormalMove? premove, /// Game only setting to override the account preference bool? moveConfirmSettingOverride, @@ -942,23 +953,28 @@ class GameState with _$GameState { GameFullId? redirectGameId, }) = _GameState; + /// The [Position] and its legal moves at the current cursor. + (Position, IMap>) get currentPosition { + final position = game.positionAt(stepCursor); + final legalMoves = makeLegalMoves(position, isChess960: game.meta.variant == Variant.chess960); + return (position, legalMoves); + } + /// Whether the zen mode is active - bool get isZenModeActive => game.playable && isZenModeEnabled; + bool get isZenModeActive => game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes; /// Whether zen mode is enabled by account preference or local game setting bool get isZenModeEnabled => - zenModeGameSetting ?? - game.prefs?.zenMode == Zen.yes || game.prefs?.zenMode == Zen.gameAuto; + zenModeGameSetting ?? game.prefs?.zenMode == Zen.yes || game.prefs?.zenMode == Zen.gameAuto; bool get canPremove => - game.meta.speed != Speed.correspondence && - (game.prefs?.enablePremove ?? true); - bool get canAutoQueen => - autoQueenSettingOverride ?? (game.prefs?.autoQueen == AutoQueen.always); - bool get canAutoQueenOnPremove => game.prefs?.autoQueen == AutoQueen.premove; + game.meta.speed != Speed.correspondence && (game.prefs?.enablePremove ?? true); + bool get canAutoQueen => autoQueenSettingOverride ?? (game.prefs?.autoQueen == AutoQueen.always); + bool get canAutoQueenOnPremove => + autoQueenSettingOverride ?? + (game.prefs?.autoQueen == AutoQueen.always || game.prefs?.autoQueen == AutoQueen.premove); bool get shouldConfirmResignAndDrawOffer => game.prefs?.confirmResign ?? true; - bool get shouldConfirmMove => - moveConfirmSettingOverride ?? game.prefs?.submitMove ?? false; + bool get shouldConfirmMove => moveConfirmSettingOverride ?? game.prefs?.submitMove ?? false; bool get isReplaying => stepCursor < game.steps.length - 1; bool get canGoForward => stepCursor < game.steps.length - 1; @@ -969,23 +985,19 @@ class GameState with _$GameState { game.meta.speed != Speed.correspondence && (game.source == GameSource.lobby || game.source == GameSource.pool); - bool get canOfferDraw => - game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; + bool get canOfferDraw => game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; bool get canShowClaimWinCountdown => - !game.isPlayerTurn && + !game.isMyTurn && game.resignable && - (game.meta.rules == null || - !game.meta.rules!.contains(GameRule.noClaimWin)); + (game.meta.rules == null || !game.meta.rules!.contains(GameRule.noClaimWin)); bool get canOfferRematch => game.rematch == null && game.rematchable && (game.finished || (game.aborted && - (!game.meta.rated || - !{GameSource.lobby, GameSource.pool} - .contains(game.source)))) && + (!game.meta.rated || !{GameSource.lobby, GameSource.pool}.contains(game.source)))) && game.boosted != true; /// Time left to move for the active player if an expiration is set @@ -993,8 +1005,8 @@ class GameState with _$GameState { if (!game.playable || game.expiration == null) { return null; } - final timeLeft = game.expiration!.movedAt.difference(DateTime.now()) + - game.expiration!.timeToMove; + final timeLeft = + game.expiration!.movedAt.difference(DateTime.now()) + game.expiration!.timeToMove; if (timeLeft.isNegative) { return Duration.zero; @@ -1006,9 +1018,6 @@ class GameState with _$GameState { if (game.clock == null && game.correspondenceClock == null) { return null; } - if (stopClockWaitingForServerAck) { - return null; - } if (game.status == GameStatus.started) { final pos = game.lastPosition; if (pos.fullmoves > 1) { @@ -1021,14 +1030,20 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); - AnalysisOptions get analysisOptions => AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.meta.variant, - initialMoveCursor: stepCursor, - orientation: game.youAre ?? Side.white, - id: gameFullId, - opening: game.meta.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, - ); + AnalysisOptions get analysisOptions => + game.finished + ? AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + gameId: gameFullId.gameId, + ) + : AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + standalone: ( + pgn: game.makePgn(), + variant: game.meta.variant, + isComputerAnalysisAllowed: false, + ), + ); } diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart new file mode 100644 index 0000000000..3b218a74a4 --- /dev/null +++ b/lib/src/model/game/game_filter.dart @@ -0,0 +1,54 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_filter.freezed.dart'; +part 'game_filter.g.dart'; + +@riverpod +class GameFilter extends _$GameFilter { + @override + GameFilterState build({GameFilterState? filter}) { + return filter ?? const GameFilterState(); + } + + void setFilter(GameFilterState filter) => + state = state.copyWith(perfs: filter.perfs, side: filter.side); +} + +@freezed +class GameFilterState with _$GameFilterState { + const GameFilterState._(); + + const factory GameFilterState({@Default(ISet.empty()) ISet perfs, Side? side}) = + _GameFilterState; + + /// Returns a translated label of the selected filters. + String selectionLabel(BuildContext context) { + final fields = [side, perfs]; + final labels = + fields + .map( + (field) => + field is ISet + ? field.map((e) => e.shortTitle).join(', ') + : (field as Side?) != null + ? field == Side.white + ? context.l10n.white + : context.l10n.black + : null, + ) + .where((label) => label != null && label.isNotEmpty) + .toList(); + return labels.isEmpty ? 'All' : labels.join(', '); + } + + int get count { + final fields = [perfs, side]; + return fields.where((field) => field is Iterable ? field.isNotEmpty : field != null).length; + } +} diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 940bd99edd..9191de56bb 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -3,18 +3,19 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -32,42 +33,35 @@ const _nbPerPage = 20; /// If the user is not logged in, or there is no connectivity, the recent games /// stored locally are fetched instead. @riverpod -Future> myRecentGames( - MyRecentGamesRef ref, -) async { - final online = await ref - .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); +Future> myRecentGames(Ref ref) async { + final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final session = ref.watch(authSessionProvider); if (session != null && online) { return ref.withClientCacheFor( - (client) => GameRepository(client) - .getUserGames(session.user.id, max: kNumberOfRecentGames), + (client) => GameRepository(client).getUserGames(session.user.id, max: kNumberOfRecentGames), const Duration(hours: 1), ); } else { - final storage = ref.watch(gameStorageProvider); + final storage = await ref.watch(gameStorageProvider.future); ref.cacheFor(const Duration(hours: 1)); return storage .page(userId: session?.user.id, max: kNumberOfRecentGames) .then( - (value) => value - // we can assume that `youAre` is not null either for logged - // in users or for anonymous users - .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) - .toIList(), + (value) => + value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), ); } } /// A provider that fetches the recent games from the server for a given user. @riverpod -Future> userRecentGames( - UserRecentGamesRef ref, { - required UserId userId, - Perf? perf, -}) { +Future> userRecentGames(Ref ref, {required UserId userId}) { return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(userId, perfType: perf), + (client) => GameRepository(client).getUserGames(userId), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -83,19 +77,13 @@ Future> userRecentGames( /// If the user is not logged in, or there is no connectivity, the number of games /// stored locally are fetched instead. @riverpod -Future userNumberOfGames( - UserNumberOfGamesRef ref, - LightUser? user, { - required bool isOnline, -}) async { +Future userNumberOfGames(Ref ref, LightUser? user, {required bool isOnline}) async { final session = ref.watch(authSessionProvider); return user != null - ? ref.watch( - userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), - ) + ? ref.watch(userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0)) : session != null && isOnline - ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) - : ref.watch(gameStorageProvider).count(userId: user?.id); + ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) + : (await ref.watch(gameStorageProvider.future)).count(userId: user?.id); } /// A provider that paginates the game history for a given user, or the current app user if no user is provided. @@ -109,6 +97,7 @@ class UserGameHistory extends _$UserGameHistory { @override Future build( UserId? userId, { + /// Whether the history is requested in an online context. Applicable only /// when [userId] is null. /// @@ -116,7 +105,7 @@ class UserGameHistory extends _$UserGameHistory { /// server. If this is false, the provider will fetch the games from the /// local storage. required bool isOnline, - Perf? perf, + GameFilterState filter = const GameFilterState(), }) async { ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { @@ -124,15 +113,23 @@ class UserGameHistory extends _$UserGameHistory { }); final session = ref.watch(authSessionProvider); - - final recentGames = userId != null - ? ref.read( - userRecentGamesProvider( - userId: userId, - perf: perf, - ).future, - ) - : ref.read(myRecentGamesProvider.future); + final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); + final storage = await ref.watch(gameStorageProvider.future); + + final id = userId ?? session?.user.id; + final recentGames = + id != null && online + ? ref.withClient((client) => GameRepository(client).getUserGames(id, filter: filter)) + : storage + .page(userId: id, filter: filter) + .then( + (value) => + value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), + ); _list.addAll(await recentGames); @@ -142,13 +139,13 @@ class UserGameHistory extends _$UserGameHistory { hasMore: true, hasError: false, online: isOnline, - perfType: perf, + filter: filter, session: session, ); } /// Fetches the next page of games. - void getNext() { + Future getNext() async { if (!state.hasValue) return; final currentVal = state.requireValue; @@ -156,43 +153,36 @@ class UserGameHistory extends _$UserGameHistory { Result.capture( userId != null ? ref.withClient( - (client) => GameRepository(client).getUserGames( - userId!, - max: _nbPerPage, - until: _list.last.game.createdAt, - perfType: currentVal.perfType, - ), - ) + (client) => GameRepository(client).getUserGames( + userId!, + max: _nbPerPage, + until: _list.last.game.createdAt, + filter: currentVal.filter, + ), + ) : currentVal.online && currentVal.session != null - ? ref.withClient( - (client) => GameRepository(client).getUserGames( - currentVal.session!.user.id, - max: _nbPerPage, - until: _list.last.game.createdAt, - perfType: currentVal.perfType, - ), - ) - : ref - .watch(gameStorageProvider) - .page(max: _nbPerPage, until: _list.last.game.createdAt) - .then( - (value) => value + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + currentVal.session!.user.id, + max: _nbPerPage, + until: _list.last.game.createdAt, + filter: currentVal.filter, + ), + ) + : (await ref.watch(gameStorageProvider.future)) + .page(max: _nbPerPage, until: _list.last.game.createdAt) + .then( + (value) => + value // we can assume that `youAre` is not null either for logged // in users or for anonymous users - .map( - (e) => ( - game: e.game.data, - pov: e.game.youAre ?? Side.white - ), - ) + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) .toIList(), - ), + ), ).fold( (value) { if (value.isEmpty) { - state = AsyncData( - currentVal.copyWith(hasMore: false, isLoading: false), - ); + state = AsyncData(currentVal.copyWith(hasMore: false, isLoading: false)); return; } @@ -207,8 +197,7 @@ class UserGameHistory extends _$UserGameHistory { ); }, (error, stackTrace) { - state = - AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + state = AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); }, ); } @@ -219,7 +208,7 @@ class UserGameHistoryState with _$UserGameHistoryState { const factory UserGameHistoryState({ required IList gameList, required bool isLoading, - Perf? perfType, + required GameFilterState filter, required bool hasMore, required bool hasError, required bool online, diff --git a/lib/src/model/game/game_preferences.dart b/lib/src/model/game/game_preferences.dart index 1f843172fe..31311b132a 100644 --- a/lib/src/model/game/game_preferences.dart +++ b/lib/src/model/game/game_preferences.dart @@ -1,58 +1,44 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'game_preferences.freezed.dart'; part 'game_preferences.g.dart'; -const _prefKey = 'game.preferences'; - /// Local game preferences, defined client-side only. @riverpod -class GamePreferences extends _$GamePreferences { +class GamePreferences extends _$GamePreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.game; + + // ignore: avoid_public_notifier_properties + @override + GamePrefs get defaults => GamePrefs.defaults; + @override - GamePrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey); - return stored != null - ? GamePrefState.fromJson(jsonDecode(stored) as Map) - : GamePrefState.defaults; + GamePrefs fromJson(Map json) => GamePrefs.fromJson(json); + + @override + GamePrefs build() { + return fetch(); } Future toggleChat() { final newState = state.copyWith(enableChat: !(state.enableChat ?? false)); - return _save(newState); + return save(newState); } Future toggleBlindfoldMode() { - return _save( - state.copyWith(blindfoldMode: !(state.blindfoldMode ?? false)), - ); - } - - Future _save(GamePrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - _prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; + return save(state.copyWith(blindfoldMode: !(state.blindfoldMode ?? false))); } } @Freezed(fromJson: true, toJson: true) -class GamePrefState with _$GamePrefState { - const factory GamePrefState({ - bool? enableChat, - bool? blindfoldMode, - }) = _GamePrefState; - - static const defaults = GamePrefState( - enableChat: true, - ); - - factory GamePrefState.fromJson(Map json) => - _$GamePrefStateFromJson(json); +class GamePrefs with _$GamePrefs implements Serializable { + const factory GamePrefs({bool? enableChat, bool? blindfoldMode}) = _GamePrefs; + + static const defaults = GamePrefs(enableChat: true); + + factory GamePrefs.fromJson(Map json) => _$GamePrefsFromJson(json); } diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index a0603a7022..9e4e6c1e61 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -1,11 +1,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/network/http.dart'; class GameRepository { const GameRepository(this.client); @@ -14,10 +15,7 @@ class GameRepository { Future getGame(GameId id) { return client.readJson( - Uri( - path: '/game/export/$id', - queryParameters: {'clocks': '1', 'accuracy': '1'}, - ), + Uri(path: '/game/export/$id', queryParameters: {'clocks': '1', 'accuracy': '1'}), headers: {'Accept': 'application/json'}, mapper: ArchivedGame.fromServerJson, ); @@ -25,14 +23,9 @@ class GameRepository { Future requestServerAnalysis(GameId id) async { final uri = Uri(path: '/$id/request-analysis'); - final response = await client.post( - Uri(path: '/$id/request-analysis'), - ); + final response = await client.post(Uri(path: '/$id/request-analysis')); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to request analysis: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to request analysis: ${response.statusCode}', uri); } } @@ -40,32 +33,34 @@ class GameRepository { UserId userId, { int max = 20, DateTime? until, - Perf? perfType, + GameFilterState filter = const GameFilterState(), }) { - assert( - ![Perf.fromPosition, Perf.puzzle, Perf.storm, Perf.streak] - .contains(perfType), - ); + assert(!filter.perfs.contains(Perf.fromPosition)); + assert(!filter.perfs.contains(Perf.puzzle)); + assert(!filter.perfs.contains(Perf.storm)); + assert(!filter.perfs.contains(Perf.streak)); return client .readNdJsonList( Uri( path: '/api/games/user/$userId', queryParameters: { 'max': max.toString(), - if (until != null) - 'until': until.millisecondsSinceEpoch.toString(), - if (perfType != null) 'perfType': perfType.name, + if (until != null) 'until': until.millisecondsSinceEpoch.toString(), 'moves': 'false', 'lastFen': 'true', 'accuracy': 'true', 'opening': 'true', + if (filter.perfs.isNotEmpty) + 'perfType': filter.perfs.map((perf) => perf.name).join(','), + if (filter.side != null) 'color': filter.side!.name, }, ), headers: {'Accept': 'application/x-ndjson'}, mapper: LightArchivedGame.fromServerJson, ) .then( - (value) => value + (value) => + value .map( (e) => ( game: e, @@ -83,22 +78,14 @@ class GameRepository { return Future.value(IList()); } return client.readJsonList( - Uri( - path: '/api/mobile/my-games', - queryParameters: { - 'ids': ids.join(','), - }, - ), + Uri(path: '/api/mobile/my-games', queryParameters: {'ids': ids.join(',')}), mapper: PlayableGame.fromServerJson, ); } Future> getGamesByIds(ISet ids) { return client.postReadNdJsonList( - Uri( - path: '/api/games/export/_ids', - queryParameters: {'moves': 'false', 'lastFen': 'true'}, - ), + Uri(path: '/api/games/export/_ids', queryParameters: {'moves': 'false', 'lastFen': 'true'}), headers: {'Accept': 'application/x-ndjson'}, body: ids.join(','), mapper: LightArchivedGame.fromServerJson, diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index dded6e0df1..c9a95b6e43 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -1,8 +1,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'game_repository.dart'; @@ -11,11 +12,8 @@ part 'game_repository_providers.g.dart'; /// Fetches a game from the local storage if available, otherwise fetches it from the server. @riverpod -Future archivedGame( - ArchivedGameRef ref, { - required GameId id, -}) async { - final gameStorage = ref.watch(gameStorageProvider); +Future archivedGame(Ref ref, {required GameId id}) async { + final gameStorage = await ref.watch(gameStorageProvider.future); final game = await gameStorage.fetch(gameId: id); if (game != null) return game; return ref.withClientCacheFor( @@ -25,11 +23,6 @@ Future archivedGame( } @riverpod -Future> gamesById( - GamesByIdRef ref, { - required ISet ids, -}) { - return ref.withClient( - (client) => GameRepository(client).getGamesByIds(ids), - ); +Future> gamesById(Ref ref, {required ISet ids}) { + return ref.withClient((client) => GameRepository(client).getGamesByIds(ids)); } diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 837fba6753..b0996ffc8d 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -1,35 +1,31 @@ import 'dart:convert'; import 'package:dartchess/dartchess.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:share_plus/share_plus.dart'; part 'game_share_service.g.dart'; @Riverpod(keepAlive: true) -GameShareService gameShareService(GameShareServiceRef ref) { +GameShareService gameShareService(Ref ref) { return GameShareService(ref); } class GameShareService { GameShareService(this._ref); - final GameShareServiceRef _ref; + final Ref _ref; /// Fetches the raw PGN of a game and launches the share dialog. Future rawPgn(GameId id) async { final resp = await _ref.withClient( (client) => client - .get( - Uri( - path: '/game/export/$id', - queryParameters: {'evals': '0', 'clocks': '0'}, - ), - ) + .get(Uri(path: '/game/export/$id', queryParameters: {'evals': '0', 'clocks': '0'})) .timeout(const Duration(seconds: 1)), ); if (resp.statusCode != 200) { @@ -42,12 +38,7 @@ class GameShareService { Future annotatedPgn(GameId id) async { final resp = await _ref.withClient( (client) => client - .get( - Uri( - path: '/game/export/$id', - queryParameters: {'literate': '1'}, - ), - ) + .get(Uri(path: '/game/export/$id', queryParameters: {'literate': '1'})) .timeout(const Duration(seconds: 1)), ); if (resp.statusCode != 200) { @@ -56,20 +47,15 @@ class GameShareService { return utf8.decode(resp.bodyBytes); } - /// Fetches the GIF screenshot of a game and launches the share dialog. - Future screenshotPosition( - GameId id, - Side orientation, - String fen, - Move? lastMove, - ) async { + /// Fetches the GIF screenshot of a position and launches the share dialog. + Future screenshotPosition(Side orientation, String fen, Move? lastMove) async { final boardTheme = _ref.read(boardPreferencesProvider).boardTheme; final pieceTheme = _ref.read(boardPreferencesProvider).pieceSet; final resp = await _ref .read(defaultClientProvider) .get( Uri.parse( - '$kLichessCDNHost/export/fen.gif?fen=${Uri.encodeComponent(fen)}&color=${orientation.name}${lastMove != null ? '&lastMove=${lastMove.uci}' : ''}&theme=${boardTheme.name}&piece=${pieceTheme.name}', + '$kLichessCDNHost/export/fen.gif?fen=${Uri.encodeComponent(fen)}&color=${orientation.name}${lastMove != null ? '&lastMove=${lastMove.uci}' : ''}&theme=${boardTheme.gifApiName}&piece=${pieceTheme.name}', ), ) .timeout(const Duration(seconds: 1)); @@ -79,15 +65,19 @@ class GameShareService { return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); } - /// Fetches the GIF animation of a game and launches the share dialog. + /// Fetches the GIF animation of a game. Future gameGif(GameId id, Side orientation) async { - final boardTheme = _ref.read(boardPreferencesProvider).boardTheme; - final pieceTheme = _ref.read(boardPreferencesProvider).pieceSet; + final boardPreferences = _ref.read(boardPreferencesProvider); + final boardTheme = + boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; + final pieceTheme = boardPreferences.pieceSet; final resp = await _ref .read(defaultClientProvider) .get( Uri.parse( - '$kLichessCDNHost/game/export/gif/${orientation.name}/$id.gif?theme=${boardTheme.name}&piece=${pieceTheme.name}', + '$kLichessCDNHost/game/export/gif/${orientation.name}/$id.gif?theme=${boardTheme.gifApiName}&piece=${pieceTheme.name}', ), ) .timeout(const Duration(seconds: 1)); @@ -96,4 +86,28 @@ class GameShareService { } return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); } + + /// Fetches the GIF animation of a study chapter. + Future chapterGif(StringId id, StringId chapterId) async { + final boardPreferences = _ref.read(boardPreferencesProvider); + final boardTheme = + boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; + final pieceTheme = boardPreferences.pieceSet; + + final resp = await _ref + .read(lichessClientProvider) + .get( + lichessUri('/study/$id/$chapterId.gif', { + 'theme': boardTheme.gifApiName, + 'piece': pieceTheme.name, + }), + ) + .timeout(const Duration(seconds: 1)); + if (resp.statusCode != 200) { + throw Exception('Failed to get GIF'); + } + return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); + } } diff --git a/lib/src/model/game/game_socket_events.dart b/lib/src/model/game/game_socket_events.dart index df44964a89..bd4463a9bf 100644 --- a/lib/src/model/game/game_socket_events.dart +++ b/lib/src/model/game/game_socket_events.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -14,10 +15,8 @@ part 'game_socket_events.freezed.dart'; @freezed class GameFullEvent with _$GameFullEvent { - const factory GameFullEvent({ - required PlayableGame game, - required int socketEventVersion, - }) = _GameFullEvent; + const factory GameFullEvent({required PlayableGame game, required int socketEventVersion}) = + _GameFullEvent; factory GameFullEvent.fromJson(Map json) { return GameFullEvent( @@ -40,7 +39,7 @@ class MoveEvent with _$MoveEvent { bool? blackOfferingDraw, GameStatus? status, Side? winner, - ({Duration white, Duration black, Duration? lag})? clock, + ({Duration white, Duration black, Duration? lag, DateTime at})? clock, }) = _MoveEvent; factory MoveEvent.fromJson(Map json) => @@ -78,10 +77,10 @@ MoveEvent _socketMoveEventFromPick(RequiredPick pick) { blackOfferingDraw: pick('bDraw').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( + at: clock.now(), white: it('white').asDurationFromSecondsOrThrow(), black: it('black').asDurationFromSecondsOrThrow(), - lag: it('lag') - .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), + lag: it('lag').letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), ), ), ); @@ -107,12 +106,9 @@ GameEndEvent _gameEndEventFromPick(RequiredPick pick) { return GameEndEvent( status: pick('status').asGameStatusOrThrow(), winner: pick('winner').asSideOrNull(), - ratingDiff: pick('ratingDiff').letOrNull( - (it) => ( - white: it('white').asIntOrThrow(), - black: it('black').asIntOrThrow(), - ), - ), + ratingDiff: pick( + 'ratingDiff', + ).letOrNull((it) => (white: it('white').asIntOrThrow(), black: it('black').asIntOrThrow())), boosted: pick('boosted').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( @@ -163,12 +159,10 @@ ServerEvalEvent _serverEvalEventFromPick(RequiredPick pick) { final glyph = glyphs?.first as Map?; final comments = node['comments'] as List?; final comment = comments?.first as Map?; - final judgment = glyph != null && comment != null - ? ( - name: _nagToJugdmentName(glyph['id'] as int), - comment: comment['text'] as String, - ) - : null; + final judgment = + glyph != null && comment != null + ? (name: _nagToJugdmentName(glyph['id'] as int), comment: comment['text'] as String) + : null; final variation = nextVariation; @@ -236,29 +230,19 @@ ServerEvalEvent _serverEvalEventFromPick(RequiredPick pick) { ), ), isAnalysisComplete: !isAnalysisIncomplete, - division: pick('division').letOrNull( - (it) => ( - middle: it('middle').asIntOrNull(), - end: it('end').asIntOrNull(), - ), - ), + division: pick( + 'division', + ).letOrNull((it) => (middle: it('middle').asIntOrNull(), end: it('end').asIntOrNull())), ); } String _nagToJugdmentName(int nag) => switch (nag) { - 6 => 'Inaccuracy', - 2 => 'Mistake', - 4 => 'Blunder', - int() => '', - }; + 6 => 'Inaccuracy', + 2 => 'Mistake', + 4 => 'Blunder', + int() => '', +}; -typedef ServerAnalysis = ({ - GameId id, - PlayerAnalysis white, - PlayerAnalysis black, -}); +typedef ServerAnalysis = ({GameId id, PlayerAnalysis white, PlayerAnalysis black}); -typedef GameDivision = ({ - int? middle, - int? end, -}); +typedef GameDivision = ({int? middle, int? end}); diff --git a/lib/src/model/game/game_status.dart b/lib/src/model/game/game_status.dart index 58047c6aac..19242a75ca 100644 --- a/lib/src/model/game/game_status.dart +++ b/lib/src/model/game/game_status.dart @@ -2,19 +2,34 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; enum GameStatus { + /// Unknown game status (not handled by the app). unknown(-1), + + /// The game is created but not started yet. created(10), started(20), + + /// From here on, the game is finished. aborted(25), mate(30), resign(31), stalemate(32), + + /// When a player leaves the game. timeout(33), draw(34), + + /// When a player runs out of time (clock flags). outoftime(35), cheat(36), + + /// The player did not make the first move in time. noStart(37), + + /// We don't know why the game ended. unknownFinish(38), + + /// Chess variant special endings. variantEnd(60); static final nameMap = IMap(GameStatus.values.asNameMap()); @@ -40,9 +55,7 @@ extension GameExtension on Pick { return gameStatus; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameStatus", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameStatus"); } GameStatus? asGameStatusOrNull() { diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index 7d957682d0..1a5fddc1e7 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -1,37 +1,31 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; part 'game_storage.g.dart'; @Riverpod(keepAlive: true) -GameStorage gameStorage( - GameStorageRef ref, -) { - final db = ref.watch(databaseProvider); +Future gameStorage(Ref ref) async { + final db = await ref.watch(databaseProvider.future); return GameStorage(db); } const kGameStorageTable = 'game'; -typedef StoredGame = ({ - UserId userId, - DateTime lastModified, - ArchivedGame game, -}); +typedef StoredGame = ({UserId userId, DateTime lastModified, ArchivedGame game}); class GameStorage { const GameStorage(this._db); final Database _db; - Future count({ - UserId? userId, - }) async { + Future count({UserId? userId}) async { final list = await _db.query( kGameStorageTable, where: 'userId = ?', @@ -44,40 +38,35 @@ class GameStorage { UserId? userId, DateTime? until, int max = 20, + GameFilterState filter = const GameFilterState(), }) async { final list = await _db.query( kGameStorageTable, - where: [ - 'userId = ?', - if (until != null) 'lastModified < ?', - ].join(' AND '), - whereArgs: [ - userId ?? kStorageAnonId, - if (until != null) until.toIso8601String(), - ], + where: ['userId = ?', if (until != null) 'lastModified < ?'].join(' AND '), + whereArgs: [userId ?? kStorageAnonId, if (until != null) until.toIso8601String()], orderBy: 'lastModified DESC', limit: max, ); - return list.map((e) { - final raw = e['data']! as String; - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[GameStorage] cannot fetch game: expected an object', - ); - } - return ( - userId: UserId(e['userId']! as String), - lastModified: DateTime.parse(e['lastModified']! as String), - game: ArchivedGame.fromJson(json), - ); - }).toIList(); + return list + .map((e) { + final raw = e['data']! as String; + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException('[GameStorage] cannot fetch game: expected an object'); + } + return ( + userId: UserId(e['userId']! as String), + lastModified: DateTime.parse(e['lastModified']! as String), + game: ArchivedGame.fromJson(json), + ); + }) + .where((e) => filter.perfs.isEmpty || filter.perfs.contains(e.game.meta.perf)) + .where((e) => filter.side == null || filter.side == e.game.youAre) + .toIList(); } - Future fetch({ - required GameId gameId, - }) async { + Future fetch({required GameId gameId}) async { final list = await _db.query( kGameStorageTable, where: 'gameId = ?', @@ -89,9 +78,7 @@ class GameStorage { if (raw != null) { final json = jsonDecode(raw); if (json is! Map) { - throw const FormatException( - '[GameStorage] cannot fetch game: expected an object', - ); + throw const FormatException('[GameStorage] cannot fetch game: expected an object'); } return ArchivedGame.fromJson(json); } @@ -99,23 +86,15 @@ class GameStorage { } Future save(ArchivedGame game) async { - await _db.insert( - kGameStorageTable, - { - 'userId': game.me?.user?.id.toString() ?? kStorageAnonId, - 'gameId': game.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(game.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await _db.insert(kGameStorageTable, { + 'userId': game.me?.user?.id.toString() ?? kStorageAnonId, + 'gameId': game.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(game.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); } Future delete(GameId gameId) async { - await _db.delete( - kGameStorageTable, - where: 'gameId = ?', - whereArgs: [gameId.toString()], - ); + await _db.delete(kGameStorageTable, where: 'gameId = ?', whereArgs: [gameId.toString()]); } } diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 6fe688f4de..b0bde7bd79 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -9,6 +9,7 @@ class MaterialDiffSide with _$MaterialDiffSide { const factory MaterialDiffSide({ required IMap pieces, required int score, + required IMap capturedPieces, }) = _MaterialDiffSide; } @@ -25,16 +26,31 @@ const IMap pieceScores = IMapConst({ class MaterialDiff with _$MaterialDiff { const MaterialDiff._(); - const factory MaterialDiff({ - required MaterialDiffSide black, - required MaterialDiffSide white, - }) = _MaterialDiff; + const factory MaterialDiff({required MaterialDiffSide black, required MaterialDiffSide white}) = + _MaterialDiff; - factory MaterialDiff.fromBoard(Board board) { + factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) { int score = 0; final IMap blackCount = board.materialCount(Side.black); final IMap whiteCount = board.materialCount(Side.white); + final IMap blackStartingCount = + startingPosition?.materialCount(Side.black) ?? Board.standard.materialCount(Side.black); + final IMap whiteStartingCount = + startingPosition?.materialCount(Side.white) ?? Board.standard.materialCount(Side.white); + + IMap subtractPieceCounts( + IMap startingCount, + IMap subtractCount, + ) { + return startingCount.map( + (role, count) => MapEntry(role, count - (subtractCount.get(role) ?? 0)), + ); + } + + final IMap blackCapturedPieces = subtractPieceCounts(whiteStartingCount, whiteCount); + final IMap whiteCapturedPieces = subtractPieceCounts(blackStartingCount, blackCount); + Map count; Map black; Map white; @@ -80,8 +96,16 @@ class MaterialDiff with _$MaterialDiff { }); return MaterialDiff( - black: MaterialDiffSide(pieces: black.toIMap(), score: -score), - white: MaterialDiffSide(pieces: white.toIMap(), score: score), + black: MaterialDiffSide( + pieces: black.toIMap(), + score: -score, + capturedPieces: blackCapturedPieces, + ), + white: MaterialDiffSide( + pieces: white.toIMap(), + score: score, + capturedPieces: whiteCapturedPieces, + ), ); } diff --git a/lib/src/model/game/over_the_board_game.dart b/lib/src/model/game/over_the_board_game.dart new file mode 100644 index 0000000000..20633e646e --- /dev/null +++ b/lib/src/model/game/over_the_board_game.dart @@ -0,0 +1,45 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +import 'game.dart'; +import 'game_status.dart'; +import 'player.dart'; + +part 'over_the_board_game.freezed.dart'; +part 'over_the_board_game.g.dart'; + +/// An offline game played in real life by two human players on the same device. +/// +/// See [PlayableGame] for a game that is played online. +@Freezed(fromJson: true, toJson: true) +abstract class OverTheBoardGame with _$OverTheBoardGame, BaseGame, IndexableSteps { + const OverTheBoardGame._(); + + @override + Player get white => const Player(); + @override + Player get black => const Player(); + + @override + IList? get evals => null; + @override + IList? get clocks => null; + + @override + GameId get id => const GameId('--------'); + + @Assert('steps.isNotEmpty') + factory OverTheBoardGame({ + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, + required GameMeta meta, + required String? initialFen, + required GameStatus status, + Side? winner, + bool? isThreefoldRepetition, + }) = _OverTheBoardGame; + + bool get finished => status.value >= GameStatus.mate.value; +} diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index a9cc421a8e..abd6ea85e7 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -30,9 +30,7 @@ part 'playable_game.freezed.dart'; /// See also: /// - [ArchivedGame] for a game that is finished and not owned by the current user. @freezed -class PlayableGame - with _$PlayableGame, BaseGame, IndexableSteps - implements BaseGame { +class PlayableGame with _$PlayableGame, BaseGame, IndexableSteps implements BaseGame { const PlayableGame._(); @Assert('steps.isNotEmpty') @@ -54,7 +52,7 @@ class PlayableGame /// The side that the current player is playing as. This is null if viewing /// the game as a spectator. Side? youAre, - GamePrefs? prefs, + ServerGamePrefs? prefs, PlayableClockData? clock, CorrespondenceClockData? correspondenceClock, bool? boosted, @@ -76,16 +74,18 @@ class PlayableGame } /// Player of the playing point of view. Null if spectating. - Player? get me => youAre == null - ? null - : youAre == Side.white + Player? get me => + youAre == null + ? null + : youAre == Side.white ? white : black; /// Opponent from the playing point of view. Null if spectating. - Player? get opponent => youAre == null - ? null - : youAre == Side.white + Player? get opponent => + youAre == null + ? null + : youAre == Side.white ? black : white; @@ -95,7 +95,8 @@ class PlayableGame bool get imported => source == GameSource.import; - bool get isPlayerTurn => lastPosition.turn == youAre; + /// Whether it is the current player's turn. + bool get isMyTurn => lastPosition.turn == youAre; /// Whether the game is properly finished (not aborted). bool get finished => status.value >= GameStatus.mate.value; @@ -109,12 +110,8 @@ class PlayableGame (meta.rules == null || !meta.rules!.contains(GameRule.noAbort)); bool get resignable => playable && !abortable; bool get drawable => - playable && - lastPosition.fullmoves >= 2 && - !(me?.offeringDraw == true) && - !hasAI; - bool get rematchable => - meta.rules == null || !meta.rules!.contains(GameRule.noRematch); + playable && lastPosition.fullmoves >= 2 && !(me?.offeringDraw == true) && !hasAI; + bool get rematchable => meta.rules == null || !meta.rules!.contains(GameRule.noRematch); bool get canTakeback => takebackable && playable && @@ -125,18 +122,18 @@ class PlayableGame bool get canClaimWin => opponent?.isGone == true && - !isPlayerTurn && + !isMyTurn && resignable && (meta.rules == null || !meta.rules!.contains(GameRule.noClaimWin)); bool get userAnalysable => - finished && steps.length > 4 || - (playable && (clock == null || youAre == null)); + finished && steps.length > 4 || (playable && (clock == null || youAre == null)); ArchivedGame toArchivedGame({required DateTime finishedAt}) { return ArchivedGame( id: id, meta: meta, + source: source, data: LightArchivedGame( id: id, variant: meta.variant, @@ -151,16 +148,17 @@ class PlayableGame status: status, white: white, black: black, - clock: meta.clock != null - ? ( - initial: meta.clock!.initial, - increment: meta.clock!.increment, - ) - : null, + clock: + meta.clock != null + ? (initial: meta.clock!.initial, increment: meta.clock!.increment) + : null, opening: meta.opening, ), + initialFen: initialFen, steps: steps, status: status, + winner: winner, + isThreefoldRepetition: isThreefoldRepetition, white: white, black: black, youAre: youAre, @@ -170,6 +168,23 @@ class PlayableGame } } +@freezed +class PlayableClockData with _$PlayableClockData { + const factory PlayableClockData({ + required bool running, + required Duration white, + required Duration black, + + /// The network lag of the clock. + /// + /// Will be sent along with move events. + required Duration? lag, + + /// The time when the clock event was received. + required DateTime at, + }) = _PlayableClockData; +} + PlayableGame _playableGameFromPick(RequiredPick pick) { final requiredGamePick = pick('game').required(); final meta = _playableGameMetaFromPick(pick); @@ -202,17 +217,15 @@ PlayableGame _playableGameFromPick(RequiredPick pick) { return PlayableGame( id: requiredGamePick('id').asGameIdOrThrow(), meta: meta, - source: requiredGamePick('source').letOrThrow( - (pick) => - GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, - ), + source: requiredGamePick( + 'source', + ).letOrThrow((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), initialFen: initialFen, steps: steps.toIList(), white: pick('white').letOrThrow(_playerFromUserGamePick), black: pick('black').letOrThrow(_playerFromUserGamePick), clock: pick('clock').letOrNull(_playableClockDataFromPick), - correspondenceClock: - pick('correspondence').letOrNull(_correspondenceClockDataFromPick), + correspondenceClock: pick('correspondence').letOrNull(_correspondenceClockDataFromPick), status: pick('game', 'status').asGameStatusOrThrow(), winner: pick('game', 'winner').asSideOrNull(), boosted: pick('game', 'boosted').asBoolOrNull(), @@ -221,16 +234,14 @@ PlayableGame _playableGameFromPick(RequiredPick pick) { takebackable: pick('takebackable').asBoolOrFalse(), youAre: pick('youAre').asSideOrNull(), prefs: pick('prefs').letOrNull(_gamePrefsFromPick), - expiration: pick('expiration').letOrNull( - (it) { - final idle = it('idleMillis').asDurationFromMilliSecondsOrThrow(); - return ( - idle: idle, - timeToMove: it('millisToMove').asDurationFromMilliSecondsOrThrow(), - movedAt: DateTime.now().subtract(idle), - ); - }, - ), + expiration: pick('expiration').letOrNull((it) { + final idle = it('idleMillis').asDurationFromMilliSecondsOrThrow(); + return ( + idle: idle, + timeToMove: it('millisToMove').asDurationFromMilliSecondsOrThrow(), + movedAt: DateTime.now().subtract(idle), + ); + }), rematch: pick('game', 'rematch').asGameIdOrNull(), ); } @@ -250,22 +261,19 @@ GameMeta _playableGameMetaFromPick(RequiredPick pick) { moreTime: cPick('moretime').asDurationFromSecondsOrNull(), ), ), - daysPerTurn: pick('correspondence') - .letOrNull((ccPick) => ccPick('daysPerTurn').asIntOrThrow()), + daysPerTurn: pick('correspondence').letOrNull((ccPick) => ccPick('daysPerTurn').asIntOrThrow()), startedAtTurn: pick('game', 'startedAtTurn').asIntOrNull(), rules: pick('game', 'rules').letOrNull( (it) => ISet( - pick.asListOrThrow( - (e) => GameRule.nameMap[e.asStringOrThrow()] ?? GameRule.unknown, - ), + pick.asListOrThrow((e) => GameRule.nameMap[e.asStringOrThrow()] ?? GameRule.unknown), ), ), division: pick('division').letOrNull(_divisionFromPick), ); } -GamePrefs _gamePrefsFromPick(RequiredPick pick) { - return GamePrefs( +ServerGamePrefs _gamePrefsFromPick(RequiredPick pick) { + return ServerGamePrefs( showRatings: pick('showRatings').asBoolOrFalse(), enablePremove: pick('enablePremove').asBoolOrFalse(), autoQueen: AutoQueen.fromInt(pick('autoQueen').asIntOrThrow()), @@ -295,6 +303,8 @@ PlayableClockData _playableClockDataFromPick(RequiredPick pick) { running: pick('running').asBoolOrThrow(), white: pick('white').asDurationFromSecondsOrThrow(), black: pick('black').asDurationFromSecondsOrThrow(), + lag: pick('lag').letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), + at: DateTime.now(), ); } diff --git a/lib/src/model/game/player.dart b/lib/src/model/game/player.dart index 6eac700062..8607e2a5c6 100644 --- a/lib/src/model/game/player.dart +++ b/lib/src/model/game/player.dart @@ -13,6 +13,7 @@ class Player with _$Player { const factory Player({ LightUser? user, + String? name, int? aiLevel, int? rating, int? ratingDiff, @@ -47,11 +48,9 @@ class Player with _$Player { /// Returns the name of the player, without title String displayName(BuildContext context) => user?.name ?? + name ?? (aiLevel != null - ? context.l10n.aiNameLevelAiLevel( - 'Stockfish', - aiLevel.toString(), - ) + ? context.l10n.aiNameLevelAiLevel('Stockfish', aiLevel.toString()) : context.l10n.anonymous); Player setOnGame(bool onGame) { @@ -75,6 +74,5 @@ class PlayerAnalysis with _$PlayerAnalysis { int? accuracy, }) = _PlayerAnalysis; - factory PlayerAnalysis.fromJson(Map json) => - _$PlayerAnalysisFromJson(json); + factory PlayerAnalysis.fromJson(Map json) => _$PlayerAnalysisFromJson(json); } diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 97a36fd8d2..fef13347f5 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,21 +1,28 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'create_game_service.g.dart'; +typedef ChallengeResponse = + ({GameFullId? gameFullId, Challenge? challenge, ChallengeDeclineReason? declineReason}); + +/// A provider for the [CreateGameService]. @riverpod -CreateGameService createGameService(CreateGameServiceRef ref) { +CreateGameService createGameService(Ref ref) { final service = CreateGameService(Logger('CreateGameService'), ref: ref); ref.onDispose(() { service.dispose(); @@ -23,10 +30,11 @@ CreateGameService createGameService(CreateGameServiceRef ref) { return service; } +/// A service to create a new game from the lobby or from a challenge. class CreateGameService { CreateGameService(this._log, {required this.ref}); - final CreateGameServiceRef ref; + final Ref ref; final Logger _log; LichessClient get lichessClient => ref.read(lichessClientProvider); @@ -39,7 +47,8 @@ class CreateGameService { ChallengeId, StreamSubscription, // socket connects events StreamSubscription, // socket events - )? _challengeConnection; + )? + _challengeConnection; Timer? _challengePingTimer; @@ -79,8 +88,7 @@ class CreateGameService { } try { - await LobbyRepository(lichessClient) - .createSeek(actualSeek, sri: socketClient.sri); + await LobbyRepository(lichessClient).createSeek(actualSeek, sri: socketClient.sri); } catch (e) { _log.warning('Failed to create seek', e); // if the completer is not yet completed, complete it with an error @@ -97,26 +105,26 @@ class CreateGameService { _log.info('Creating new correspondence game'); await ref.withClient( - (client) => LobbyRepository(client).createSeek( - seek, - sri: ref.read(sriProvider), - ), + (client) => LobbyRepository( + client, + ).createSeek(seek, sri: ref.read(preloadedDataProvider).requireValue.sri), ); } - /// Create a new challenge game. + /// Create a new real time challenge. /// - /// Returns the game id or the decline reason if the challenge was declined. - Future<(GameFullId?, DeclineReason?)> newChallenge( - ChallengeRequest challengeReq, - ) async { + /// Will listen to the challenge socket and await the response from the destinated user. + /// Returns the challenge, along with [GameFullId] if the challenge was accepted, + /// or the [ChallengeDeclineReason] if the challenge was declined. + Future newRealTimeChallenge(ChallengeRequest challengeReq) async { + assert(challengeReq.timeControl == ChallengeTimeControlType.clock); + if (_challengeConnection != null) { throw StateError('Already creating a game.'); } // ensure the pending connection is closed in any case - final completer = Completer<(GameFullId?, DeclineReason?)>() - ..future.whenComplete(dispose); + final completer = Completer()..future.whenComplete(dispose); try { _log.info('Creating new challenge game'); @@ -147,9 +155,17 @@ class CreateGameService { try { final updatedChallenge = await repo.show(challenge.id); if (updatedChallenge.gameFullId != null) { - completer.complete((updatedChallenge.gameFullId, null)); + completer.complete(( + gameFullId: updatedChallenge.gameFullId, + challenge: null, + declineReason: null, + )); } else if (updatedChallenge.status == ChallengeStatus.declined) { - completer.complete((null, updatedChallenge.declineReason)); + completer.complete(( + gameFullId: null, + challenge: challenge, + declineReason: updatedChallenge.declineReason, + )); } } catch (e) { _log.warning('Failed to reload challenge', e); @@ -168,10 +184,22 @@ class CreateGameService { return completer.future; } + /// Create a new correspondence challenge. + /// + /// Returns the created challenge immediately. If the challenge is accepted, + /// a notification will be sent to the user when the game starts. + Future newCorrespondenceChallenge(ChallengeRequest challenge) async { + assert(challenge.timeControl == ChallengeTimeControlType.correspondence); + + _log.info('Creating new correspondence challenge'); + + return ref.withClient((client) => ChallengeRepository(client).create(challenge)); + } + /// Cancel the current game creation. Future cancelSeek() async { _log.info('Cancelling game creation'); - final sri = ref.read(sriProvider); + final sri = ref.read(preloadedDataProvider).requireValue.sri; try { await LobbyRepository(lichessClient).cancelSeek(sri: sri); } catch (e) { diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 15b8eb0f5f..ec9e8386b6 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:dartchess/dartchess.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -9,7 +8,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; part 'game_seek.freezed.dart'; @@ -27,16 +26,12 @@ class GameSeek with _$GameSeek { 'ratingDelta == null || ratingRange == null', 'Rating delta and rating range cannot be used together', ) - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set', - ) + @Assert('clock != null || days != null', 'Either clock or days must be set') const factory GameSeek({ (Duration time, Duration increment)? clock, int? days, required bool rated, Variant? variant, - Side? side, /// Rating range (int, int)? ratingRange, @@ -48,16 +43,13 @@ class GameSeek with _$GameSeek { /// Construct a game seek from a predefined time control. factory GameSeek.fastPairing(TimeIncrement setup, AuthSessionState? session) { return GameSeek( - clock: ( - Duration(seconds: setup.time), - Duration(seconds: setup.increment), - ), + clock: (Duration(seconds: setup.time), Duration(seconds: setup.increment)), rated: session != null, ); } - /// Construct a game seek from saved [GameSetup], using all the custom params. - factory GameSeek.custom(GameSetup setup, User? account) { + /// Construct a game seek from saved [GameSetupPrefs], using all the custom params. + factory GameSeek.custom(GameSetupPrefs setup, User? account) { return GameSeek( clock: ( Duration(seconds: setup.customTimeSeconds), @@ -65,43 +57,29 @@ class GameSeek with _$GameSeek { ), rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == PlayableSide.random - ? null - : setup.customSide == PlayableSide.white - ? Side.white - : Side.black, - ratingRange: - account != null ? setup.ratingRangeFromCustom(account) : null, + ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); } - /// Construct a correspondence seek from saved [GameSetup]. - factory GameSeek.correspondence(GameSetup setup, User? account) { + /// Construct a correspondence seek from saved [GameSetupPrefs]. + factory GameSeek.correspondence(GameSetupPrefs setup, User? account) { return GameSeek( days: setup.customDaysPerTurn, rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == PlayableSide.random - ? null - : setup.customSide == PlayableSide.white - ? Side.white - : Side.black, - ratingRange: - account != null ? setup.ratingRangeFromCustom(account) : null, + ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); } /// Construct a game seek from a playable game to find a new opponent, using /// the same time control, variant and rated status. - factory GameSeek.newOpponentFromGame(PlayableGame game, GameSetup setup) { + factory GameSeek.newOpponentFromGame(PlayableGame game, GameSetupPrefs setup) { return GameSeek( - clock: game.meta.clock != null - ? (game.meta.clock!.initial, game.meta.clock!.increment) - : null, + clock: + game.meta.clock != null ? (game.meta.clock!.initial, game.meta.clock!.increment) : null, rated: game.meta.rated, variant: game.meta.variant, - ratingDelta: - game.source == GameSource.lobby ? setup.customRatingDelta : null, + ratingDelta: game.source == GameSource.lobby ? setup.customRatingDelta : null, ); } @@ -118,28 +96,20 @@ class GameSeek with _$GameSeek { return copyWith(ratingRange: range, ratingDelta: null); } - TimeIncrement? get timeIncrement => clock != null - ? TimeIncrement( - clock!.$1.inSeconds, - clock!.$2.inSeconds, - ) - : null; + TimeIncrement? get timeIncrement => + clock != null ? TimeIncrement(clock!.$1.inSeconds, clock!.$2.inSeconds) : null; Perf get perf => Perf.fromVariantAndSpeed( - variant ?? Variant.standard, - timeIncrement != null - ? Speed.fromTimeIncrement(timeIncrement!) - : Speed.correspondence, - ); + variant ?? Variant.standard, + timeIncrement != null ? Speed.fromTimeIncrement(timeIncrement!) : Speed.correspondence, + ); Map get requestBody => { - if (clock != null) 'time': (clock!.$1.inSeconds / 60).toString(), - if (clock != null) 'increment': clock!.$2.inSeconds.toString(), - if (days != null) 'days': days.toString(), - 'rated': rated.toString(), - if (variant != null) 'variant': variant!.name, - if (side != null) 'color': side!.name, - if (ratingRange != null) - 'ratingRange': '${ratingRange!.$1}-${ratingRange!.$2}', - }; + if (clock != null) 'time': (clock!.$1.inSeconds / 60).toString(), + if (clock != null) 'increment': clock!.$2.inSeconds.toString(), + if (days != null) 'days': days.toString(), + 'rated': rated.toString(), + if (variant != null) 'variant': variant!.name, + if (ratingRange != null) 'ratingRange': '${ratingRange!.$1}-${ratingRange!.$2}', + }; } diff --git a/lib/src/model/lobby/game_setup.dart b/lib/src/model/lobby/game_setup_preferences.dart similarity index 50% rename from lib/src/model/lobby/game_setup.dart rename to lib/src/model/lobby/game_setup_preferences.dart index 53e64062f0..ca9da1e218 100644 --- a/lib/src/model/lobby/game_setup.dart +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -1,41 +1,74 @@ -import 'dart:convert'; import 'dart:math' as math; - import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'game_setup.freezed.dart'; -part 'game_setup.g.dart'; +part 'game_setup_preferences.freezed.dart'; +part 'game_setup_preferences.g.dart'; + +@riverpod +class GameSetupPreferences extends _$GameSetupPreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.gameSetup; + + @override + GameSetupPrefs defaults({LightUser? user}) => GameSetupPrefs.defaults; + + @override + GameSetupPrefs fromJson(Map json) => GameSetupPrefs.fromJson(json); + + @override + GameSetupPrefs build() { + return fetch(); + } + + Future setQuickPairingTimeIncrement(TimeIncrement timeInc) { + return save(state.copyWith(quickPairingTimeIncrement: timeInc)); + } -enum PlayableSide { random, white, black } + Future setCustomTimeControl(TimeControl control) { + return save(state.copyWith(customTimeControl: control)); + } -String playableSideL10n(AppLocalizations l10n, PlayableSide side) { - switch (side) { - case PlayableSide.white: - return l10n.white; - case PlayableSide.black: - return l10n.black; - case PlayableSide.random: - return l10n.randomColor; + Future setCustomTimeSeconds(int seconds) { + return save(state.copyWith(customTimeSeconds: seconds)); + } + + Future setCustomIncrementSeconds(int seconds) { + return save(state.copyWith(customIncrementSeconds: seconds)); + } + + Future setCustomVariant(Variant variant) { + return save(state.copyWith(customVariant: variant)); + } + + Future setCustomRated(bool rated) { + return save(state.copyWith(customRated: rated)); + } + + Future setCustomRatingRange(int min, int max) { + return save(state.copyWith(customRatingDelta: (min, max))); + } + + Future setCustomDaysPerTurn(int days) { + return save(state.copyWith(customDaysPerTurn: days)); } } enum TimeControl { realTime, correspondence } -/// Saved custom game setup preferences. @Freezed(fromJson: true, toJson: true) -class GameSetup with _$GameSetup { - const GameSetup._(); +class GameSetupPrefs with _$GameSetupPrefs implements Serializable { + const GameSetupPrefs._(); - const factory GameSetup({ + const factory GameSetupPrefs({ required TimeIncrement quickPairingTimeIncrement, required TimeControl customTimeControl, required int customTimeSeconds, @@ -43,33 +76,24 @@ class GameSetup with _$GameSetup { required int customDaysPerTurn, required Variant customVariant, required bool customRated, - required PlayableSide customSide, required (int, int) customRatingDelta, - }) = _GameSetup; + }) = _GameSetupPrefs; - static const defaults = GameSetup( + static const defaults = GameSetupPrefs( quickPairingTimeIncrement: TimeIncrement(600, 0), customTimeControl: TimeControl.realTime, customTimeSeconds: 180, customIncrementSeconds: 0, customVariant: Variant.standard, customRated: false, - customSide: PlayableSide.random, customRatingDelta: (-500, 500), customDaysPerTurn: 3, ); - Speed get speedFromCustom => Speed.fromTimeIncrement( - TimeIncrement( - customTimeSeconds, - customIncrementSeconds, - ), - ); + Speed get speedFromCustom => + Speed.fromTimeIncrement(TimeIncrement(customTimeSeconds, customIncrementSeconds)); - Perf get perfFromCustom => Perf.fromVariantAndSpeed( - customVariant, - speedFromCustom, - ); + Perf get perfFromCustom => Perf.fromVariantAndSpeed(customVariant, speedFromCustom); /// Returns the rating range for the custom setup, or null if the user /// doesn't have a rating for the custom setup perf. @@ -82,106 +106,18 @@ class GameSetup with _$GameSetup { return (min, max); } - factory GameSetup.fromJson(Map json) { + factory GameSetupPrefs.fromJson(Map json) { try { - return _$GameSetupFromJson(json); + return _$GameSetupPrefsFromJson(json); } catch (_) { return defaults; } } } -@Riverpod(keepAlive: true) -class GameSetupPreferences extends _$GameSetupPreferences { - static String _prefKey(AuthSessionState? session) => - 'preferences.game_setup.${session?.user.id ?? '**anon**'}'; +const kSubtractingRatingRange = [-500, -450, -400, -350, -300, -250, -200, -150, -100, -50, 0]; - @override - GameSetup build() { - final session = ref.watch(authSessionProvider); - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey(session)); - return stored != null - ? GameSetup.fromJson( - jsonDecode(stored) as Map, - ) - : GameSetup.defaults; - } - - Future setQuickPairingTimeIncrement(TimeIncrement timeInc) { - return _save(state.copyWith(quickPairingTimeIncrement: timeInc)); - } - - Future setCustomTimeControl(TimeControl control) { - return _save(state.copyWith(customTimeControl: control)); - } - - Future setCustomTimeSeconds(int seconds) { - return _save(state.copyWith(customTimeSeconds: seconds)); - } - - Future setCustomIncrementSeconds(int seconds) { - return _save(state.copyWith(customIncrementSeconds: seconds)); - } - - Future setCustomVariant(Variant variant) { - return _save(state.copyWith(customVariant: variant)); - } - - Future setCustomRated(bool rated) { - return _save(state.copyWith(customRated: rated)); - } - - Future setCustomSide(PlayableSide side) { - return _save(state.copyWith(customSide: side)); - } - - Future setCustomRatingRange(int min, int max) { - return _save(state.copyWith(customRatingDelta: (min, max))); - } - - Future setCustomDaysPerTurn(int days) { - return _save(state.copyWith(customDaysPerTurn: days)); - } - - Future _save(GameSetup newState) async { - final prefs = ref.read(sharedPreferencesProvider); - final session = ref.read(authSessionProvider); - await prefs.setString( - _prefKey(session), - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -const kSubtractingRatingRange = [ - -500, - -450, - -400, - -350, - -300, - -250, - -200, - -150, - -100, - -50, - 0, -]; - -const kAddingRatingRange = [ - 0, - 50, - 100, - 150, - 200, - 250, - 300, - 350, - 400, - 450, - 500, -]; +const kAddingRatingRange = [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500]; const kAvailableTimesInSeconds = [ 0, diff --git a/lib/src/model/lobby/lobby_numbers.dart b/lib/src/model/lobby/lobby_numbers.dart index db7131ef96..88b1fea700 100644 --- a/lib/src/model/lobby/lobby_numbers.dart +++ b/lib/src/model/lobby/lobby_numbers.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'lobby_numbers.g.dart'; @@ -21,10 +22,7 @@ class LobbyNumbers extends _$LobbyNumbers { _socketSubscription = socketGlobalStream.listen((event) { if (event.topic == 'n') { final data = event.data as Map; - state = ( - nbPlayers: data['nbPlayers']!, - nbGames: data['nbGames']!, - ); + state = (nbPlayers: data['nbPlayers']!, nbGames: data['nbGames']!); } }); diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index 38cad7d6e2..4565a91b72 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -1,10 +1,11 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'correspondence_challenge.dart'; @@ -13,12 +14,8 @@ import 'game_seek.dart'; part 'lobby_repository.g.dart'; @riverpod -Future> correspondenceChallenges( - CorrespondenceChallengesRef ref, -) { - return ref.withClient( - (client) => LobbyRepository(client).getCorrespondenceChallenges(), - ); +Future> correspondenceChallenges(Ref ref) { + return ref.withClient((client) => LobbyRepository(client).getCorrespondenceChallenges()); } class LobbyRepository { @@ -30,10 +27,7 @@ class LobbyRepository { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); final response = await client.post(uri, body: seek.requestBody); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to create seek: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to create seek: ${response.statusCode}', uri); } } @@ -41,10 +35,7 @@ class LobbyRepository { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); final response = await client.delete(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to cancel seek: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to cancel seek: ${response.statusCode}', uri); } } diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart new file mode 100644 index 0000000000..01f7119b51 --- /dev/null +++ b/lib/src/model/notifications/notification_service.dart @@ -0,0 +1,338 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/localizations.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; +import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/badge_service.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'notification_service.g.dart'; + +final _logger = Logger('NotificationService'); + +/// A provider instance of the [FlutterLocalNotificationsPlugin]. +@Riverpod(keepAlive: true) +FlutterLocalNotificationsPlugin notificationDisplay(Ref _) => FlutterLocalNotificationsPlugin(); + +/// A provider instance of the [NotificationService]. +@Riverpod(keepAlive: true) +NotificationService notificationService(Ref ref) { + final service = NotificationService(ref); + + ref.onDispose(() => service._dispose()); + + return service; +} + +/// A service that manages notifications. +/// +/// This service is responsible for handling incoming messages from the Firebase +/// Cloud Messaging service and showing notifications. +/// +/// It also listens for notification interaction responses and dispatches them to the +/// appropriate services. +class NotificationService { + NotificationService(this._ref); + + final Ref _ref; + + /// The Firebase Cloud Messaging token refresh subscription. + StreamSubscription? _fcmTokenRefreshSubscription; + + /// The connectivity changes stream subscription. + ProviderSubscription>? _connectivitySubscription; + + /// The stream controller for notification responses. + static final StreamController _responseStreamController = + StreamController.broadcast(); + + /// The stream subscription for notification responses. + StreamSubscription? _responseStreamSubscription; + + /// Whether the device has been registered for push notifications. + bool _registeredDevice = false; + + AppLocalizations get _l10n => _ref.read(localizationsProvider).strings; + + FlutterLocalNotificationsPlugin get _notificationDisplay => + _ref.read(notificationDisplayProvider); + + /// Starts the notification service. + /// + /// This method listens for incoming messages and updates the application state + /// accordingly. + /// It also registers the device for push notifications once the app is online. + /// + /// This method should be called once the app is ready to receive notifications, + /// and after [LichessBinding.initializeNotifications] has been called. + Future start() async { + // listen for connectivity changes to register device once the app is online + _connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async { + if (current.value?.isOnline == true && !_registeredDevice) { + try { + await registerDevice(); + _registeredDevice = true; + } catch (e, st) { + _logger.severe('Could not setup push notifications; $e\n$st'); + } + } + }); + + // Listen for incoming messages while the app is in the foreground. + LichessBinding.instance.firebaseMessagingOnMessage.listen((RemoteMessage message) { + _processFcmMessage(message, fromBackground: false); + }); + + // Listen for incoming messages while the app is in the background. + LichessBinding.instance.firebaseMessagingOnBackgroundMessage( + _firebaseMessagingBackgroundHandler, + ); + + // Request permission to receive notifications. Pop-up will appear only + // once. + await LichessBinding.instance.firebaseMessaging.requestPermission( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + ); + + // Listen for token refresh and update the token on the server accordingly. + _fcmTokenRefreshSubscription = LichessBinding.instance.firebaseMessaging.onTokenRefresh.listen(( + String token, + ) { + _registerToken(token); + }); + + // Get any messages which caused the application to open from + // a terminated state. + final RemoteMessage? initialMessage = + await LichessBinding.instance.firebaseMessaging.getInitialMessage(); + + if (initialMessage != null) { + _handleFcmMessageOpenedApp(initialMessage); + } + + // Handle any other interaction that caused the app to open when in background. + LichessBinding.instance.firebaseMessagingOnMessageOpenedApp.listen(_handleFcmMessageOpenedApp); + + // start listening for notification responses + _responseStreamSubscription = _responseStreamController.stream.listen( + _dispatchNotificationResponse, + ); + } + + /// Shows a notification. + Future show(LocalNotification notification) async { + final id = notification.id; + final payload = jsonEncode(notification.payload); + + await _notificationDisplay.show( + id, + notification.title(_l10n), + notification.body(_l10n), + notification.details(_l10n), + payload: payload, + ); + _logger.info( + 'Show local notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', + ); + + return id; + } + + /// Cancels/removes a notification. + Future cancel(int id) async { + _logger.info('canceled notification id: [$id]'); + return _notificationDisplay.cancel(id); + } + + void _dispose() { + _fcmTokenRefreshSubscription?.cancel(); + _connectivitySubscription?.close(); + _responseStreamSubscription?.cancel(); + } + + /// Dispatch a notification response to the appropriate service according to the notification type. + void _dispatchNotificationResponse(NotificationResponse response) { + final rawPayload = response.payload; + + if (rawPayload == null) return; + + final json = jsonDecode(rawPayload) as Map; + final notification = LocalNotification.fromJson(json); + + switch (notification) { + case CorresGameUpdateNotification(fullId: final gameFullId): + _ref.read(correspondenceServiceProvider).onNotificationResponse(gameFullId); + case ChallengeNotification(challenge: final challenge): + _ref.read(challengeServiceProvider).onNotificationResponse(response.actionId, challenge); + } + } + + /// Function called by the notification plugin when a notification has been tapped on. + static void onDidReceiveNotificationResponse(NotificationResponse response) { + _logger.fine('received local notification ${response.id} response in foreground.'); + + _responseStreamController.add(response); + } + + /// Handle an FCM message that caused the application to open + void _handleFcmMessageOpenedApp(RemoteMessage message) { + final parsedMessage = FcmMessage.fromRemoteMessage(message); + + switch (parsedMessage) { + case CorresGameUpdateFcmMessage(fullId: final fullId): + _ref.read(correspondenceServiceProvider).onNotificationResponse(fullId); + + // TODO: handle other notification types + case UnhandledFcmMessage(data: final data): + _logger.warning('Received unhandled FCM notification type: ${data['lichess.type']}'); + + case MalformedFcmMessage(data: final data): + _logger.severe('Received malformed FCM message: $data'); + } + } + + /// Process a message received from the Firebase Cloud Messaging service. + /// + /// If the message contains a [RemoteMessage.notification] field and if it is + /// received while the app was in foreground, the notification is by default not + /// shown to the user. + /// Depending on the message type, we may as well show a local notification. + /// + /// Some messages (whether or not they have an associated notification), have + /// a [RemoteMessage.data] field used to update the application state according + /// to the message type. + /// + /// A special data field, 'lichess.iosBadge', is used to update the iOS app's + /// badge count according to the value held by the server. + Future _processFcmMessage( + RemoteMessage message, { + + /// Whether the message was received while the app was in the background. + required bool fromBackground, + }) async { + _logger.fine( + 'Processing a FCM message from ${fromBackground ? 'background' : 'foreground'}: ${message.data}', + ); + + final parsedMessage = FcmMessage.fromRemoteMessage(message); + + switch (parsedMessage) { + case CorresGameUpdateFcmMessage( + fullId: final fullId, + game: final game, + notification: final notification, + ): + if (game != null) { + await _ref + .read(correspondenceServiceProvider) + .onServerUpdateEvent(fullId, game, fromBackground: fromBackground); + } + + if (fromBackground == false && notification != null) { + await show(CorresGameUpdateNotification(fullId, notification.title!, notification.body!)); + } + + // TODO: handle other notification types + + case UnhandledFcmMessage(data: final data): + _logger.warning('Received unhandled FCM notification type: ${data['lichess.type']}'); + + case MalformedFcmMessage(data: final data): + _logger.severe('Received malformed FCM message: $data'); + } + + // update badge + final badge = message.data['lichess.iosBadge'] as String?; + if (badge != null) { + try { + await BadgeService.instance.setBadge(int.parse(badge)); + } catch (e) { + _logger.severe('Could not parse badge: $badge'); + } + } + } + + /// Register the device for push notifications. + Future registerDevice() async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); + if (apnsToken == null) { + _logger.warning('APNS token is null'); + return; + } + } + final token = await LichessBinding.instance.firebaseMessaging.getToken(); + if (token != null) { + await _registerToken(token); + } + } + + /// Unregister the device from push notifications. + Future unregister() async { + _logger.info('will unregister'); + final session = _ref.read(authSessionProvider); + if (session == null) { + return; + } + try { + await _ref.withClient((client) => client.post(Uri(path: '/mobile/unregister'))); + } catch (e, st) { + _logger.severe('could not unregister device; $e', e, st); + } + } + + Future _registerToken(String token) async { + final settings = await LichessBinding.instance.firebaseMessaging.getNotificationSettings(); + if (settings.authorizationStatus == AuthorizationStatus.denied) { + return; + } + _logger.info('will register fcmToken: $token'); + final session = _ref.read(authSessionProvider); + if (session == null) { + return; + } + try { + await _ref.withClient((client) => client.post(Uri(path: '/mobile/register/firebase/$token'))); + } catch (e, st) { + _logger.severe('could not register device; $e', e, st); + } + } + + @pragma('vm:entry-point') + static Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // create a new provider scope for the background isolate + final ref = ProviderContainer(); + + final lichessBinding = AppLichessBinding.ensureInitialized(); + await lichessBinding.preloadSharedPreferences(); + await ref.read(preloadedDataProvider.future); + + try { + await ref.read(notificationServiceProvider)._processFcmMessage(message, fromBackground: true); + + ref.dispose(); + } catch (e) { + _logger.severe('Error when processing an FCM background message: $e'); + ref.dispose(); + } + } +} diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart new file mode 100644 index 0000000000..771887faa8 --- /dev/null +++ b/lib/src/model/notifications/notifications.dart @@ -0,0 +1,317 @@ +import 'dart:convert'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:meta/meta.dart'; + +/// FCM Messages +/////////////// + +/// Parsed data from an FCM message. +/// +/// Messages received from Firebase Cloud Messaging (FCM) service support different +/// kind of use cases, depending on the message content: +/// +/// * when no [RemoteMessage.notification] is present, the message is a data message, +/// and the application can handle it in the foreground or background; It typically +/// serves to update the application state silently. +/// +/// * when a [RemoteMessage.notification] is present, the message is a notification message. +/// If the application is in the background, the system displays the notification to the user. +/// If the application is in the foreground, the system does not display the notification +/// automatically, but we might want to display it ourselves. +/// A notification message can contain additional data in the [RemoteMessage.data] field, +/// which can also be used to update the application state. +@immutable +sealed class FcmMessage { + const FcmMessage(); + + RemoteNotification? get notification; + + factory FcmMessage.fromRemoteMessage(RemoteMessage message) { + final messageType = message.data['lichess.type'] as String?; + + if (messageType == null) { + return UnhandledFcmMessage(message.data); + } else { + switch (messageType) { + case 'corresAlarm': + case 'gameTakebackOffer': + case 'gameDrawOffer': + case 'gameMove': + case 'gameFinish': + final gameFullId = message.data['lichess.fullId'] as String?; + final round = message.data['lichess.round'] as String?; + if (gameFullId != null) { + final fullId = GameFullId(gameFullId); + final game = + round != null + ? PlayableGame.fromServerJson(jsonDecode(round) as Map) + : null; + return CorresGameUpdateFcmMessage( + fullId, + game: game, + notification: message.notification, + ); + } else { + return MalformedFcmMessage(message.data); + } + default: + return UnhandledFcmMessage(message.data); + } + } + } +} + +/// An [FcmMessage] that represents a correspondence game update. +@immutable +class CorresGameUpdateFcmMessage extends FcmMessage { + const CorresGameUpdateFcmMessage(this.fullId, {required this.game, required this.notification}); + + final GameFullId fullId; + final PlayableGame? game; + + @override + final RemoteNotification? notification; +} + +/// An [FcmMessage] that could not be parsed. +@immutable +class MalformedFcmMessage extends FcmMessage { + const MalformedFcmMessage(this.data); + + final Map data; + + @override + RemoteNotification? get notification => null; +} + +/// An [FcmMessage] that is not handled by the application. +@immutable +class UnhandledFcmMessage extends FcmMessage { + const UnhandledFcmMessage(this.data); + + final Map data; + + @override + RemoteNotification? get notification => null; +} + +/// Local Notifications +/////////////////////// + +/// A notification shown to the user from the platform's notification system. +@immutable +sealed class LocalNotification { + const LocalNotification(); + + /// The unique identifier of the notification. + int get id; + + /// The channel identifier of the notification. + /// + /// Corresponds to [AndroidNotificationDetails.channelId] for android and + /// [DarwinNotificationDetails.threadIdentifier] for iOS. + /// + /// It must match the channel identifier of the notification details. + String get channelId; + + /// The localized title of the notification. + String title(AppLocalizations l10n); + + /// The localized body of the notification. + String? body(AppLocalizations l10n); + + /// The payload of the notification. + /// + /// Implementations must not override this getter, but [_concretePayload] instead. + /// + /// See [LocalNotification.fromJson] where the [channelId] is used to determine the + /// concrete type of the notification, to be able to deserialize it. + Map get payload => {'channel': channelId, ..._concretePayload}; + + /// The actual payload of the notification. + /// + /// Will be merged with the channel:[channelId] entry to form the final payload. + Map get _concretePayload; + + /// The localized details of the notification for each platform. + NotificationDetails details(AppLocalizations l10n); + + /// Retrives a local notification from a JSON payload. + factory LocalNotification.fromJson(Map json) { + final channel = json['channel'] as String; + switch (channel) { + case 'corresGameUpdate': + return CorresGameUpdateNotification.fromJson(json); + case 'challenge': + return ChallengeNotification.fromJson(json); + default: + throw ArgumentError('Unknown notification channel: $channel'); + } + } +} + +/// A notification for a correspondence game update. +/// +/// This notification is shown when a correspondence game is updated on the server +/// and a FCM message is received and contains itself a notification. +/// +/// Fields [title] and [body] are dynamic and part of the payload because they +/// are generated server side and are included in the FCM message's [RemoteMessage.notification] field. +class CorresGameUpdateNotification extends LocalNotification { + const CorresGameUpdateNotification(this.fullId, String title, String body) + : _title = title, + _body = body; + + final GameFullId fullId; + + final String _title; + final String _body; + + factory CorresGameUpdateNotification.fromJson(Map json) { + final gameId = GameFullId.fromJson(json['fullId'] as String); + final title = json['title'] as String; + final body = json['body'] as String; + return CorresGameUpdateNotification(gameId, title, body); + } + + @override + String get channelId => 'corresGameUpdate'; + + @override + int get id => fullId.hashCode; + + @override + Map get _concretePayload => { + 'fullId': fullId.toJson(), + 'title': _title, + 'body': _body, + }; + + @override + String title(_) => _title; + + @override + String? body(_) => _body; + + @override + NotificationDetails details(AppLocalizations l10n) => NotificationDetails( + android: AndroidNotificationDetails( + channelId, + l10n.preferencesNotifyGameEvent, + importance: Importance.high, + priority: Priority.defaultPriority, + autoCancel: true, + ), + iOS: DarwinNotificationDetails(threadIdentifier: channelId), + ); +} + +/// A notification for a received challenge. +/// +/// This notification is shown when a challenge is received from the server through +/// the web socket. +class ChallengeNotification extends LocalNotification { + const ChallengeNotification(this.challenge); + + final Challenge challenge; + + factory ChallengeNotification.fromJson(Map json) { + final challenge = Challenge.fromJson(json['challenge'] as Map); + return ChallengeNotification(challenge); + } + + @override + String get channelId => 'challenge'; + + @override + int get id => challenge.id.value.hashCode; + + @override + Map get _concretePayload => {'challenge': challenge.toJson()}; + + @override + String title(AppLocalizations _) => '${challenge.challenger!.user.name} challenges you!'; + + @override + String body(AppLocalizations l10n) => challenge.description(l10n); + + @override + NotificationDetails details(AppLocalizations l10n) => NotificationDetails( + android: AndroidNotificationDetails( + channelId, + l10n.preferencesNotifyChallenge, + importance: Importance.max, + priority: Priority.high, + autoCancel: false, + actions: [ + if (challenge.variant.isPlaySupported) + AndroidNotificationAction( + 'accept', + l10n.accept, + icon: const DrawableResourceAndroidBitmap('tick'), + showsUserInterface: true, + contextual: true, + ), + AndroidNotificationAction( + 'decline', + l10n.decline, + icon: const DrawableResourceAndroidBitmap('cross'), + showsUserInterface: true, + contextual: true, + ), + ], + ), + iOS: DarwinNotificationDetails( + threadIdentifier: channelId, + categoryIdentifier: + challenge.variant.isPlaySupported + ? darwinPlayableVariantCategoryId + : darwinUnplayableVariantCategoryId, + ), + ); + + static const darwinPlayableVariantCategoryId = 'challenge-notification-playable-variant'; + + static const darwinUnplayableVariantCategoryId = 'challenge-notification-unplayable-variant'; + + static DarwinNotificationCategory darwinPlayableVariantCategory(AppLocalizations l10n) => + DarwinNotificationCategory( + darwinPlayableVariantCategoryId, + actions: [ + DarwinNotificationAction.plain( + 'accept', + l10n.accept, + options: {DarwinNotificationActionOption.foreground}, + ), + DarwinNotificationAction.plain( + 'decline', + l10n.decline, + options: {DarwinNotificationActionOption.foreground}, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); + + static DarwinNotificationCategory darwinUnplayableVariantCategory(AppLocalizations l10n) => + DarwinNotificationCategory( + darwinUnplayableVariantCategoryId, + actions: [ + DarwinNotificationAction.plain( + 'decline', + l10n.decline, + options: {DarwinNotificationActionOption.foreground}, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); +} diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart new file mode 100644 index 0000000000..4b62f41823 --- /dev/null +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -0,0 +1,107 @@ +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; + +part 'opening_explorer.freezed.dart'; +part 'opening_explorer.g.dart'; + +enum OpeningDatabase { master, lichess, player } + +@Freezed(fromJson: true) +class OpeningExplorerEntry with _$OpeningExplorerEntry { + const OpeningExplorerEntry._(); + + const factory OpeningExplorerEntry({ + required int white, + required int draws, + required int black, + required IList moves, + IList? topGames, + IList? recentGames, + LightOpening? opening, + int? queuePosition, + }) = _OpeningExplorerEntry; + + factory OpeningExplorerEntry.empty() => + const OpeningExplorerEntry(white: 0, draws: 0, black: 0, moves: IList.empty()); + + factory OpeningExplorerEntry.fromJson(Map json) => + _$OpeningExplorerEntryFromJson(json); +} + +@Freezed(fromJson: true) +class OpeningMove with _$OpeningMove { + const OpeningMove._(); + + const factory OpeningMove({ + required String uci, + required String san, + required int white, + required int draws, + required int black, + int? averageRating, + int? averageOpponentRating, + int? performance, + OpeningExplorerGame? game, + }) = _OpeningMove; + + factory OpeningMove.fromJson(Map json) => _$OpeningMoveFromJson(json); + + int get games { + return white + draws + black; + } +} + +@Freezed(fromJson: true) +class OpeningExplorerGame with _$OpeningExplorerGame { + factory OpeningExplorerGame({ + required GameId id, + required ({String name, int rating}) white, + required ({String name, int rating}) black, + String? uci, + String? winner, + Perf? speed, + GameMode? mode, + int? year, + String? month, + }) = _OpeningExplorerGame; + + factory OpeningExplorerGame.fromJson(Map json) => + OpeningExplorerGame.fromPick(pick(json).required()); + + factory OpeningExplorerGame.fromPick(RequiredPick pick) { + return OpeningExplorerGame( + id: pick('id').asGameIdOrThrow(), + white: pick('white').letOrThrow( + (pick) => (name: pick('name').asStringOrThrow(), rating: pick('rating').asIntOrThrow()), + ), + black: pick('black').letOrThrow( + (pick) => (name: pick('name').asStringOrThrow(), rating: pick('rating').asIntOrThrow()), + ), + uci: pick('uci').asStringOrNull(), + winner: pick('winner').asStringOrNull(), + speed: pick('speed').asPerfOrNull(), + mode: switch (pick('mode').value) { + 'casual' => GameMode.casual, + 'rated' => GameMode.rated, + _ => null, + }, + year: pick('year').asIntOrNull(), + month: pick('month').asStringOrNull(), + ); + } +} + +enum GameMode { casual, rated } + +@freezed +class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { + const factory OpeningExplorerCacheKey({ + required String fen, + required OpeningExplorerPrefs prefs, + }) = _OpeningExplorerCacheKey; +} diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart new file mode 100644 index 0000000000..f3dcbc1188 --- /dev/null +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -0,0 +1,215 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'opening_explorer_preferences.freezed.dart'; +part 'opening_explorer_preferences.g.dart'; + +@riverpod +class OpeningExplorerPreferences extends _$OpeningExplorerPreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.openingExplorer; + + @override + OpeningExplorerPrefs defaults({LightUser? user}) => OpeningExplorerPrefs.defaults(user: user); + + @override + OpeningExplorerPrefs fromJson(Map json) => OpeningExplorerPrefs.fromJson(json); + + @override + OpeningExplorerPrefs build() { + return fetch(); + } + + Future setDatabase(OpeningDatabase db) => save(state.copyWith(db: db)); + + Future setMasterDbSince(int year) => + save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); + + Future toggleLichessDbSpeed(Speed speed) => save( + state.copyWith( + lichessDb: state.lichessDb.copyWith( + speeds: + state.lichessDb.speeds.contains(speed) + ? state.lichessDb.speeds.remove(speed) + : state.lichessDb.speeds.add(speed), + ), + ), + ); + + Future toggleLichessDbRating(int rating) => save( + state.copyWith( + lichessDb: state.lichessDb.copyWith( + ratings: + state.lichessDb.ratings.contains(rating) + ? state.lichessDb.ratings.remove(rating) + : state.lichessDb.ratings.add(rating), + ), + ), + ); + + Future setLichessDbSince(DateTime since) => + save(state.copyWith(lichessDb: state.lichessDb.copyWith(since: since))); + + Future setPlayerDbUsernameOrId(String username) => + save(state.copyWith(playerDb: state.playerDb.copyWith(username: username))); + + Future setPlayerDbSide(Side side) => + save(state.copyWith(playerDb: state.playerDb.copyWith(side: side))); + + Future togglePlayerDbSpeed(Speed speed) => save( + state.copyWith( + playerDb: state.playerDb.copyWith( + speeds: + state.playerDb.speeds.contains(speed) + ? state.playerDb.speeds.remove(speed) + : state.playerDb.speeds.add(speed), + ), + ), + ); + + Future togglePlayerDbGameMode(GameMode gameMode) => save( + state.copyWith( + playerDb: state.playerDb.copyWith( + gameModes: + state.playerDb.gameModes.contains(gameMode) + ? state.playerDb.gameModes.remove(gameMode) + : state.playerDb.gameModes.add(gameMode), + ), + ), + ); + + Future setPlayerDbSince(DateTime since) => + save(state.copyWith(playerDb: state.playerDb.copyWith(since: since))); +} + +@Freezed(fromJson: true, toJson: true) +class OpeningExplorerPrefs with _$OpeningExplorerPrefs implements Serializable { + const OpeningExplorerPrefs._(); + + const factory OpeningExplorerPrefs({ + required OpeningDatabase db, + required MasterDb masterDb, + required LichessDb lichessDb, + required PlayerDb playerDb, + }) = _OpeningExplorerPrefs; + + factory OpeningExplorerPrefs.defaults({LightUser? user}) => OpeningExplorerPrefs( + db: OpeningDatabase.master, + masterDb: MasterDb.defaults, + lichessDb: LichessDb.defaults, + playerDb: PlayerDb.defaults(user: user), + ); + + factory OpeningExplorerPrefs.fromJson(Map json) { + return _$OpeningExplorerPrefsFromJson(json); + } +} + +@Freezed(fromJson: true, toJson: true) +class MasterDb with _$MasterDb { + const MasterDb._(); + + const factory MasterDb({required int sinceYear}) = _MasterDb; + + static const kEarliestYear = 1952; + static final now = DateTime.now(); + static final datesMap = { + 'Last 3 years': now.year - 3, + 'Last 10 years': now.year - 10, + 'Last 20 years': now.year - 20, + 'All time': kEarliestYear, + }; + static const defaults = MasterDb(sinceYear: kEarliestYear); + + factory MasterDb.fromJson(Map json) { + return _$MasterDbFromJson(json); + } +} + +@Freezed(fromJson: true, toJson: true) +class LichessDb with _$LichessDb { + const LichessDb._(); + + const factory LichessDb({ + required ISet speeds, + required ISet ratings, + required DateTime since, + }) = _LichessDb; + + static const kAvailableSpeeds = ISetConst({ + Speed.ultraBullet, + Speed.bullet, + Speed.blitz, + Speed.rapid, + Speed.classical, + Speed.correspondence, + }); + static const kAvailableRatings = ISetConst({400, 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2500}); + static final earliestDate = DateTime.utc(2012, 12); + static final now = DateTime.now(); + static const kDaysInAYear = 365; + static final datesMap = { + 'Last year': now.subtract(const Duration(days: kDaysInAYear)), + 'Last 5 years': now.subtract(const Duration(days: kDaysInAYear * 5)), + 'All time': earliestDate, + }; + static final defaults = LichessDb( + speeds: kAvailableSpeeds.remove(Speed.ultraBullet), + ratings: kAvailableRatings.remove(400), + since: earliestDate, + ); + + factory LichessDb.fromJson(Map json) { + return _$LichessDbFromJson(json); + } +} + +@Freezed(fromJson: true, toJson: true) +class PlayerDb with _$PlayerDb { + const PlayerDb._(); + + const factory PlayerDb({ + String? username, + required Side side, + required ISet speeds, + required ISet gameModes, + required DateTime since, + }) = _PlayerDb; + + static const kAvailableSpeeds = ISetConst({ + Speed.ultraBullet, + Speed.bullet, + Speed.blitz, + Speed.rapid, + Speed.classical, + Speed.correspondence, + }); + static final earliestDate = DateTime.utc(2012, 12); + static final now = DateTime.now(); + static final datesMap = { + 'This month': now, + 'Last month': now.subtract(const Duration(days: 32)), + 'Last 6 months': now.subtract(const Duration(days: 183)), + 'Last year': now.subtract(const Duration(days: 365)), + 'All time': earliestDate, + }; + factory PlayerDb.defaults({LightUser? user}) => PlayerDb( + username: user?.name, + side: Side.white, + speeds: kAvailableSpeeds, + gameModes: GameMode.values.toISet(), + since: earliestDate, + ); + + factory PlayerDb.fromJson(Map json) { + return _$PlayerDbFromJson(json); + } +} diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart new file mode 100644 index 0000000000..3a78c779ac --- /dev/null +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:http/http.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'opening_explorer_repository.g.dart'; + +@riverpod +class OpeningExplorer extends _$OpeningExplorer { + StreamSubscription? _openingExplorerSubscription; + + @override + Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({required String fen}) async { + await ref.debounce(const Duration(milliseconds: 300)); + ref.onDispose(() { + _openingExplorerSubscription?.cancel(); + }); + + final prefs = ref.watch(openingExplorerPreferencesProvider); + final client = ref.read(defaultClientProvider); + switch (prefs.db) { + case OpeningDatabase.master: + final openingExplorer = await OpeningExplorerRepository( + client, + ).getMasterDatabase(fen, since: prefs.masterDb.sinceYear); + return (entry: openingExplorer, isIndexing: false); + case OpeningDatabase.lichess: + final openingExplorer = await OpeningExplorerRepository(client).getLichessDatabase( + fen, + speeds: prefs.lichessDb.speeds, + ratings: prefs.lichessDb.ratings, + since: prefs.lichessDb.since, + ); + return (entry: openingExplorer, isIndexing: false); + case OpeningDatabase.player: + final openingExplorerStream = await OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.username!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + gameModes: prefs.playerDb.gameModes, + since: prefs.playerDb.since, + ); + + _openingExplorerSubscription = openingExplorerStream.listen( + (openingExplorer) => state = AsyncValue.data((entry: openingExplorer, isIndexing: true)), + onDone: + () => + state.value != null + ? state = AsyncValue.data((entry: state.value!.entry, isIndexing: false)) + : state = AsyncValue.error( + 'No opening explorer data returned for player ${prefs.playerDb.username}', + StackTrace.current, + ), + ); + return null; + } + } +} + +class OpeningExplorerRepository { + const OpeningExplorerRepository(this.client); + + final Client client; + + Future getMasterDatabase(String fen, {int? since}) { + return client.readJson( + Uri.https(kLichessOpeningExplorerHost, '/masters', { + 'fen': fen, + if (since != null) 'since': since.toString(), + }), + mapper: OpeningExplorerEntry.fromJson, + ); + } + + Future getLichessDatabase( + String fen, { + required ISet speeds, + required ISet ratings, + DateTime? since, + }) { + return client.readJson( + Uri.https(kLichessOpeningExplorerHost, '/lichess', { + 'fen': fen, + if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), + if (ratings.isNotEmpty) 'ratings': ratings.join(','), + if (since != null) 'since': '${since.year}-${since.month}', + }), + mapper: OpeningExplorerEntry.fromJson, + ); + } + + Future> getPlayerDatabase( + String fen, { + required String usernameOrId, + required Side color, + required ISet speeds, + required ISet gameModes, + DateTime? since, + }) { + return client.readNdJsonStream( + Uri.https(kLichessOpeningExplorerHost, '/player', { + 'fen': fen, + 'player': usernameOrId, + 'color': color.name, + if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), + if (gameModes.isNotEmpty) 'modes': gameModes.map((gameMode) => gameMode.name).join(','), + if (since != null) 'since': '${since.year}-${since.month}', + }), + mapper: OpeningExplorerEntry.fromJson, + ); + } +} diff --git a/lib/src/model/over_the_board/over_the_board_clock.dart b/lib/src/model/over_the_board/over_the_board_clock.dart new file mode 100644 index 0000000000..390aee8e86 --- /dev/null +++ b/lib/src/model/over_the_board/over_the_board_clock.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_clock.freezed.dart'; +part 'over_the_board_clock.g.dart'; + +@riverpod +class OverTheBoardClock extends _$OverTheBoardClock { + final Stopwatch _stopwatch = Stopwatch(); + + late Timer _updateTimer; + + @override + OverTheBoardClockState build() { + _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + if (_stopwatch.isRunning) { + final newTime = state.timeLeft(state.activeClock!)! - _stopwatch.elapsed; + + if (state.activeClock == Side.white) { + state = state.copyWith(whiteTimeLeft: newTime); + } else { + state = state.copyWith(blackTimeLeft: newTime); + } + + if (newTime <= Duration.zero) { + state = state.copyWith(flagSide: state.activeClock); + } + + _stopwatch.reset(); + } + }); + + ref.onDispose(() { + _updateTimer.cancel(); + }); + + return OverTheBoardClockState.fromTimeIncrement( + TimeIncrement(const Duration(minutes: 5).inSeconds, const Duration(seconds: 3).inSeconds), + ); + } + + void setupClock(TimeIncrement timeIncrement) { + _stopwatch.stop(); + _stopwatch.reset(); + + state = OverTheBoardClockState.fromTimeIncrement(timeIncrement); + } + + void restart() { + setupClock(state.timeIncrement); + } + + void switchSide({required Side newSideToMove, required bool addIncrement}) { + if (state.timeIncrement.isInfinite || state.flagSide != null) return; + + final increment = Duration(seconds: addIncrement ? state.timeIncrement.increment : 0); + if (newSideToMove == Side.black) { + state = state.copyWith( + whiteTimeLeft: state.whiteTimeLeft! + increment, + activeClock: Side.black, + ); + } else { + state = state.copyWith( + blackTimeLeft: state.blackTimeLeft! + increment, + activeClock: Side.white, + ); + } + + _stopwatch.reset(); + _stopwatch.start(); + } + + void onMove({required Side newSideToMove}) { + switchSide(newSideToMove: newSideToMove, addIncrement: state.active); + } + + void pause() { + if (_stopwatch.isRunning) { + state = state.copyWith(activeClock: null); + _stopwatch.reset(); + _stopwatch.stop(); + } + } + + void resume(Side newSideToMove) { + _stopwatch.reset(); + _stopwatch.start(); + + state = state.copyWith(activeClock: newSideToMove); + } +} + +@freezed +class OverTheBoardClockState with _$OverTheBoardClockState { + const OverTheBoardClockState._(); + + const factory OverTheBoardClockState({ + required TimeIncrement timeIncrement, + required Duration? whiteTimeLeft, + required Duration? blackTimeLeft, + required Side? activeClock, + required Side? flagSide, + }) = _OverTheBoardClockState; + + factory OverTheBoardClockState.fromTimeIncrement(TimeIncrement timeIncrement) { + final initialTime = + timeIncrement.isInfinite + ? null + : Duration(seconds: max(timeIncrement.time, timeIncrement.increment)); + + return OverTheBoardClockState( + timeIncrement: timeIncrement, + whiteTimeLeft: initialTime, + blackTimeLeft: initialTime, + activeClock: null, + flagSide: null, + ); + } + + bool get active => activeClock != null || flagSide != null; + + Duration? timeLeft(Side side) => side == Side.white ? whiteTimeLeft : blackTimeLeft; +} diff --git a/lib/src/model/over_the_board/over_the_board_game_controller.dart b/lib/src/model/over_the_board/over_the_board_game_controller.dart new file mode 100644 index 0000000000..5f4e75c7ff --- /dev/null +++ b/lib/src/model/over_the_board/over_the_board_game_controller.dart @@ -0,0 +1,159 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/chess960.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/material_diff.dart'; +import 'package:lichess_mobile/src/model/game/over_the_board_game.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_game_controller.freezed.dart'; +part 'over_the_board_game_controller.g.dart'; + +@riverpod +class OverTheBoardGameController extends _$OverTheBoardGameController { + @override + OverTheBoardGameState build() => OverTheBoardGameState.fromVariant( + Variant.standard, + Speed.fromTimeIncrement(const TimeIncrement(0, 0)), + ); + + void startNewGame(Variant variant, TimeIncrement timeIncrement) { + state = OverTheBoardGameState.fromVariant(variant, Speed.fromTimeIncrement(timeIncrement)); + } + + void rematch() { + state = OverTheBoardGameState.fromVariant(state.game.meta.variant, state.game.meta.speed); + } + + void makeMove(NormalMove move) { + if (isPromotionPawnMove(state.currentPosition, move)) { + state = state.copyWith(promotionMove: move); + return; + } + + final (newPos, newSan) = state.currentPosition.makeSan(Move.parse(move.uci)!); + final sanMove = SanMove(newSan, move); + final newStep = GameStep( + position: newPos, + sanMove: sanMove, + diff: MaterialDiff.fromBoard(newPos.board), + ); + + // In an over-the-board game, we support "implicit takebacks": + // When going back one or more steps (i.e. stepCursor < game.steps.length - 1), + // a new move can be made, removing all steps after the current stepCursor. + state = state.copyWith( + game: state.game.copyWith( + steps: state.game.steps + .removeRange(state.stepCursor + 1, state.game.steps.length) + .add(newStep), + ), + stepCursor: state.stepCursor + 1, + ); + + if (state.currentPosition.isCheckmate) { + state = state.copyWith( + game: state.game.copyWith(status: GameStatus.mate, winner: state.turn.opposite), + ); + } else if (state.currentPosition.isStalemate) { + state = state.copyWith(game: state.game.copyWith(status: GameStatus.stalemate)); + } + + _moveFeedback(sanMove); + } + + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + makeMove(move); + state = state.copyWith(promotionMove: null); + } + } + + void onFlag(Side side) { + state = state.copyWith( + game: state.game.copyWith(status: GameStatus.outoftime, winner: side.opposite), + ); + } + + void goForward() { + if (state.canGoForward) { + state = state.copyWith(stepCursor: state.stepCursor + 1); + } + } + + void goBack() { + if (state.canGoBack) { + state = state.copyWith(stepCursor: state.stepCursor - 1); + } + } + + void _moveFeedback(SanMove sanMove) { + final isCheck = sanMove.san.contains('+'); + if (sanMove.san.contains('x')) { + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } +} + +@freezed +class OverTheBoardGameState with _$OverTheBoardGameState { + const OverTheBoardGameState._(); + + const factory OverTheBoardGameState({ + required OverTheBoardGame game, + @Default(0) int stepCursor, + @Default(null) NormalMove? promotionMove, + }) = _OverTheBoardGameState; + + factory OverTheBoardGameState.fromVariant(Variant variant, Speed speed) { + final position = + variant == Variant.chess960 ? randomChess960Position() : variant.initialPosition; + return OverTheBoardGameState( + game: OverTheBoardGame( + steps: [GameStep(position: position)].lock, + status: GameStatus.started, + initialFen: position.fen, + meta: GameMeta( + createdAt: DateTime.now(), + rated: false, + variant: variant, + speed: speed, + perf: Perf.fromVariantAndSpeed(variant, speed), + ), + ), + ); + } + + Position get currentPosition => game.stepAt(stepCursor).position; + Side get turn => currentPosition.turn; + bool get finished => game.finished; + NormalMove? get lastMove => + stepCursor > 0 ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) : null; + + IMap> get legalMoves => + makeLegalMoves(currentPosition, isChess960: game.meta.variant == Variant.chess960); + + MaterialDiffSide? currentMaterialDiff(Side side) { + return game.steps[stepCursor].diff?.bySide(side); + } + + List get moves => game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false); + + bool get canGoForward => stepCursor < game.steps.length - 1; + bool get canGoBack => stepCursor > 0; +} diff --git a/lib/src/model/puzzle/puzzle.dart b/lib/src/model/puzzle/puzzle.dart index 4ca49d5e51..8b8562e0fa 100644 --- a/lib/src/model/puzzle/puzzle.dart +++ b/lib/src/model/puzzle/puzzle.dart @@ -32,8 +32,7 @@ class Puzzle with _$Puzzle { if (isCheckmate) { return true; } - if (uci != solutionUci && - (!altCastles.containsKey(uci) || altCastles[uci] != solutionUci)) { + if (uci != solutionUci && (!altCastles.containsKey(uci) || altCastles[uci] != solutionUci)) { return false; } } @@ -54,8 +53,9 @@ class PuzzleData with _$PuzzleData { required ISet themes, }) = _PuzzleData; - factory PuzzleData.fromJson(Map json) => - _$PuzzleDataFromJson(json); + Side get sideToMove => initialPly.isEven ? Side.black : Side.white; + + factory PuzzleData.fromJson(Map json) => _$PuzzleDataFromJson(json); } @Freezed(fromJson: true, toJson: true) @@ -68,17 +68,13 @@ class PuzzleGlicko with _$PuzzleGlicko { bool? provisional, }) = _PuzzleGlicko; - factory PuzzleGlicko.fromJson(Map json) => - _$PuzzleGlickoFromJson(json); + factory PuzzleGlicko.fromJson(Map json) => _$PuzzleGlickoFromJson(json); } @freezed class PuzzleRound with _$PuzzleRound { - const factory PuzzleRound({ - required PuzzleId id, - required int ratingDiff, - required bool win, - }) = _PuzzleRound; + const factory PuzzleRound({required PuzzleId id, required int ratingDiff, required bool win}) = + _PuzzleRound; } @Freezed(fromJson: true, toJson: true) @@ -92,32 +88,23 @@ class PuzzleGame with _$PuzzleGame { required String pgn, }) = _PuzzleGame; - factory PuzzleGame.fromJson(Map json) => - _$PuzzleGameFromJson(json); + factory PuzzleGame.fromJson(Map json) => _$PuzzleGameFromJson(json); } @Freezed(fromJson: true, toJson: true) class PuzzleGamePlayer with _$PuzzleGamePlayer { - const factory PuzzleGamePlayer({ - required Side side, - required String name, - String? title, - }) = _PuzzleGamePlayer; - - factory PuzzleGamePlayer.fromJson(Map json) => - _$PuzzleGamePlayerFromJson(json); + const factory PuzzleGamePlayer({required Side side, required String name, String? title}) = + _PuzzleGamePlayer; + + factory PuzzleGamePlayer.fromJson(Map json) => _$PuzzleGamePlayerFromJson(json); } @Freezed(fromJson: true, toJson: true) class PuzzleSolution with _$PuzzleSolution { - const factory PuzzleSolution({ - required PuzzleId id, - required bool win, - required bool rated, - }) = _PuzzleSolution; + const factory PuzzleSolution({required PuzzleId id, required bool win, required bool rated}) = + _PuzzleSolution; - factory PuzzleSolution.fromJson(Map json) => - _$PuzzleSolutionFromJson(json); + factory PuzzleSolution.fromJson(Map json) => _$PuzzleSolutionFromJson(json); } @freezed @@ -150,12 +137,11 @@ class LitePuzzle with _$LitePuzzle { required int rating, }) = _LitePuzzle; - factory LitePuzzle.fromJson(Map json) => - _$LitePuzzleFromJson(json); + factory LitePuzzle.fromJson(Map json) => _$LitePuzzleFromJson(json); (Side, String, Move) get preview { final pos1 = Chess.fromSetup(Setup.parseFen(fen)); - final move = Move.fromUci(solution.first); + final move = Move.parse(solution.first); final pos = pos1.play(move!); return (pos.turn, pos.fen, move); } @@ -193,11 +179,7 @@ class PuzzleHistoryEntry with _$PuzzleHistoryEntry { Duration? solvingTime, }) = _PuzzleHistoryEntry; - factory PuzzleHistoryEntry.fromLitePuzzle( - LitePuzzle puzzle, - bool win, - Duration duration, - ) { + factory PuzzleHistoryEntry.fromLitePuzzle(LitePuzzle puzzle, bool win, Duration duration) { final (_, fen, move) = puzzle.preview; return PuzzleHistoryEntry( date: DateTime.now(), @@ -210,6 +192,5 @@ class PuzzleHistoryEntry with _$PuzzleHistoryEntry { ); } - (String, Side, Move) get preview => - (fen, Chess.fromSetup(Setup.parseFen(fen)).turn, lastMove); + (String, Side, Move) get preview => (fen, Chess.fromSetup(Setup.parseFen(fen)).turn, lastMove); } diff --git a/lib/src/model/puzzle/puzzle_activity.dart b/lib/src/model/puzzle/puzzle_activity.dart index 55a7a71cbf..2e4d9a7e25 100644 --- a/lib/src/model/puzzle/puzzle_activity.dart +++ b/lib/src/model/puzzle/puzzle_activity.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -23,7 +23,7 @@ class PuzzleActivity extends _$PuzzleActivity { @override Future build() async { - ref.cacheFor(const Duration(minutes: 30)); + ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { _list.clear(); }); @@ -45,9 +45,7 @@ class PuzzleActivity extends _$PuzzleActivity { ); } - Map> _groupByDay( - Iterable list, - ) { + Map> _groupByDay(Iterable list) { final map = >{}; for (final entry in list) { final date = DateTime(entry.date.year, entry.date.month, entry.date.day); @@ -64,19 +62,16 @@ class PuzzleActivity extends _$PuzzleActivity { if (!state.hasValue) return; final currentVal = state.requireValue; - if (_list.length < _maxPuzzles) { + if (currentVal.hasMore && _list.length < _maxPuzzles) { state = AsyncData(currentVal.copyWith(isLoading: true)); Result.capture( ref.withClient( - (client) => PuzzleRepository(client) - .puzzleActivity(_nbPerPage, before: _list.last.date), + (client) => PuzzleRepository(client).puzzleActivity(_nbPerPage, before: _list.last.date), ), ).fold( (value) { if (value.isEmpty) { - state = AsyncData( - currentVal.copyWith(hasMore: false, isLoading: false), - ); + state = AsyncData(currentVal.copyWith(hasMore: false, isLoading: false)); return; } _list.addAll(value); @@ -90,8 +85,7 @@ class PuzzleActivity extends _$PuzzleActivity { ); }, (error, stackTrace) { - state = - AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + state = AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); }, ); } diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index f110930790..15eb38c3b1 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -16,9 +17,9 @@ part 'puzzle_batch_storage.freezed.dart'; part 'puzzle_batch_storage.g.dart'; @Riverpod(keepAlive: true) -PuzzleBatchStorage puzzleBatchStorage(PuzzleBatchStorageRef ref) { - final database = ref.watch(databaseProvider); - return PuzzleBatchStorage(database); +Future puzzleBatchStorage(Ref ref) async { + final database = await ref.watch(databaseProvider.future); + return PuzzleBatchStorage(database, ref); } const _anonUserKey = '**anon**'; @@ -26,9 +27,10 @@ const _tableName = 'puzzle_batchs'; /// Local storage for puzzles. class PuzzleBatchStorage { - const PuzzleBatchStorage(this._db); + const PuzzleBatchStorage(this._db, this._ref); final Database _db; + final Ref _ref; Future fetch({ required UserId? userId, @@ -40,10 +42,7 @@ class PuzzleBatchStorage { userId = ? AND angle = ? ''', - whereArgs: [ - userId ?? _anonUserKey, - angle.key, - ], + whereArgs: [userId ?? _anonUserKey, angle.key], ); final raw = list.firstOrNull?['data'] as String?; @@ -65,15 +64,12 @@ class PuzzleBatchStorage { required PuzzleBatch data, PuzzleAngle angle = const PuzzleTheme(PuzzleThemeKey.mix), }) async { - await _db.insert( - _tableName, - { - 'userId': userId ?? _anonUserKey, - 'angle': angle.key, - 'data': jsonEncode(data.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await _db.insert(_tableName, { + 'userId': userId ?? _anonUserKey, + 'angle': angle.key, + 'data': jsonEncode(data.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + _ref.invalidateSelf(); } Future delete({ @@ -86,94 +82,112 @@ class PuzzleBatchStorage { userId = ? AND angle = ? ''', - whereArgs: [ - userId ?? _anonUserKey, - angle.key, - ], + whereArgs: [userId ?? _anonUserKey, angle.key], ); + _ref.invalidateSelf(); } - Future> fetchSavedThemes({ - required UserId? userId, - }) async { + /// Fetches all saved puzzles batches (except mix) for the given user. + Future> fetchAll({required UserId? userId}) async { final list = await _db.query( _tableName, where: 'userId = ?', - whereArgs: [ - userId ?? _anonUserKey, - ], + whereArgs: [userId ?? _anonUserKey], + orderBy: 'lastModified DESC', ); + return list + .map((entry) { + final angleStr = entry['angle'] as String?; + final raw = entry['data'] as String?; + + if (angleStr == null || raw == null) return null; + + final angle = PuzzleAngle.fromKey(angleStr); - return list.fold>( - IMap(const {}), - (acc, map) { - final angle = map['angle'] as String?; - final raw = map['data'] as String?; - - final theme = angle != null ? puzzleThemeNameMap.get(angle) : null; - - if (theme != null) { - int? count; - if (raw != null) { - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', - ); - } - final data = PuzzleBatch.fromJson(json); - count = data.unsolved.length; + if (angle == const PuzzleTheme(PuzzleThemeKey.mix)) return null; + + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); } - return count != null ? acc.add(theme, count) : acc; - } + final data = PuzzleBatch.fromJson(json); + final count = data.unsolved.length; + return (angle, count); + }) + .nonNulls + .toIList(); + } - return acc; - }, + Future> fetchSavedThemes({required UserId? userId}) async { + final list = await _db.query( + _tableName, + where: 'userId = ?', + whereArgs: [userId ?? _anonUserKey], ); + + return list.fold>(IMap(const {}), (acc, map) { + final angle = map['angle'] as String?; + final raw = map['data'] as String?; + + final theme = angle != null ? puzzleThemeNameMap.get(angle) : null; + + if (theme != null) { + int? count; + if (raw != null) { + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); + } + final data = PuzzleBatch.fromJson(json); + count = data.unsolved.length; + } + return count != null ? acc.add(theme, count) : acc; + } + + return acc; + }); } - Future> fetchSavedOpenings({ - required UserId? userId, - }) async { + Future> fetchSavedOpenings({required UserId? userId}) async { final list = await _db.query( _tableName, where: 'userId = ?', - whereArgs: [ - userId ?? _anonUserKey, - ], + whereArgs: [userId ?? _anonUserKey], ); - return list.fold>( - IMap(const {}), - (acc, map) { - final angle = map['angle'] as String?; - final raw = map['data'] as String?; + return list.fold>(IMap(const {}), (acc, map) { + final angle = map['angle'] as String?; + final raw = map['data'] as String?; - final openingKey = angle != null - ? switch (PuzzleAngle.fromKey(angle)) { + final openingKey = + angle != null + ? switch (PuzzleAngle.fromKey(angle)) { PuzzleTheme(themeKey: _) => null, PuzzleOpening(key: final key) => key, } - : null; - - if (openingKey != null) { - int? count; - if (raw != null) { - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', - ); - } - final data = PuzzleBatch.fromJson(json); - count = data.unsolved.length; + : null; + + if (openingKey != null) { + int? count; + if (raw != null) { + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); } - return count != null ? acc.add(openingKey, count) : acc; + final data = PuzzleBatch.fromJson(json); + count = data.unsolved.length; } + return count != null ? acc.add(openingKey, count) : acc; + } - return acc; - }, - ); + return acc; + }); } } @@ -184,6 +198,5 @@ class PuzzleBatch with _$PuzzleBatch { required IList unsolved, }) = _PuzzleBatch; - factory PuzzleBatch.fromJson(Map json) => - _$PuzzleBatchFromJson(json); + factory PuzzleBatch.fromJson(Map json) => _$PuzzleBatchFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index bc98fa010b..48c0f19a5c 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'package:async/async.dart'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -22,6 +21,8 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/model/puzzle/streak_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -41,14 +42,10 @@ class PuzzleController extends _$PuzzleController { final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 100)); - late final _service = ref.read(puzzleServiceFactoryProvider)( - queueLength: kPuzzleLocalQueueLength, - ); + Future get _service => + ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength); @override - PuzzleState build( - PuzzleContext initialContext, { - PuzzleStreak? initialStreak, - }) { + PuzzleState build(PuzzleContext initialContext, {PuzzleStreak? initialStreak}) { final evaluationService = ref.read(evaluationServiceProvider); ref.onDispose(() { @@ -59,16 +56,28 @@ class PuzzleController extends _$PuzzleController { evaluationService.disposeEngine(); }); + // we might not have the user rating yet so let's update it now + // then it will be updated on each puzzle completion + if (initialContext.userId != null) { + _updateUserRating(); + } + return _loadNewContext(initialContext, initialStreak); } - PuzzleRepository _repository(LichessClient client) => - PuzzleRepository(client); + PuzzleRepository _repository(LichessClient client) => PuzzleRepository(client); - PuzzleState _loadNewContext( - PuzzleContext context, - PuzzleStreak? streak, - ) { + Future _updateUserRating() async { + try { + final data = await ref.withClient((client) => _repository(client).selectBatch(nb: 0)); + final glicko = data.glicko; + if (glicko != null) { + state = state.copyWith(glicko: glicko); + } + } catch (_) {} + } + + PuzzleState _loadNewContext(PuzzleContext context, PuzzleStreak? streak) { final root = Root.fromPgnMoves(context.puzzle.game.pgn); _gameTree = root.nodeAt(root.mainlinePath.penultimate) as Branch; @@ -79,9 +88,7 @@ class PuzzleController extends _$PuzzleController { // enable solution button after 4 seconds _enableSolutionButtonTimer = Timer(const Duration(seconds: 4), () { - state = state.copyWith( - canViewSolution: true, - ); + state = state.copyWith(canViewSolution: true); }); final initialPath = UciPath.fromId(_gameTree.children.first.id); @@ -99,9 +106,7 @@ class PuzzleController extends _$PuzzleController { initialPath: initialPath, currentPath: UciPath.empty, node: _gameTree.view, - pov: _gameTree.nodeAt(initialPath).position.ply.isEven - ? Side.white - : Side.black, + pov: _gameTree.nodeAt(initialPath).position.ply.isEven ? Side.white : Side.black, canViewSolution: false, resultSent: false, isChangingDifficulty: false, @@ -113,24 +118,25 @@ class PuzzleController extends _$PuzzleController { ); } - Future onUserMove(Move move) async { + Future onUserMove(NormalMove move) async { + if (isPromotionPawnMove(state.position, move)) { + state = state.copyWith(promotionMove: move); + return; + } + _addMove(move); if (state.mode == PuzzleMode.play) { final nodeList = _gameTree.branchesOn(state.currentPath).toList(); - final movesToTest = - nodeList.sublist(state.initialPath.size).map((e) => e.sanMove); + final movesToTest = nodeList.sublist(state.initialPath.size).map((e) => e.sanMove); final isGoodMove = state.puzzle.testSolution(movesToTest); if (isGoodMove) { - state = state.copyWith( - feedback: PuzzleFeedback.good, - ); + state = state.copyWith(feedback: PuzzleFeedback.good); final isCheckmate = movesToTest.last.san.endsWith('#'); - final nextUci = - state.puzzle.puzzle.solution.getOrNull(movesToTest.length); + final nextUci = state.puzzle.puzzle.solution.getOrNull(movesToTest.length); // checkmate is always a win if (isCheckmate) { _completePuzzle(); @@ -138,16 +144,14 @@ class PuzzleController extends _$PuzzleController { // another puzzle move: let's continue else if (nextUci != null) { await Future.delayed(const Duration(milliseconds: 500)); - _addMove(Move.fromUci(nextUci)!); + _addMove(Move.parse(nextUci)!); } // no more puzzle move: it's a win else { _completePuzzle(); } } else { - state = state.copyWith( - feedback: PuzzleFeedback.bad, - ); + state = state.copyWith(feedback: PuzzleFeedback.bad); _onFailOrWin(PuzzleResult.lose); if (initialStreak == null) { await Future.delayed(const Duration(milliseconds: 500)); @@ -157,20 +161,28 @@ class PuzzleController extends _$PuzzleController { } } + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + onUserMove(move); + } + } + void userNext() { _viewSolutionTimer?.cancel(); _goToNextNode(replaying: true); - state = state.copyWith( - viewedSolutionRecently: false, - ); + state = state.copyWith(viewedSolutionRecently: false); } void userPrevious() { _viewSolutionTimer?.cancel(); _goToPreviousNode(replaying: true); - state = state.copyWith( - viewedSolutionRecently: false, - ); + state = state.copyWith(viewedSolutionRecently: false); } void viewSolution() { @@ -178,15 +190,11 @@ class PuzzleController extends _$PuzzleController { _mergeSolution(); - state = state.copyWith( - node: _gameTree.branchAt(state.currentPath).view, - ); + state = state.copyWith(node: _gameTree.branchAt(state.currentPath).view); _onFailOrWin(PuzzleResult.lose); - state = state.copyWith( - mode: PuzzleMode.view, - ); + state = state.copyWith(mode: PuzzleMode.view); Timer(const Duration(milliseconds: 800), () { _goToNextNode(); @@ -205,79 +213,63 @@ class PuzzleController extends _$PuzzleController { state = state.copyWith.streak!(hasSkipped: true); final moveIndex = state.currentPath.size - state.initialPath.size; final solution = state.puzzle.puzzle.solution[moveIndex]; - onUserMove(Move.fromUci(solution)!); + onUserMove(NormalMove.fromUci(solution)); } } Future changeDifficulty(PuzzleDifficulty difficulty) async { - state = state.copyWith( - isChangingDifficulty: true, - ); + state = state.copyWith(isChangingDifficulty: true); - await ref - .read( - puzzlePreferencesProvider(initialContext.userId).notifier, - ) - .setDifficulty(difficulty); + await ref.read(puzzlePreferencesProvider.notifier).setDifficulty(difficulty); - final nextPuzzle = _service.resetBatch( + final nextPuzzle = (await _service).resetBatch( userId: initialContext.userId, angle: initialContext.angle, ); - state = state.copyWith( - isChangingDifficulty: false, - ); + state = state.copyWith(isChangingDifficulty: false); return nextPuzzle; } - void loadPuzzle(PuzzleContext nextContext, {PuzzleStreak? nextStreak}) { + void onLoadPuzzle(PuzzleContext nextContext, {PuzzleStreak? nextStreak}) { ref.read(evaluationServiceProvider).disposeEngine(); state = _loadNewContext(nextContext, nextStreak ?? state.streak); + _saveCurrentStreakLocally(); + } + + void _saveCurrentStreakLocally() { + if (state.streak != null) { + ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); + } } - void sendStreakResult() { + void _sendStreakResult() { + ref.read(streakStorageProvider(initialContext.userId)).clearActiveStreak(); + if (initialContext.userId != null) { final streak = state.streak?.index; if (streak != null && streak > 0) { - ref.withClient( - (client) => _repository(client).postStreakRun(streak), - ); + ref.withClient((client) => _repository(client).postStreakRun(streak)); } } } - FutureResult retryFetchNextStreakPuzzle( - PuzzleStreak streak, - ) async { - state = state.copyWith( - nextPuzzleStreakFetchIsRetrying: true, - ); + FutureResult retryFetchNextStreakPuzzle(PuzzleStreak streak) async { + state = state.copyWith(nextPuzzleStreakFetchIsRetrying: true); final result = await _fetchNextStreakPuzzle(streak); - state = state.copyWith( - nextPuzzleStreakFetchIsRetrying: false, - ); + state = state.copyWith(nextPuzzleStreakFetchIsRetrying: false); result.match( onSuccess: (nextContext) { if (nextContext != null) { - state = state.copyWith( - streak: streak.copyWith( - index: streak.index + 1, - ), - ); + state = state.copyWith(streak: streak.copyWith(index: streak.index + 1)); } else { // no more puzzle - state = state.copyWith( - streak: streak.copyWith( - index: streak.index + 1, - finished: true, - ), - ); + state = state.copyWith(streak: streak.copyWith(index: streak.index + 1, finished: true)); } }, ); @@ -288,25 +280,24 @@ class PuzzleController extends _$PuzzleController { FutureResult _fetchNextStreakPuzzle(PuzzleStreak streak) { return streak.nextId != null ? Result.capture( - ref.withClient( - (client) => _repository(client).fetch(streak.nextId!).then( - (puzzle) => PuzzleContext( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzle: puzzle, - userId: initialContext.userId, - ), + ref.withClient( + (client) => _repository(client) + .fetch(streak.nextId!) + .then( + (puzzle) => PuzzleContext( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzle: puzzle, + userId: initialContext.userId, ), - ), - ) + ), + ), + ) : Future.value(Result.value(null)); } void _goToNextNode({bool replaying = false}) { if (state.node.children.isEmpty) return; - _setPath( - state.currentPath + state.node.children.first.id, - replaying: replaying, - ); + _setPath(state.currentPath + state.node.children.first.id, replaying: replaying); } void _goToPreviousNode({bool replaying = false}) { @@ -314,24 +305,19 @@ class PuzzleController extends _$PuzzleController { } Future _completePuzzle() async { - state = state.copyWith( - mode: PuzzleMode.view, - ); + state = state.copyWith(mode: PuzzleMode.view); await _onFailOrWin(state.result ?? PuzzleResult.win); } Future _onFailOrWin(PuzzleResult result) async { if (state.resultSent) return; - state = state.copyWith( - result: result, - resultSent: true, - ); + state = state.copyWith(result: result, resultSent: true); final soundService = ref.read(soundServiceProvider); if (state.streak == null) { - final next = await _service.solve( + final next = await (await _service).solve( userId: initialContext.userId, angle: initialContext.angle, puzzle: state.puzzle, @@ -342,34 +328,23 @@ class PuzzleController extends _$PuzzleController { ), ); - state = state.copyWith( - nextContext: next, - ); + state = state.copyWith(nextContext: next); ref - .read( - puzzleSessionProvider(initialContext.userId, initialContext.angle) - .notifier, - ) - .addAttempt( - state.puzzle.puzzle.id, - win: result == PuzzleResult.win, - ); + .read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier) + .addAttempt(state.puzzle.puzzle.id, win: result == PuzzleResult.win); final rounds = next?.rounds; if (rounds != null) { ref - .read( - puzzleSessionProvider(initialContext.userId, initialContext.angle) - .notifier, - ) + .read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier) .setRatingDiffs(rounds); } if (next != null && result == PuzzleResult.win && - ref.read(puzzlePreferencesProvider(initialContext.userId)).autoNext) { - loadPuzzle(next); + ref.read(puzzlePreferencesProvider).autoNext) { + onLoadPuzzle(next); } } else { // one fail and streak is over @@ -381,11 +356,9 @@ class PuzzleController extends _$PuzzleController { state = state.copyWith( mode: PuzzleMode.view, node: _gameTree.branchAt(state.currentPath).view, - streak: state.streak!.copyWith( - finished: true, - ), + streak: state.streak!.copyWith(finished: true), ); - sendStreakResult(); + _sendStreakResult(); } else { if (_nextPuzzleFuture == null) { assert(false, 'next puzzle future cannot be null with streak'); @@ -396,22 +369,17 @@ class PuzzleController extends _$PuzzleController { if (nextContext != null) { await Future.delayed(const Duration(milliseconds: 250)); soundService.play(Sound.confirmation); - loadPuzzle( + onLoadPuzzle( nextContext, - nextStreak: - state.streak!.copyWith(index: state.streak!.index + 1), + nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); } else { // no more puzzle - state = state.copyWith.streak!( - finished: true, - ); + state = state.copyWith.streak!(finished: true); } }, onError: (error, _) { - state = state.copyWith( - nextPuzzleStreakFetchError: true, - ); + state = state.copyWith(nextPuzzleStreakFetchError: true); }, ); } @@ -419,11 +387,7 @@ class PuzzleController extends _$PuzzleController { } } - void _setPath( - UciPath path, { - bool replaying = false, - bool firstMove = false, - }) { + void _setPath(UciPath path, {bool replaying = false, bool firstMove = false}) { final pathChange = state.currentPath != path; final newNode = _gameTree.branchAt(path).view; final sanMove = newNode.sanMove; @@ -451,6 +415,7 @@ class PuzzleController extends _$PuzzleController { currentPath: path, node: newNode, lastMove: sanMove.move, + promotionMove: null, ); if (pathChange) { @@ -459,9 +424,7 @@ class PuzzleController extends _$PuzzleController { } void toggleLocalEvaluation() { - state = state.copyWith( - isLocalEvalEnabled: !state.isLocalEvalEnabled, - ); + state = state.copyWith(isLocalEvalEnabled: !state.isLocalEvalEnabled); if (state.isLocalEvalEnabled) { ref.read(evaluationServiceProvider).initEngine(state.evaluationContext); _startEngineEval(); @@ -473,9 +436,8 @@ class PuzzleController extends _$PuzzleController { String makePgn() { final initPosition = _gameTree.nodeAt(state.initialPath).position; var currentPosition = initPosition; - final pgnMoves = state.puzzle.puzzle.solution.fold>([], - (List acc, move) { - final moveObj = Move.fromUci(move); + final pgnMoves = state.puzzle.puzzle.solution.fold>([], (List acc, move) { + final moveObj = Move.parse(move); if (moveObj != null) { final String san; (currentPosition, san) = currentPosition.makeSan(moveObj); @@ -488,8 +450,9 @@ class PuzzleController extends _$PuzzleController { return pgn; } - void _startEngineEval() { + Future _startEngineEval() async { if (!state.isEngineEnabled) return; + await ref.read(evaluationServiceProvider).ensureEngineInitialized(state.evaluationContext); _engineEvalDebounce( () => ref .read(evaluationServiceProvider) @@ -500,11 +463,14 @@ class PuzzleController extends _$PuzzleController { shouldEmit: (work) => work.path == state.currentPath, ) ?.forEach((t) { - final (work, eval) = t; - _gameTree.updateAt(work.path, (node) { - node.eval = eval; - }); - }), + final (work, eval) = t; + _gameTree.updateAt(work.path, (node) { + node.eval = eval; + }); + if (work.path == state.currentPath && eval.searchTime >= work.searchTime) { + state = state.copyWith(node: _gameTree.branchAt(state.currentPath).view); + } + }), ); } @@ -524,18 +490,10 @@ class PuzzleController extends _$PuzzleController { final (_, newNodes) = state.puzzle.puzzle.solution.foldIndexed( (initialNode.position, IList(const [])), (index, previous, uci) { - final move = Move.fromUci(uci); + final move = Move.parse(uci); final (pos, nodes) = previous; final (newPos, newSan) = pos.makeSan(move!); - return ( - newPos, - nodes.add( - Branch( - position: newPos, - sanMove: SanMove(newSan, move), - ), - ), - ); + return (newPos, nodes.add(Branch(position: newPos, sanMove: SanMove(newSan, move)))); }, ); _gameTree.addNodesAt(state.initialPath, newNodes, prepend: true); @@ -562,6 +520,7 @@ class PuzzleState with _$PuzzleState { required Side pov, required ViewBranch node, Move? lastMove, + NormalMove? promotionMove, PuzzleResult? result, PuzzleFeedback? feedback, required bool canViewSolution, @@ -581,16 +540,13 @@ class PuzzleState with _$PuzzleState { return mode == PuzzleMode.view && isLocalEvalEnabled; } - EvaluationContext get evaluationContext => EvaluationContext( - variant: Variant.standard, - initialPosition: initialPosition, - ); + EvaluationContext get evaluationContext => + EvaluationContext(variant: Variant.standard, initialPosition: initialPosition); Position get position => node.position; String get fen => node.position.fen; bool get canGoNext => mode == PuzzleMode.view && node.children.isNotEmpty; - bool get canGoBack => - mode == PuzzleMode.view && currentPath.size > initialPath.size; + bool get canGoBack => mode == PuzzleMode.view && currentPath.size > initialPath.size; - IMap> get validMoves => algebraicLegalMoves(position); + IMap> get validMoves => makeLegalMoves(position); } diff --git a/lib/src/model/puzzle/puzzle_difficulty.dart b/lib/src/model/puzzle/puzzle_difficulty.dart index bcda61e0f5..06012507e9 100644 --- a/lib/src/model/puzzle/puzzle_difficulty.dart +++ b/lib/src/model/puzzle/puzzle_difficulty.dart @@ -13,8 +13,9 @@ enum PuzzleDifficulty { const PuzzleDifficulty(this.ratingDelta); } -final IMap puzzleDifficultyNameMap = - IMap(PuzzleDifficulty.values.asNameMap()); +final IMap puzzleDifficultyNameMap = IMap( + PuzzleDifficulty.values.asNameMap(), +); String puzzleDifficultyL10n(BuildContext context, PuzzleDifficulty difficulty) { switch (difficulty) { diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index 8dba915b6f..6c3a88cab1 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -1,48 +1,24 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; -import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'puzzle_opening.freezed.dart'; part 'puzzle_opening.g.dart'; -@freezed -class PuzzleOpeningFamily with _$PuzzleOpeningFamily { - const factory PuzzleOpeningFamily({ - required String key, - required String name, - required int count, - required IList openings, - }) = _PuzzleOpeningFamily; -} +typedef PuzzleOpeningFamily = + ({String key, String name, int count, IList openings}); -@freezed -class PuzzleOpeningData with _$PuzzleOpeningData { - const factory PuzzleOpeningData({ - required String key, - required String name, - required int count, - }) = _PuzzleOpeningData; -} +typedef PuzzleOpeningData = ({String key, String name, int count}); +/// Returns a flattened list of openings with their respective counts. @riverpod -Future> _flatOpeningsList( - _FlatOpeningsListRef ref, -) async { - ref.cacheFor(const Duration(days: 1)); +Future> flatOpeningsList(Ref ref) async { final families = await ref.watch(puzzleOpeningsProvider.future); return families .map( (f) => [ - PuzzleOpeningData(key: f.key, name: f.name, count: f.count), - ...f.openings.map( - (o) => PuzzleOpeningData( - key: o.key, - name: '${f.name}: ${o.name}', - count: o.count, - ), - ), + (key: f.key, name: f.name, count: f.count), + ...f.openings.map((o) => (key: o.key, name: '${f.name}: ${o.name}', count: o.count)), ], ) .expand((e) => e) @@ -50,7 +26,7 @@ Future> _flatOpeningsList( } @riverpod -Future puzzleOpeningName(PuzzleOpeningNameRef ref, String key) async { - final openings = await ref.watch(_flatOpeningsListProvider.future); +Future puzzleOpeningName(Ref ref, String key) async { + final openings = await ref.watch(flatOpeningsListProvider.future); return openings.firstWhere((element) => element.key == key).name; } diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 0285ac476f..3f71177c44 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -1,47 +1,42 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_preferences.freezed.dart'; part 'puzzle_preferences.g.dart'; -const _prefKey = 'puzzle.preferences'; - @riverpod -class PuzzlePreferences extends _$PuzzlePreferences { +class PuzzlePreferences extends _$PuzzlePreferences with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties @override - PuzzlePrefState build(UserId? id) { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_makeKey(id)); - return stored != null - ? PuzzlePrefState.fromJson(jsonDecode(stored) as Map) - : PuzzlePrefState.defaults(id: id); + final prefCategory = PrefCategory.puzzle; + + @override + PuzzlePrefs defaults({LightUser? user}) => PuzzlePrefs.defaults(id: user?.id); + + @override + PuzzlePrefs fromJson(Map json) => PuzzlePrefs.fromJson(json); + + @override + PuzzlePrefs build() { + return fetch(); } Future setDifficulty(PuzzleDifficulty difficulty) async { - final newState = state.copyWith(difficulty: difficulty); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(_makeKey(id), jsonEncode(newState.toJson())); - state = newState; + save(state.copyWith(difficulty: difficulty)); } Future setAutoNext(bool autoNext) async { - final newState = state.copyWith(autoNext: autoNext); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(_makeKey(id), jsonEncode(newState.toJson())); - state = newState; + save(state.copyWith(autoNext: autoNext)); } - - String _makeKey(UserId? id) => '$_prefKey.${id ?? ''}'; } @Freezed(fromJson: true, toJson: true) -class PuzzlePrefState with _$PuzzlePrefState { - const factory PuzzlePrefState({ +class PuzzlePrefs with _$PuzzlePrefs implements Serializable { + const factory PuzzlePrefs({ required UserId? id, required PuzzleDifficulty difficulty, @@ -49,14 +44,10 @@ class PuzzlePrefState with _$PuzzlePrefState { /// no effect on puzzle streaks, which always show next puzzle. Defaults to /// `false`. @Default(false) bool autoNext, - }) = _PuzzlePrefState; + }) = _PuzzlePrefs; - factory PuzzlePrefState.defaults({UserId? id}) => PuzzlePrefState( - id: id, - difficulty: PuzzleDifficulty.normal, - autoNext: false, - ); + factory PuzzlePrefs.defaults({UserId? id}) => + PuzzlePrefs(id: id, difficulty: PuzzleDifficulty.normal, autoNext: false); - factory PuzzlePrefState.fromJson(Map json) => - _$PuzzlePrefStateFromJson(json); + factory PuzzlePrefs.fromJson(Map json) => _$PuzzlePrefsFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 13d71c7662..7923986f87 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; @@ -11,49 +11,73 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/puzzle/storm.dart'; +import 'package:lichess_mobile/src/model/puzzle/streak_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_providers.g.dart'; @riverpod -Future nextPuzzle( - NextPuzzleRef ref, - PuzzleAngle angle, -) { +Future nextPuzzle(Ref ref, PuzzleAngle angle) async { final session = ref.watch(authSessionProvider); - final puzzleService = ref.read(puzzleServiceFactoryProvider)( + final puzzleService = await ref.read(puzzleServiceFactoryProvider)( queueLength: kPuzzleLocalQueueLength, ); + // useful for for preview puzzle list in puzzle tab (providers in a list can + // be invalidated multiple times when the user scrolls the list) + ref.cacheFor(const Duration(minutes: 1)); - return puzzleService.nextPuzzle( - userId: session?.user.id, - angle: angle, - ); + return puzzleService.nextPuzzle(userId: session?.user.id, angle: angle); } +typedef InitialStreak = ({PuzzleStreak streak, Puzzle puzzle}); + +/// Fetches the active streak from the local storage if available, otherwise fetches it from the server. @riverpod -Future streak(StreakRef ref) { - return ref.withClient((client) => PuzzleRepository(client).streak()); +Future streak(Ref ref) async { + final session = ref.watch(authSessionProvider); + final streakStorage = ref.watch(streakStorageProvider(session?.user.id)); + final activeStreak = await streakStorage.loadActiveStreak(); + if (activeStreak != null) { + final puzzle = await ref.read(puzzleProvider(activeStreak.streak[activeStreak.index]).future); + + return (streak: activeStreak, puzzle: puzzle); + } + + final rsp = await ref.withClient((client) => PuzzleRepository(client).streak()); + + return ( + streak: PuzzleStreak( + streak: rsp.streak, + index: 0, + hasSkipped: false, + finished: false, + timestamp: rsp.timestamp, + ), + puzzle: rsp.puzzle, + ); } @riverpod -Future storm(StormRef ref) { +Future storm(Ref ref) { return ref.withClient((client) => PuzzleRepository(client).storm()); } /// Fetches a puzzle from the local storage if available, otherwise fetches it from the server. @riverpod -Future puzzle(PuzzleRef ref, PuzzleId id) async { - final puzzleStorage = ref.watch(puzzleStorageProvider); +Future puzzle(Ref ref, PuzzleId id) async { + final puzzleStorage = await ref.watch(puzzleStorageProvider.future); final puzzle = await puzzleStorage.fetch(puzzleId: id); if (puzzle != null) return puzzle; return ref.withClient((client) => PuzzleRepository(client).fetch(id)); } @riverpod -Future dailyPuzzle(DailyPuzzleRef ref) { +Future dailyPuzzle(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).daily(), const Duration(hours: 6), @@ -61,26 +85,28 @@ Future dailyPuzzle(DailyPuzzleRef ref) { } @riverpod -Future> savedThemeBatches(SavedThemeBatchesRef ref) { +Future> savedBatches(Ref ref) async { + final session = ref.watch(authSessionProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); + return storage.fetchAll(userId: session?.user.id); +} + +@riverpod +Future> savedThemeBatches(Ref ref) async { final session = ref.watch(authSessionProvider); - final storage = ref.watch(puzzleBatchStorageProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedThemes(userId: session?.user.id); } @riverpod -Future> savedOpeningBatches( - SavedOpeningBatchesRef ref, -) async { +Future> savedOpeningBatches(Ref ref) async { final session = ref.watch(authSessionProvider); - final storage = ref.watch(puzzleBatchStorageProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedOpenings(userId: session?.user.id); } @riverpod -Future puzzleDashboard( - PuzzleDashboardRef ref, - int days, -) async { +Future puzzleDashboard(Ref ref, int days) async { final session = ref.watch(authSessionProvider); if (session == null) return null; return ref.withClientCacheFor( @@ -90,9 +116,7 @@ Future puzzleDashboard( } @riverpod -Future?> puzzleRecentActivity( - PuzzleRecentActivityRef ref, -) async { +Future?> puzzleRecentActivity(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return null; return ref.withClientCacheFor( @@ -102,16 +126,12 @@ Future?> puzzleRecentActivity( } @riverpod -Future stormDashboard(StormDashboardRef ref, UserId id) async { - return ref.withClient( - (client) => PuzzleRepository(client).stormDashboard(id), - ); +Future stormDashboard(Ref ref, UserId id) async { + return ref.withClient((client) => PuzzleRepository(client).stormDashboard(id)); } @riverpod -Future> puzzleThemes( - PuzzleThemesRef ref, -) { +Future> puzzleThemes(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).puzzleThemes(), const Duration(days: 1), @@ -119,7 +139,7 @@ Future> puzzleThemes( } @riverpod -Future> puzzleOpenings(PuzzleOpeningsRef ref) { +Future> puzzleOpenings(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).puzzleOpenings(), const Duration(days: 1), diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index de6028f972..ca334d58a4 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -8,9 +8,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'puzzle.dart'; @@ -36,10 +36,7 @@ class PuzzleRepository { return client.readJson( Uri( path: '/api/puzzle/batch/${angle.key}', - queryParameters: { - 'nb': nb.toString(), - 'difficulty': difficulty.name, - }, + queryParameters: {'nb': nb.toString(), 'difficulty': difficulty.name}, ), mapper: _decodeBatchResponse, ); @@ -54,32 +51,18 @@ class PuzzleRepository { return client.postReadJson( Uri( path: '/api/puzzle/batch/${angle.key}', - queryParameters: { - 'nb': nb.toString(), - 'difficulty': difficulty.name, - }, + queryParameters: {'nb': nb.toString(), 'difficulty': difficulty.name}, ), headers: {'Content-type': 'application/json'}, body: jsonEncode({ - 'solutions': solved - .map( - (e) => { - 'id': e.id, - 'win': e.win, - 'rated': e.rated, - }, - ) - .toList(), + 'solutions': solved.map((e) => {'id': e.id, 'win': e.win, 'rated': e.rated}).toList(), }), mapper: _decodeBatchResponse, ); } Future fetch(PuzzleId id) { - return client.readJson( - Uri(path: '/api/puzzle/$id'), - mapper: _puzzleFromJson, - ); + return client.readJson(Uri(path: '/api/puzzle/$id'), mapper: _puzzleFromJson); } Future streak() { @@ -88,11 +71,7 @@ class PuzzleRepository { mapper: (Map json) { return PuzzleStreakResponse( puzzle: _puzzleFromPick(pick(json).required()), - streak: IList( - pick(json['streak']).asStringOrThrow().split(' ').map( - (e) => PuzzleId(e), - ), - ), + streak: IList(pick(json['streak']).asStringOrThrow().split(' ').map((e) => PuzzleId(e))), timestamp: DateTime.now(), ); }, @@ -103,10 +82,7 @@ class PuzzleRepository { final uri = Uri(path: '/api/streak/$run'); final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to post streak run: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to post streak run: ${response.statusCode}', uri); } } @@ -115,9 +91,7 @@ class PuzzleRepository { Uri(path: '/api/storm'), mapper: (Map json) { return PuzzleStormResponse( - puzzles: IList( - pick(json['puzzles']).asListOrThrow(_litePuzzleFromPick), - ), + puzzles: IList(pick(json['puzzles']).asListOrThrow(_litePuzzleFromPick)), highscore: pick(json['high']).letOrNull(_stormHighScoreFromPick), key: pick(json['key']).asStringOrNull(), timestamp: DateTime.now(), @@ -155,15 +129,8 @@ class PuzzleRepository { Future daily() { return client - .readJson( - Uri(path: '/api/puzzle/daily'), - mapper: _puzzleFromJson, - ) - .then( - (puzzle) => puzzle.copyWith( - isDailyPuzzle: true, - ), - ); + .readJson(Uri(path: '/api/puzzle/daily'), mapper: _puzzleFromJson) + .then((puzzle) => puzzle.copyWith(isDailyPuzzle: true)); } Future puzzleDashboard(int days) { @@ -173,17 +140,13 @@ class PuzzleRepository { ); } - Future> puzzleActivity( - int max, { - DateTime? before, - }) { + Future> puzzleActivity(int max, {DateTime? before}) { return client.readNdJsonList( Uri( path: '/api/puzzle/activity', queryParameters: { 'max': max.toString(), - if (before != null) - 'before': before.millisecondsSinceEpoch.toString(), + if (before != null) 'before': before.millisecondsSinceEpoch.toString(), }, ), mapper: _puzzleActivityFromJson, @@ -228,13 +191,9 @@ class PuzzleRepository { }), ), glicko: pick(json['glicko']).letOrNull(_puzzleGlickoFromPick), - rounds: pick(json['rounds']).letOrNull( - (p0) => IList( - p0.asListOrNull( - (p1) => _puzzleRoundFromPick(p1), - ), - ), - ), + rounds: pick( + json['rounds'], + ).letOrNull((p0) => IList(p0.asListOrNull((p1) => _puzzleRoundFromPick(p1)))), ); } } @@ -284,15 +243,12 @@ class PuzzleStormResponse with _$PuzzleStormResponse { PuzzleHistoryEntry _puzzleActivityFromJson(Map json) => _historyPuzzleFromPick(pick(json).required()); -Puzzle _puzzleFromJson(Map json) => - _puzzleFromPick(pick(json).required()); +Puzzle _puzzleFromJson(Map json) => _puzzleFromPick(pick(json).required()); PuzzleDashboard _puzzleDashboardFromJson(Map json) => _puzzleDashboardFromPick(pick(json).required()); -IMap _puzzleThemeFromJson( - Map json, -) => +IMap _puzzleThemeFromJson(Map json) => _puzzleThemeFromPick(pick(json).required()); IList _puzzleOpeningFromJson(Map json) => @@ -317,20 +273,17 @@ StormDashboard _stormDashboardFromPick(RequiredPick pick) { month: pick('high', 'month').asIntOrThrow(), week: pick('high', 'week').asIntOrThrow(), ), - dayHighscores: pick('days') - .asListOrThrow((p0) => _stormDayFromPick(p0, dateFormat)) - .toIList(), + dayHighscores: pick('days').asListOrThrow((p0) => _stormDayFromPick(p0, dateFormat)).toIList(), ); } -StormDayScore _stormDayFromPick(RequiredPick pick, DateFormat format) => - StormDayScore( - runs: pick('runs').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - time: pick('time').asIntOrThrow(), - highest: pick('highest').asIntOrThrow(), - day: format.parse(pick('_id').asStringOrThrow()), - ); +StormDayScore _stormDayFromPick(RequiredPick pick, DateFormat format) => StormDayScore( + runs: pick('runs').asIntOrThrow(), + score: pick('score').asIntOrThrow(), + time: pick('time').asIntOrThrow(), + highest: pick('highest').asIntOrThrow(), + day: format.parse(pick('_id').asStringOrThrow()), +); LitePuzzle _litePuzzleFromPick(RequiredPick pick) { return LitePuzzle( @@ -357,8 +310,7 @@ PuzzleData _puzzleDatafromPick(RequiredPick pick) { plays: pick('plays').asIntOrThrow(), initialPly: pick('initialPly').asIntOrThrow(), solution: pick('solution').asListOrThrow((p0) => p0.asStringOrThrow()).lock, - themes: - pick('themes').asListOrThrow((p0) => p0.asStringOrThrow()).toSet().lock, + themes: pick('themes').asListOrThrow((p0) => p0.asStringOrThrow()).toSet().lock, ); } @@ -384,14 +336,10 @@ PuzzleGame _puzzleGameFromPick(RequiredPick pick) { perf: pick('perf', 'key').asPerfOrThrow(), rated: pick('rated').asBoolOrThrow(), white: pick('players').letOrThrow( - (it) => it - .asListOrThrow(_puzzlePlayerFromPick) - .firstWhere((p) => p.side == Side.white), + (it) => it.asListOrThrow(_puzzlePlayerFromPick).firstWhere((p) => p.side == Side.white), ), black: pick('players').letOrThrow( - (it) => it - .asListOrThrow(_puzzlePlayerFromPick) - .firstWhere((p) => p.side == Side.black), + (it) => it.asListOrThrow(_puzzlePlayerFromPick).firstWhere((p) => p.side == Side.black), ), pgn: pick('pgn').asStringOrThrow(), ); @@ -417,29 +365,24 @@ PuzzleHistoryEntry _historyPuzzleFromPick(RequiredPick pick) { } PuzzleDashboard _puzzleDashboardFromPick(RequiredPick pick) => PuzzleDashboard( - global: PuzzleDashboardData( - nb: pick('global')('nb').asIntOrThrow(), - firstWins: pick('global')('firstWins').asIntOrThrow(), - replayWins: pick('global')('replayWins').asIntOrThrow(), - performance: pick('global')('performance').asIntOrThrow(), - theme: PuzzleThemeKey.mix, - ), - themes: pick('themes') + global: PuzzleDashboardData( + nb: pick('global')('nb').asIntOrThrow(), + firstWins: pick('global')('firstWins').asIntOrThrow(), + replayWins: pick('global')('replayWins').asIntOrThrow(), + performance: pick('global')('performance').asIntOrThrow(), + theme: PuzzleThemeKey.mix, + ), + themes: + pick('themes') .asMapOrThrow>() .keys .map( - (key) => _puzzleDashboardDataFromPick( - pick('themes')(key)('results').required(), - key, - ), + (key) => _puzzleDashboardDataFromPick(pick('themes')(key)('results').required(), key), ) .toIList(), - ); +); -PuzzleDashboardData _puzzleDashboardDataFromPick( - RequiredPick results, - String themeKey, -) => +PuzzleDashboardData _puzzleDashboardDataFromPick(RequiredPick results, String themeKey) => PuzzleDashboardData( nb: results('nb').asIntOrThrow(), firstWins: results('firstWins').asIntOrThrow(), @@ -457,8 +400,7 @@ IMap _puzzleThemeFromPick(RequiredPick pick) { return PuzzleThemeData( count: listPick('count').asIntOrThrow(), desc: listPick('desc').asStringOrThrow(), - key: themeMap[listPick('key').asStringOrThrow()] ?? - PuzzleThemeKey.unsupported, + key: themeMap[listPick('key').asStringOrThrow()] ?? PuzzleThemeKey.unsupported, name: listPick('name').asStringOrThrow(), ); }) @@ -475,18 +417,18 @@ IList _puzzleOpeningFromPick(RequiredPick pick) { return pick('openings').asListOrThrow((openingPick) { final familyPick = openingPick('family'); final openings = openingPick('openings').asListOrNull( - (openPick) => PuzzleOpeningData( + (openPick) => ( key: openPick('key').asStringOrThrow(), name: openPick('name').asStringOrThrow(), count: openPick('count').asIntOrThrow(), ), ); - return PuzzleOpeningFamily( + return ( key: familyPick('key').asStringOrThrow(), name: familyPick('name').asStringOrThrow(), count: familyPick('count').asIntOrThrow(), - openings: openings != null ? openings.toIList() : IList(const []), + openings: openings != null ? openings.toIList() : IList(const []), ); }).toIList(); } diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index 1e0a01dc56..9dd9196076 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -2,10 +2,11 @@ import 'dart:math' show max; import 'package:async/async.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -24,20 +25,25 @@ part 'puzzle_service.g.dart'; const kPuzzleLocalQueueLength = 50; @Riverpod(keepAlive: true) -PuzzleServiceFactory puzzleServiceFactory(PuzzleServiceFactoryRef ref) { +Future puzzleService(Ref ref) { + return ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength); +} + +@Riverpod(keepAlive: true) +PuzzleServiceFactory puzzleServiceFactory(Ref ref) { return PuzzleServiceFactory(ref); } class PuzzleServiceFactory { PuzzleServiceFactory(this._ref); - final PuzzleServiceFactoryRef _ref; + final Ref _ref; - PuzzleService call({required int queueLength}) { + Future call({required int queueLength}) async { return PuzzleService( _ref, - batchStorage: _ref.read(puzzleBatchStorageProvider), - puzzleStorage: _ref.read(puzzleStorageProvider), + batchStorage: await _ref.read(puzzleBatchStorageProvider.future), + puzzleStorage: await _ref.read(puzzleStorageProvider.future), queueLength: queueLength, ); } @@ -66,7 +72,7 @@ class PuzzleService { required this.queueLength, }); - final PuzzleServiceFactoryRef _ref; + final Ref _ref; final int queueLength; final PuzzleBatchStorage batchStorage; final PuzzleStorage puzzleStorage; @@ -82,15 +88,16 @@ class PuzzleService { }) { return Result.release( _syncAndLoadData(userId, angle).map( - (data) => data.$1 != null && data.$1!.unsolved.isNotEmpty - ? PuzzleContext( - puzzle: data.$1!.unsolved[0], - angle: angle, - userId: userId, - glicko: data.$2, - rounds: data.$3, - ) - : null, + (data) => + data.$1 != null && data.$1!.unsolved.isNotEmpty + ? PuzzleContext( + puzzle: data.$1!.unsolved[0], + angle: angle, + userId: userId, + glicko: data.$2, + rounds: data.$3, + ) + : null, ), ); } @@ -106,18 +113,14 @@ class PuzzleService { PuzzleAngle angle = const PuzzleTheme(PuzzleThemeKey.mix), }) async { puzzleStorage.save(puzzle: puzzle); - final data = await batchStorage.fetch( - userId: userId, - angle: angle, - ); + final data = await batchStorage.fetch(userId: userId, angle: angle); if (data != null) { await batchStorage.save( userId: userId, angle: angle, data: PuzzleBatch( solved: IList([...data.solved, solution]), - unsolved: - data.unsolved.removeWhere((e) => e.puzzle.id == solution.id), + unsolved: data.unsolved.removeWhere((e) => e.puzzle.id == solution.id), ), ); } @@ -133,6 +136,11 @@ class PuzzleService { return nextPuzzle(userId: userId, angle: angle); } + /// Deletes the puzzle batch of [angle] from the local storage. + Future deleteBatch({required UserId? userId, required PuzzleAngle angle}) async { + await batchStorage.delete(userId: userId, angle: angle); + } + /// Synchronize offline puzzle queue with server and gets latest data. /// /// This task will fetch missing puzzles so the queue length is always equal to @@ -141,71 +149,60 @@ class PuzzleService { /// /// This method should never fail, as if the network is down it will fallback /// to the local database. - FutureResult<(PuzzleBatch?, PuzzleGlicko?, IList?)> - _syncAndLoadData( + FutureResult<(PuzzleBatch?, PuzzleGlicko?, IList?)> _syncAndLoadData( UserId? userId, PuzzleAngle angle, ) async { - final data = await batchStorage.fetch( - userId: userId, - angle: angle, - ); + final data = await batchStorage.fetch(userId: userId, angle: angle); final unsolved = data?.unsolved ?? IList(const []); final solved = data?.solved ?? IList(const []); final deficit = max(0, queueLength - unsolved.length); - _log.fine('Have a puzzle deficit of $deficit, will sync with lichess'); - - final difficulty = _ref.read(puzzlePreferencesProvider(userId)).difficulty; - - // anonymous users can't solve puzzles so we just download the deficit - // we send the request even if the deficit is 0 to get the glicko rating - final batchResponse = _ref.withClient( - (client) => Result.capture( - solved.isNotEmpty && userId != null - ? PuzzleRepository(client).solveBatch( - nb: deficit, - solved: solved, - angle: angle, - difficulty: difficulty, - ) - : PuzzleRepository(client).selectBatch( - nb: deficit, - angle: angle, - difficulty: difficulty, - ), - ), - ); - - return batchResponse - .fold( - (value) => Result.value( - ( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([...unsolved, ...value.puzzles]), - ), - value.glicko, - value.rounds, - true, // should save the batch + if (deficit > 0) { + _log.fine('Have a puzzle deficit of $deficit, will sync with lichess'); + + final difficulty = _ref.read(puzzlePreferencesProvider).difficulty; + + // anonymous users can't solve puzzles so we just download the deficit + // we send the request even if the deficit is 0 to get the glicko rating + final batchResponse = _ref.withClient( + (client) => Result.capture( + solved.isNotEmpty && userId != null + ? PuzzleRepository( + client, + ).solveBatch(nb: deficit, solved: solved, angle: angle, difficulty: difficulty) + : PuzzleRepository( + client, + ).selectBatch(nb: deficit, angle: angle, difficulty: difficulty), ), - ), + ); + + return batchResponse + .fold( + (value) => Result.value(( + PuzzleBatch( + solved: IList(const []), + unsolved: IList([...unsolved, ...value.puzzles]), + ), + value.glicko, + value.rounds, + true, // should save the batch + )), + + // we don't need to save the batch if the request failed + (_, __) => Result.value((data, null, null, false)), + ) + .flatMap((tuple) async { + final (newBatch, glicko, rounds, shouldSave) = tuple; + if (newBatch != null && shouldSave) { + await batchStorage.save(userId: userId, angle: angle, data: newBatch); + } + return Result.value((newBatch, glicko, rounds)); + }); + } - // we don't need to save the batch if the request failed - (_, __) => Result.value((data, null, null, false)), - ) - .flatMap((tuple) async { - final (newBatch, glicko, rounds, shouldSave) = tuple; - if (newBatch != null && shouldSave) { - await batchStorage.save( - userId: userId, - angle: angle, - data: newBatch, - ); - } - return Result.value((newBatch, glicko, rounds)); - }); + return Result.value((data, null, null)); } } diff --git a/lib/src/model/puzzle/puzzle_session.dart b/lib/src/model/puzzle/puzzle_session.dart index 609112286a..572d302ffc 100644 --- a/lib/src/model/puzzle/puzzle_session.dart +++ b/lib/src/model/puzzle/puzzle_session.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; @@ -38,8 +38,7 @@ class PuzzleSession extends _$PuzzleSession { addIfNotFound: true, ); final newState = d.copyWith( - attempts: - newAttempts.length > maxSize ? newAttempts.sublist(1) : newAttempts, + attempts: newAttempts.length > maxSize ? newAttempts.sublist(1) : newAttempts, lastUpdatedAt: DateTime.now(), ); state = newState; @@ -50,20 +49,19 @@ class PuzzleSession extends _$PuzzleSession { Future setRatingDiffs(Iterable rounds) async { await _update((d) { final newState = d.copyWith( - attempts: d.attempts.map((a) { - final round = rounds.firstWhereOrNull((r) => r.id == a.id); - return round != null ? a.copyWith(ratingDiff: round.ratingDiff) : a; - }).toIList(), + attempts: + d.attempts.map((a) { + final round = rounds.firstWhereOrNull((r) => r.id == a.id); + return round != null ? a.copyWith(ratingDiff: round.ratingDiff) : a; + }).toIList(), ); state = newState; return newState; }); } - Future _update( - PuzzleSessionData Function(PuzzleSessionData d) update, - ) async { - await _store.setString(_storageKey, jsonEncode((update(state)).toJson())); + Future _update(PuzzleSessionData Function(PuzzleSessionData d) update) async { + await _store.setString(_storageKey, jsonEncode(update(state).toJson())); } PuzzleSessionData? get _stored { @@ -71,13 +69,12 @@ class PuzzleSession extends _$PuzzleSession { if (stored == null) { return PuzzleSessionData.initial(angle: angle); } - return PuzzleSessionData.fromJson( - jsonDecode(stored) as Map, - ); + return PuzzleSessionData.fromJson(jsonDecode(stored) as Map); } - SharedPreferences get _store => ref.read(sharedPreferencesProvider); - String get _storageKey => 'puzzle_session.${userId ?? 'anon'}'; + SharedPreferencesWithCache get _store => LichessBinding.instance.sharedPreferences; + + String get _storageKey => 'puzzle_session.${userId ?? '**anon**'}'; } @Freezed(fromJson: true, toJson: true) @@ -88,9 +85,7 @@ class PuzzleSessionData with _$PuzzleSessionData { required DateTime lastUpdatedAt, }) = _PuzzleSession; - factory PuzzleSessionData.initial({ - required PuzzleAngle angle, - }) { + factory PuzzleSessionData.initial({required PuzzleAngle angle}) { return PuzzleSessionData( angle: angle, attempts: IList(const []), @@ -102,9 +97,7 @@ class PuzzleSessionData with _$PuzzleSessionData { try { return _$PuzzleSessionDataFromJson(json); } catch (e) { - return PuzzleSessionData.initial( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ); + return PuzzleSessionData.initial(angle: const PuzzleTheme(PuzzleThemeKey.mix)); } } } @@ -113,14 +106,10 @@ class PuzzleSessionData with _$PuzzleSessionData { class PuzzleAttempt with _$PuzzleAttempt { const PuzzleAttempt._(); - const factory PuzzleAttempt({ - required PuzzleId id, - required bool win, - int? ratingDiff, - }) = _PuzzleAttempt; + const factory PuzzleAttempt({required PuzzleId id, required bool win, int? ratingDiff}) = + _PuzzleAttempt; - factory PuzzleAttempt.fromJson(Map json) => - _$PuzzleAttemptFromJson(json); + factory PuzzleAttempt.fromJson(Map json) => _$PuzzleAttemptFromJson(json); String? get ratingDiffString { if (ratingDiff == null) return null; diff --git a/lib/src/model/puzzle/puzzle_storage.dart b/lib/src/model/puzzle/puzzle_storage.dart index 39fa23a440..c11fc70ae0 100644 --- a/lib/src/model/puzzle/puzzle_storage.dart +++ b/lib/src/model/puzzle/puzzle_storage.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -9,8 +10,8 @@ import 'package:sqflite/sqflite.dart'; part 'puzzle_storage.g.dart'; @Riverpod(keepAlive: true) -PuzzleStorage puzzleStorage(PuzzleStorageRef ref) { - final db = ref.watch(databaseProvider); +Future puzzleStorage(Ref ref) async { + final db = await ref.watch(databaseProvider.future); return PuzzleStorage(db); } @@ -21,9 +22,7 @@ class PuzzleStorage { const PuzzleStorage(this._db); final Database _db; - Future fetch({ - required PuzzleId puzzleId, - }) async { + Future fetch({required PuzzleId puzzleId}) async { final list = await _db.query( _tableName, where: 'puzzleId = ?', @@ -44,17 +43,11 @@ class PuzzleStorage { return null; } - Future save({ - required Puzzle puzzle, - }) async { - await _db.insert( - _tableName, - { - 'puzzleId': puzzle.puzzle.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(puzzle.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + Future save({required Puzzle puzzle}) async { + await _db.insert(_tableName, { + 'puzzleId': puzzle.puzzle.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(puzzle.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); } } diff --git a/lib/src/model/puzzle/puzzle_streak.dart b/lib/src/model/puzzle/puzzle_streak.dart index 02b7122be0..68a03913dc 100644 --- a/lib/src/model/puzzle/puzzle_streak.dart +++ b/lib/src/model/puzzle/puzzle_streak.dart @@ -2,11 +2,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +part 'puzzle_streak.g.dart'; part 'puzzle_streak.freezed.dart'; typedef Streak = IList; -@freezed +@Freezed(fromJson: true, toJson: true) class PuzzleStreak with _$PuzzleStreak { const PuzzleStreak._(); @@ -19,4 +20,6 @@ class PuzzleStreak with _$PuzzleStreak { }) = _PuzzleStreak; PuzzleId? get nextId => streak.getOrNull(index + 1); + + factory PuzzleStreak.fromJson(Map json) => _$PuzzleStreakFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index fbba9af65f..0a223fbb01 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -1,9 +1,10 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/localizations.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_theme.freezed.dart'; @@ -20,91 +21,397 @@ class PuzzleThemeData with _$PuzzleThemeData { } enum PuzzleThemeKey { - mix, - advancedPawn, - advantage, - anastasiaMate, - arabianMate, - attackingF2F7, - attraction, - backRankMate, - bishopEndgame, - bodenMate, - capturingDefender, - castling, - clearance, - crushing, - defensiveMove, - deflection, - discoveredAttack, - doubleBishopMate, - doubleCheck, - dovetailMate, - equality, - endgame, - enPassant, - exposedKing, - fork, - hangingPiece, - hookMate, - interference, - intermezzo, - kingsideAttack, - knightEndgame, - long, - master, - masterVsMaster, - mate, - mateIn1, - mateIn2, - mateIn3, - mateIn4, - mateIn5, - smotheredMate, - middlegame, - oneMove, - opening, - pawnEndgame, - pin, - promotion, - queenEndgame, - queenRookEndgame, - queensideAttack, - quietMove, - rookEndgame, - sacrifice, - short, - skewer, - superGM, - trappedPiece, - underPromotion, - veryLong, - xRayAttack, - zugzwang, - // checkFirst, + mix(PuzzleIcons.mix), + advancedPawn(PuzzleIcons.advancedPawn), + advantage(PuzzleIcons.advantage), + anastasiaMate(PuzzleIcons.anastasiaMate), + arabianMate(PuzzleIcons.arabianMate), + attackingF2F7(PuzzleIcons.attackingF2F7), + attraction(PuzzleIcons.attraction), + backRankMate(PuzzleIcons.backRankMate), + bishopEndgame(PuzzleIcons.bishopEndgame), + bodenMate(PuzzleIcons.bodenMate), + capturingDefender(PuzzleIcons.capturingDefender), + castling(PuzzleIcons.castling), + clearance(PuzzleIcons.clearance), + crushing(PuzzleIcons.crushing), + defensiveMove(PuzzleIcons.defensiveMove), + deflection(PuzzleIcons.deflection), + discoveredAttack(PuzzleIcons.discoveredAttack), + doubleBishopMate(PuzzleIcons.doubleBishopMate), + doubleCheck(PuzzleIcons.doubleCheck), + dovetailMate(PuzzleIcons.dovetailMate), + equality(PuzzleIcons.equality), + endgame(PuzzleIcons.endgame), + enPassant(PuzzleIcons.enPassant), + exposedKing(PuzzleIcons.exposedKing), + fork(PuzzleIcons.fork), + hangingPiece(PuzzleIcons.hangingPiece), + hookMate(PuzzleIcons.hookMate), + interference(PuzzleIcons.interference), + intermezzo(PuzzleIcons.intermezzo), + kingsideAttack(PuzzleIcons.kingsideAttack), + knightEndgame(PuzzleIcons.knightEndgame), + long(PuzzleIcons.long), + master(PuzzleIcons.master), + masterVsMaster(PuzzleIcons.masterVsMaster), + mate(PuzzleIcons.mate), + mateIn1(PuzzleIcons.mate), + mateIn2(PuzzleIcons.mate), + mateIn3(PuzzleIcons.mate), + mateIn4(PuzzleIcons.mate), + mateIn5(PuzzleIcons.mate), + smotheredMate(PuzzleIcons.smotheredMate), + middlegame(PuzzleIcons.middlegame), + oneMove(PuzzleIcons.oneMove), + opening(PuzzleIcons.opening), + pawnEndgame(PuzzleIcons.pawnEndgame), + pin(PuzzleIcons.pin), + promotion(PuzzleIcons.promotion), + queenEndgame(PuzzleIcons.queenEndgame), + queenRookEndgame(PuzzleIcons.queenRookEndgame), + queensideAttack(PuzzleIcons.queensideAttack), + quietMove(PuzzleIcons.quietMove), + rookEndgame(PuzzleIcons.rookEndgame), + sacrifice(PuzzleIcons.sacrifice), + short(PuzzleIcons.short), + skewer(PuzzleIcons.skewer), + superGM(PuzzleIcons.superGM), + trappedPiece(PuzzleIcons.trappedPiece), + underPromotion(PuzzleIcons.underPromotion), + veryLong(PuzzleIcons.veryLong), + xRayAttack(PuzzleIcons.xRayAttack), + zugzwang(PuzzleIcons.zugzwang), // used internally to filter out unsupported keys - unsupported, + unsupported(PuzzleIcons.mix); + + const PuzzleThemeKey(this.icon); + + final IconData icon; + + PuzzleThemeL10n l10n(AppLocalizations l10n) { + switch (this) { + case PuzzleThemeKey.mix: + case PuzzleThemeKey.unsupported: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMix, + description: l10n.puzzleThemeMixDescription, + ); + case PuzzleThemeKey.advancedPawn: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAdvancedPawn, + description: l10n.puzzleThemeAdvancedPawnDescription, + ); + case PuzzleThemeKey.advantage: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAdvantage, + description: l10n.puzzleThemeAdvantageDescription, + ); + case PuzzleThemeKey.anastasiaMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAnastasiaMate, + description: l10n.puzzleThemeAnastasiaMateDescription, + ); + case PuzzleThemeKey.arabianMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeArabianMate, + description: l10n.puzzleThemeArabianMateDescription, + ); + case PuzzleThemeKey.attackingF2F7: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAttackingF2F7, + description: l10n.puzzleThemeAttackingF2F7Description, + ); + case PuzzleThemeKey.attraction: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAttraction, + description: l10n.puzzleThemeAttractionDescription, + ); + case PuzzleThemeKey.backRankMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBackRankMate, + description: l10n.puzzleThemeBackRankMateDescription, + ); + case PuzzleThemeKey.bishopEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBishopEndgame, + description: l10n.puzzleThemeBishopEndgameDescription, + ); + case PuzzleThemeKey.bodenMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBodenMate, + description: l10n.puzzleThemeBodenMateDescription, + ); + case PuzzleThemeKey.capturingDefender: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCapturingDefender, + description: l10n.puzzleThemeCapturingDefenderDescription, + ); + case PuzzleThemeKey.castling: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCastling, + description: l10n.puzzleThemeCastlingDescription, + ); + case PuzzleThemeKey.clearance: + return PuzzleThemeL10n( + name: l10n.puzzleThemeClearance, + description: l10n.puzzleThemeClearanceDescription, + ); + case PuzzleThemeKey.crushing: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCrushing, + description: l10n.puzzleThemeCrushingDescription, + ); + case PuzzleThemeKey.defensiveMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDefensiveMove, + description: l10n.puzzleThemeDefensiveMoveDescription, + ); + case PuzzleThemeKey.deflection: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDeflection, + description: l10n.puzzleThemeDeflectionDescription, + ); + case PuzzleThemeKey.discoveredAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDiscoveredAttack, + description: l10n.puzzleThemeDiscoveredAttackDescription, + ); + case PuzzleThemeKey.doubleBishopMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDoubleBishopMate, + description: l10n.puzzleThemeDoubleBishopMateDescription, + ); + case PuzzleThemeKey.doubleCheck: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDoubleCheck, + description: l10n.puzzleThemeDoubleCheckDescription, + ); + case PuzzleThemeKey.dovetailMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDovetailMate, + description: l10n.puzzleThemeDovetailMateDescription, + ); + case PuzzleThemeKey.equality: + return PuzzleThemeL10n( + name: l10n.puzzleThemeEquality, + description: l10n.puzzleThemeEqualityDescription, + ); + case PuzzleThemeKey.endgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeEndgame, + description: l10n.puzzleThemeEndgameDescription, + ); + case PuzzleThemeKey.enPassant: + return PuzzleThemeL10n( + name: 'En passant', + description: l10n.puzzleThemeEnPassantDescription, + ); + case PuzzleThemeKey.exposedKing: + return PuzzleThemeL10n( + name: l10n.puzzleThemeExposedKing, + description: l10n.puzzleThemeExposedKingDescription, + ); + case PuzzleThemeKey.fork: + return PuzzleThemeL10n( + name: l10n.puzzleThemeFork, + description: l10n.puzzleThemeForkDescription, + ); + case PuzzleThemeKey.hangingPiece: + return PuzzleThemeL10n( + name: l10n.puzzleThemeHangingPiece, + description: l10n.puzzleThemeHangingPieceDescription, + ); + case PuzzleThemeKey.hookMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeHookMate, + description: l10n.puzzleThemeHookMateDescription, + ); + case PuzzleThemeKey.interference: + return PuzzleThemeL10n( + name: l10n.puzzleThemeInterference, + description: l10n.puzzleThemeInterferenceDescription, + ); + case PuzzleThemeKey.intermezzo: + return PuzzleThemeL10n( + name: l10n.puzzleThemeIntermezzo, + description: l10n.puzzleThemeIntermezzoDescription, + ); + case PuzzleThemeKey.kingsideAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeKingsideAttack, + description: l10n.puzzleThemeKingsideAttackDescription, + ); + case PuzzleThemeKey.knightEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeKnightEndgame, + description: l10n.puzzleThemeKnightEndgameDescription, + ); + case PuzzleThemeKey.long: + return PuzzleThemeL10n( + name: l10n.puzzleThemeLong, + description: l10n.puzzleThemeLongDescription, + ); + case PuzzleThemeKey.master: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMaster, + description: l10n.puzzleThemeMasterDescription, + ); + case PuzzleThemeKey.masterVsMaster: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMasterVsMaster, + description: l10n.puzzleThemeMasterVsMasterDescription, + ); + case PuzzleThemeKey.mate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMate, + description: l10n.puzzleThemeMateDescription, + ); + case PuzzleThemeKey.mateIn1: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn1, + description: l10n.puzzleThemeMateIn1Description, + ); + case PuzzleThemeKey.mateIn2: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn2, + description: l10n.puzzleThemeMateIn2Description, + ); + case PuzzleThemeKey.mateIn3: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn3, + description: l10n.puzzleThemeMateIn3Description, + ); + case PuzzleThemeKey.mateIn4: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn4, + description: l10n.puzzleThemeMateIn4Description, + ); + case PuzzleThemeKey.mateIn5: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn5, + description: l10n.puzzleThemeMateIn5Description, + ); + case PuzzleThemeKey.smotheredMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSmotheredMate, + description: l10n.puzzleThemeSmotheredMateDescription, + ); + case PuzzleThemeKey.middlegame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMiddlegame, + description: l10n.puzzleThemeMiddlegameDescription, + ); + case PuzzleThemeKey.oneMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeOneMove, + description: l10n.puzzleThemeOneMoveDescription, + ); + case PuzzleThemeKey.opening: + return PuzzleThemeL10n( + name: l10n.puzzleThemeOpening, + description: l10n.puzzleThemeOpeningDescription, + ); + case PuzzleThemeKey.pawnEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemePawnEndgame, + description: l10n.puzzleThemePawnEndgameDescription, + ); + case PuzzleThemeKey.pin: + return PuzzleThemeL10n( + name: l10n.puzzleThemePin, + description: l10n.puzzleThemePinDescription, + ); + case PuzzleThemeKey.promotion: + return PuzzleThemeL10n( + name: l10n.puzzleThemePromotion, + description: l10n.puzzleThemePromotionDescription, + ); + case PuzzleThemeKey.queenEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueenEndgame, + description: l10n.puzzleThemeQueenEndgameDescription, + ); + case PuzzleThemeKey.queenRookEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueenRookEndgame, + description: l10n.puzzleThemeQueenRookEndgameDescription, + ); + case PuzzleThemeKey.queensideAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueensideAttack, + description: l10n.puzzleThemeQueensideAttackDescription, + ); + case PuzzleThemeKey.quietMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQuietMove, + description: l10n.puzzleThemeQuietMoveDescription, + ); + case PuzzleThemeKey.rookEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeRookEndgame, + description: l10n.puzzleThemeRookEndgameDescription, + ); + case PuzzleThemeKey.sacrifice: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSacrifice, + description: l10n.puzzleThemeSacrificeDescription, + ); + case PuzzleThemeKey.short: + return PuzzleThemeL10n( + name: l10n.puzzleThemeShort, + description: l10n.puzzleThemeShortDescription, + ); + case PuzzleThemeKey.skewer: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSkewer, + description: l10n.puzzleThemeSkewerDescription, + ); + case PuzzleThemeKey.superGM: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSuperGM, + description: l10n.puzzleThemeSuperGMDescription, + ); + case PuzzleThemeKey.trappedPiece: + return PuzzleThemeL10n( + name: l10n.puzzleThemeTrappedPiece, + description: l10n.puzzleThemeTrappedPieceDescription, + ); + case PuzzleThemeKey.underPromotion: + return PuzzleThemeL10n( + name: l10n.puzzleThemeUnderPromotion, + description: l10n.puzzleThemeUnderPromotionDescription, + ); + case PuzzleThemeKey.veryLong: + return PuzzleThemeL10n( + name: l10n.puzzleThemeVeryLong, + description: l10n.puzzleThemeVeryLongDescription, + ); + case PuzzleThemeKey.xRayAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeXRayAttack, + description: l10n.puzzleThemeXRayAttackDescription, + ); + case PuzzleThemeKey.zugzwang: + return PuzzleThemeL10n( + name: l10n.puzzleThemeZugzwang, + description: l10n.puzzleThemeZugzwangDescription, + ); + } + } } -final IMap puzzleThemeNameMap = - IMap(PuzzleThemeKey.values.asNameMap()); +final IMap puzzleThemeNameMap = IMap(PuzzleThemeKey.values.asNameMap()); typedef PuzzleThemeCategory = (String, List); @Riverpod(keepAlive: true) -IList puzzleThemeCategories( - PuzzleThemeCategoriesRef ref, -) { - final l10n = ref.watch(l10nProvider); +IList puzzleThemeCategories(Ref ref) { + final l10n = ref.watch(localizationsProvider); return IList([ - ( - l10n.strings.puzzleRecommended, - [ - PuzzleThemeKey.mix, - ], - ), + (l10n.strings.puzzleRecommended, [PuzzleThemeKey.mix]), ( l10n.strings.puzzlePhases, [ @@ -191,20 +498,11 @@ IList puzzleThemeCategories( ), ( l10n.strings.puzzleLengths, - [ - PuzzleThemeKey.oneMove, - PuzzleThemeKey.short, - PuzzleThemeKey.long, - PuzzleThemeKey.veryLong, - ], + [PuzzleThemeKey.oneMove, PuzzleThemeKey.short, PuzzleThemeKey.long, PuzzleThemeKey.veryLong], ), ( l10n.strings.puzzleOrigin, - [ - PuzzleThemeKey.master, - PuzzleThemeKey.masterVsMaster, - PuzzleThemeKey.superGM, - ], + [PuzzleThemeKey.master, PuzzleThemeKey.masterVsMaster, PuzzleThemeKey.superGM], ), ]); } @@ -214,442 +512,3 @@ class PuzzleThemeL10n { final String name; final String description; } - -PuzzleThemeL10n puzzleThemeL10n(BuildContext context, PuzzleThemeKey theme) { - switch (theme) { - case PuzzleThemeKey.mix: - case PuzzleThemeKey.unsupported: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHealthyMix, - description: context.l10n.puzzleThemeHealthyMixDescription, - ); - case PuzzleThemeKey.advancedPawn: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAdvancedPawn, - description: context.l10n.puzzleThemeAdvancedPawnDescription, - ); - case PuzzleThemeKey.advantage: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAdvantage, - description: context.l10n.puzzleThemeAdvantageDescription, - ); - case PuzzleThemeKey.anastasiaMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAnastasiaMate, - description: context.l10n.puzzleThemeAnastasiaMateDescription, - ); - case PuzzleThemeKey.arabianMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeArabianMate, - description: context.l10n.puzzleThemeArabianMateDescription, - ); - case PuzzleThemeKey.attackingF2F7: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAttackingF2F7, - description: context.l10n.puzzleThemeAttackingF2F7Description, - ); - case PuzzleThemeKey.attraction: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAttraction, - description: context.l10n.puzzleThemeAttractionDescription, - ); - case PuzzleThemeKey.backRankMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBackRankMate, - description: context.l10n.puzzleThemeBackRankMateDescription, - ); - case PuzzleThemeKey.bishopEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBishopEndgame, - description: context.l10n.puzzleThemeBishopEndgameDescription, - ); - case PuzzleThemeKey.bodenMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBodenMate, - description: context.l10n.puzzleThemeBodenMateDescription, - ); - case PuzzleThemeKey.capturingDefender: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCapturingDefender, - description: context.l10n.puzzleThemeCapturingDefenderDescription, - ); - case PuzzleThemeKey.castling: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCastling, - description: context.l10n.puzzleThemeCastlingDescription, - ); - case PuzzleThemeKey.clearance: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeClearance, - description: context.l10n.puzzleThemeClearanceDescription, - ); - case PuzzleThemeKey.crushing: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCrushing, - description: context.l10n.puzzleThemeCrushingDescription, - ); - case PuzzleThemeKey.defensiveMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDefensiveMove, - description: context.l10n.puzzleThemeDefensiveMoveDescription, - ); - case PuzzleThemeKey.deflection: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDeflection, - description: context.l10n.puzzleThemeDeflectionDescription, - ); - case PuzzleThemeKey.discoveredAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDiscoveredAttack, - description: context.l10n.puzzleThemeDiscoveredAttackDescription, - ); - case PuzzleThemeKey.doubleBishopMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDoubleBishopMate, - description: context.l10n.puzzleThemeDoubleBishopMateDescription, - ); - case PuzzleThemeKey.doubleCheck: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDoubleCheck, - description: context.l10n.puzzleThemeDoubleCheckDescription, - ); - case PuzzleThemeKey.dovetailMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDovetailMate, - description: context.l10n.puzzleThemeDovetailMateDescription, - ); - case PuzzleThemeKey.equality: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeEquality, - description: context.l10n.puzzleThemeEqualityDescription, - ); - case PuzzleThemeKey.endgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeEndgame, - description: context.l10n.puzzleThemeEndgameDescription, - ); - case PuzzleThemeKey.enPassant: - return PuzzleThemeL10n( - name: 'En passant', - description: context.l10n.puzzleThemeEnPassantDescription, - ); - case PuzzleThemeKey.exposedKing: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeExposedKing, - description: context.l10n.puzzleThemeExposedKingDescription, - ); - case PuzzleThemeKey.fork: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeFork, - description: context.l10n.puzzleThemeForkDescription, - ); - case PuzzleThemeKey.hangingPiece: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHangingPiece, - description: context.l10n.puzzleThemeHangingPieceDescription, - ); - case PuzzleThemeKey.hookMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHookMate, - description: context.l10n.puzzleThemeHookMateDescription, - ); - case PuzzleThemeKey.interference: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeInterference, - description: context.l10n.puzzleThemeInterferenceDescription, - ); - case PuzzleThemeKey.intermezzo: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeIntermezzo, - description: context.l10n.puzzleThemeIntermezzoDescription, - ); - case PuzzleThemeKey.kingsideAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeKingsideAttack, - description: context.l10n.puzzleThemeKingsideAttackDescription, - ); - case PuzzleThemeKey.knightEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeKnightEndgame, - description: context.l10n.puzzleThemeKnightEndgameDescription, - ); - case PuzzleThemeKey.long: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeLong, - description: context.l10n.puzzleThemeLongDescription, - ); - case PuzzleThemeKey.master: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMaster, - description: context.l10n.puzzleThemeMasterDescription, - ); - case PuzzleThemeKey.masterVsMaster: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMasterVsMaster, - description: context.l10n.puzzleThemeMasterVsMasterDescription, - ); - case PuzzleThemeKey.mate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMate, - description: context.l10n.puzzleThemeMateDescription, - ); - case PuzzleThemeKey.mateIn1: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn1, - description: context.l10n.puzzleThemeMateIn1Description, - ); - case PuzzleThemeKey.mateIn2: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn2, - description: context.l10n.puzzleThemeMateIn2Description, - ); - case PuzzleThemeKey.mateIn3: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn3, - description: context.l10n.puzzleThemeMateIn3Description, - ); - case PuzzleThemeKey.mateIn4: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn4, - description: context.l10n.puzzleThemeMateIn4Description, - ); - case PuzzleThemeKey.mateIn5: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn5, - description: context.l10n.puzzleThemeMateIn5Description, - ); - case PuzzleThemeKey.smotheredMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSmotheredMate, - description: context.l10n.puzzleThemeSmotheredMateDescription, - ); - case PuzzleThemeKey.middlegame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMiddlegame, - description: context.l10n.puzzleThemeMiddlegameDescription, - ); - case PuzzleThemeKey.oneMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeOneMove, - description: context.l10n.puzzleThemeOneMoveDescription, - ); - case PuzzleThemeKey.opening: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeOpening, - description: context.l10n.puzzleThemeOpeningDescription, - ); - case PuzzleThemeKey.pawnEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePawnEndgame, - description: context.l10n.puzzleThemePawnEndgameDescription, - ); - case PuzzleThemeKey.pin: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePin, - description: context.l10n.puzzleThemePinDescription, - ); - case PuzzleThemeKey.promotion: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePromotion, - description: context.l10n.puzzleThemePromotionDescription, - ); - case PuzzleThemeKey.queenEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueenEndgame, - description: context.l10n.puzzleThemeQueenEndgameDescription, - ); - case PuzzleThemeKey.queenRookEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueenRookEndgame, - description: context.l10n.puzzleThemeQueenRookEndgameDescription, - ); - case PuzzleThemeKey.queensideAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueensideAttack, - description: context.l10n.puzzleThemeQueensideAttackDescription, - ); - case PuzzleThemeKey.quietMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQuietMove, - description: context.l10n.puzzleThemeQuietMoveDescription, - ); - case PuzzleThemeKey.rookEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeRookEndgame, - description: context.l10n.puzzleThemeRookEndgameDescription, - ); - case PuzzleThemeKey.sacrifice: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSacrifice, - description: context.l10n.puzzleThemeSacrificeDescription, - ); - case PuzzleThemeKey.short: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeShort, - description: context.l10n.puzzleThemeShortDescription, - ); - case PuzzleThemeKey.skewer: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSkewer, - description: context.l10n.puzzleThemeSkewerDescription, - ); - case PuzzleThemeKey.superGM: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSuperGM, - description: context.l10n.puzzleThemeSuperGMDescription, - ); - case PuzzleThemeKey.trappedPiece: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeTrappedPiece, - description: context.l10n.puzzleThemeTrappedPieceDescription, - ); - case PuzzleThemeKey.underPromotion: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeUnderPromotion, - description: context.l10n.puzzleThemeUnderPromotionDescription, - ); - case PuzzleThemeKey.veryLong: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeVeryLong, - description: context.l10n.puzzleThemeVeryLongDescription, - ); - case PuzzleThemeKey.xRayAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeXRayAttack, - description: context.l10n.puzzleThemeXRayAttackDescription, - ); - case PuzzleThemeKey.zugzwang: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeZugzwang, - description: context.l10n.puzzleThemeZugzwangDescription, - ); - } -} - -IconData puzzleThemeIcon(PuzzleThemeKey theme) { - switch (theme) { - case PuzzleThemeKey.mix: - case PuzzleThemeKey.unsupported: - return PuzzleIcons.mix; - case PuzzleThemeKey.advancedPawn: - return PuzzleIcons.advancedPawn; - case PuzzleThemeKey.advantage: - return PuzzleIcons.advantage; - case PuzzleThemeKey.anastasiaMate: - return PuzzleIcons.anastasiaMate; - case PuzzleThemeKey.arabianMate: - return PuzzleIcons.arabianMate; - case PuzzleThemeKey.attackingF2F7: - return PuzzleIcons.attackingF2F7; - case PuzzleThemeKey.attraction: - return PuzzleIcons.attraction; - case PuzzleThemeKey.backRankMate: - return PuzzleIcons.backRankMate; - case PuzzleThemeKey.bishopEndgame: - return PuzzleIcons.bishopEndgame; - case PuzzleThemeKey.bodenMate: - return PuzzleIcons.bodenMate; - case PuzzleThemeKey.capturingDefender: - return PuzzleIcons.capturingDefender; - case PuzzleThemeKey.castling: - return PuzzleIcons.castling; - case PuzzleThemeKey.clearance: - return PuzzleIcons.clearance; - case PuzzleThemeKey.crushing: - return PuzzleIcons.crushing; - case PuzzleThemeKey.defensiveMove: - return PuzzleIcons.defensiveMove; - case PuzzleThemeKey.deflection: - return PuzzleIcons.deflection; - case PuzzleThemeKey.discoveredAttack: - return PuzzleIcons.discoveredAttack; - case PuzzleThemeKey.doubleBishopMate: - return PuzzleIcons.doubleBishopMate; - case PuzzleThemeKey.doubleCheck: - return PuzzleIcons.doubleCheck; - case PuzzleThemeKey.dovetailMate: - return PuzzleIcons.dovetailMate; - case PuzzleThemeKey.equality: - return PuzzleIcons.equality; - case PuzzleThemeKey.endgame: - return PuzzleIcons.endgame; - case PuzzleThemeKey.enPassant: - return PuzzleIcons.enPassant; - case PuzzleThemeKey.exposedKing: - return PuzzleIcons.exposedKing; - case PuzzleThemeKey.fork: - return PuzzleIcons.fork; - case PuzzleThemeKey.hangingPiece: - return PuzzleIcons.hangingPiece; - case PuzzleThemeKey.hookMate: - return PuzzleIcons.hookMate; - case PuzzleThemeKey.interference: - return PuzzleIcons.interference; - case PuzzleThemeKey.intermezzo: - return PuzzleIcons.intermezzo; - case PuzzleThemeKey.kingsideAttack: - return PuzzleIcons.kingsideAttack; - case PuzzleThemeKey.knightEndgame: - return PuzzleIcons.knightEndgame; - case PuzzleThemeKey.long: - return PuzzleIcons.long; - case PuzzleThemeKey.master: - return PuzzleIcons.master; - case PuzzleThemeKey.masterVsMaster: - return PuzzleIcons.masterVsMaster; - case PuzzleThemeKey.mate: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn1: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn2: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn3: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn4: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn5: - return PuzzleIcons.mate; - case PuzzleThemeKey.smotheredMate: - return PuzzleIcons.smotheredMate; - case PuzzleThemeKey.middlegame: - return PuzzleIcons.middlegame; - case PuzzleThemeKey.oneMove: - return PuzzleIcons.oneMove; - case PuzzleThemeKey.opening: - return PuzzleIcons.opening; - case PuzzleThemeKey.pawnEndgame: - return PuzzleIcons.pawnEndgame; - case PuzzleThemeKey.pin: - return PuzzleIcons.pin; - case PuzzleThemeKey.promotion: - return PuzzleIcons.promotion; - case PuzzleThemeKey.queenEndgame: - return PuzzleIcons.queenEndgame; - case PuzzleThemeKey.queenRookEndgame: - return PuzzleIcons.queenRookEndgame; - case PuzzleThemeKey.queensideAttack: - return PuzzleIcons.queensideAttack; - case PuzzleThemeKey.quietMove: - return PuzzleIcons.quietMove; - case PuzzleThemeKey.rookEndgame: - return PuzzleIcons.rookEndgame; - case PuzzleThemeKey.sacrifice: - return PuzzleIcons.sacrifice; - case PuzzleThemeKey.short: - return PuzzleIcons.short; - case PuzzleThemeKey.skewer: - return PuzzleIcons.skewer; - case PuzzleThemeKey.superGM: - return PuzzleIcons.superGM; - case PuzzleThemeKey.trappedPiece: - return PuzzleIcons.trappedPiece; - case PuzzleThemeKey.underPromotion: - return PuzzleIcons.underPromotion; - case PuzzleThemeKey.veryLong: - return PuzzleIcons.veryLong; - case PuzzleThemeKey.xRayAttack: - return PuzzleIcons.xRayAttack; - case PuzzleThemeKey.zugzwang: - return PuzzleIcons.zugzwang; - } -} diff --git a/lib/src/model/puzzle/storm.dart b/lib/src/model/puzzle/storm.dart index 105a1c4093..949b8a9840 100644 --- a/lib/src/model/puzzle/storm.dart +++ b/lib/src/model/puzzle/storm.dart @@ -36,11 +36,12 @@ class StormRunStats with _$StormRunStats { IList historyFilter(StormFilter filter) { return history .where( - (e) => (filter.slow && filter.failed) - ? (!e.win && slowPuzzleIds.any((id) => id == e.id)) - : (filter.slow - ? slowPuzzleIds.any((id) => id == e.id) - : (!filter.failed || !e.win)), + (e) => + (filter.slow && filter.failed) + ? (!e.win && slowPuzzleIds.any((id) => id == e.id)) + : (filter.slow + ? slowPuzzleIds.any((id) => id == e.id) + : (!filter.failed || !e.win)), ) .toIList(); } @@ -56,12 +57,7 @@ class StormFilter { StormFilter(slow: slow ?? this.slow, failed: failed ?? this.failed); } -enum StormNewHighType { - day, - week, - month, - allTime, -} +enum StormNewHighType { day, week, month, allTime } @freezed class StormDashboard with _$StormDashboard { @@ -84,14 +80,12 @@ class StormDayScore with _$StormDayScore { @freezed class StormNewHigh with _$StormNewHigh { - const factory StormNewHigh({ - required StormNewHighType key, - required int prev, - }) = _StormNewHigh; + const factory StormNewHigh({required StormNewHighType key, required int prev}) = _StormNewHigh; } -final IMap stormNewHighTypeMap = - IMap(StormNewHighType.values.asNameMap()); +final IMap stormNewHighTypeMap = IMap( + StormNewHighType.values.asNameMap(), +); extension StormExtension on Pick { StormNewHighType asStormNewHighTypeOrThrow() { @@ -104,9 +98,7 @@ extension StormExtension on Pick { return stormNewHighTypeMap[value]!; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to StormNewHighType", - ); + throw PickException("value $value at $debugParsingExit can't be casted to StormNewHighType"); } StormNewHighType? asStormNewHighTypeOrNull() { diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index 634e203ea8..edd18949f8 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -3,15 +3,15 @@ import 'dart:core'; import 'dart:math' as math; import 'package:async/async.dart'; -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -76,9 +76,15 @@ class StormController extends _$StormController { return newState; } - Future onUserMove(Move move) async { + Future onUserMove(NormalMove move) async { if (state.clock.endAt != null) return; state.clock.start(); + + if (isPromotionPawnMove(state.position, move)) { + state = state.copyWith(promotionMove: move); + return; + } + final expected = state.expectedMove; _addMove(move, ComboState.noChange, runStarted: true, userMove: true); state = state.copyWith(moves: state.moves + 1); @@ -99,12 +105,7 @@ class StormController extends _$StormController { } await Future.delayed(moveDelay); - _addMove( - state.expectedMove!, - ComboState.increase, - runStarted: true, - userMove: false, - ); + _addMove(state.expectedMove!, ComboState.increase, runStarted: true, userMove: false); } else { state = state.copyWith(errors: state.errors + 1); ref.read(soundServiceProvider).play(Sound.error); @@ -119,10 +120,16 @@ class StormController extends _$StormController { } } - void setPremove(cg.Move? move) { - state = state.copyWith( - premove: move, - ); + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + onUserMove(move); + } } Future end() async { @@ -137,16 +144,11 @@ class StormController extends _$StormController { if (session != null) { final res = await ref.withClient( (client) => Result.capture( - PuzzleRepository(client) - .postStormRun(stats) - .timeout(const Duration(seconds: 2)), + PuzzleRepository(client).postStormRun(stats).timeout(const Duration(seconds: 2)), ), ); - final newState = state.copyWith( - stats: stats, - mode: StormMode.ended, - ); + final newState = state.copyWith(stats: stats, mode: StormMode.ended); res.match( onSuccess: (newHigh) { @@ -161,10 +163,7 @@ class StormController extends _$StormController { }, ); } else { - state = state.copyWith( - stats: stats, - mode: StormMode.ended, - ); + state = state.copyWith(stats: stats, mode: StormMode.ended); } } @@ -194,12 +193,7 @@ class StormController extends _$StormController { ), ); await Future.delayed(moveDelay); - _addMove( - state.expectedMove!, - ComboState.noChange, - runStarted: true, - userMove: false, - ); + _addMove(state.expectedMove!, ComboState.noChange, runStarted: true, userMove: false); } void _addMove( @@ -228,25 +222,20 @@ class StormController extends _$StormController { current: newComboCurrent, best: math.max(state.combo.best, state.combo.current + 1), ), + promotionMove: null, ); - Future.delayed( - userMove ? Duration.zero : const Duration(milliseconds: 250), () { + Future.delayed(userMove ? Duration.zero : const Duration(milliseconds: 250), () { if (pos.board.pieceAt(move.to) != null) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: state.position.isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: state.position.isCheck); } else { - ref - .read(moveFeedbackServiceProvider) - .moveFeedback(check: state.position.isCheck); + ref.read(moveFeedbackServiceProvider).moveFeedback(check: state.position.isCheck); } }); } StormRunStats _getStats() { final wins = state.history.where((e) => e.win == true).toList(); - final mean = state.history.sumBy((e) => e.solvingTime!.inSeconds) / - state.history.length; + final mean = state.history.sumBy((e) => e.solvingTime!.inSeconds) / state.history.length; final threshold = mean * 1.5; return StormRunStats( moves: state.moves, @@ -255,23 +244,26 @@ class StormController extends _$StormController { comboBest: state.combo.best, time: state.clock.endAt!, timePerMove: mean, - highest: wins.isNotEmpty - ? wins.map((e) => e.rating).reduce( - (maxRating, rating) => rating > maxRating ? rating : maxRating, - ) - : 0, + highest: + wins.isNotEmpty + ? wins + .map((e) => e.rating) + .reduce((maxRating, rating) => rating > maxRating ? rating : maxRating) + : 0, history: state.history, - slowPuzzleIds: state.history - .where((e) => e.solvingTime!.inSeconds > threshold) - .map((e) => e.id) - .toIList(), + slowPuzzleIds: + state.history + .where((e) => e.solvingTime!.inSeconds > threshold) + .map((e) => e.id) + .toIList(), ); } void _pushToHistory({required bool success}) { - final timeTaken = state.lastSolvedTime != null - ? DateTime.now().difference(state.lastSolvedTime!) - : DateTime.now().difference(state.clock.startAt!); + final timeTaken = + state.lastSolvedTime != null + ? DateTime.now().difference(state.lastSolvedTime!) + : DateTime.now().difference(state.clock.startAt!); state = state.copyWith( history: state.history.add( PuzzleHistoryEntry.fromLitePuzzle(state.puzzle, success, timeTaken), @@ -334,18 +326,17 @@ class StormState with _$StormState { /// bool to indicate that the first move has been played required bool firstMovePlayed, - /// premove to be played - cg.Move? premove, + /// Promotion move to be selected + NormalMove? promotionMove, }) = _StormState; - Move? get expectedMove => Move.fromUci(puzzle.solution[moveIndex + 1]); + Move? get expectedMove => Move.parse(puzzle.solution[moveIndex + 1]); - Move? get lastMove => - moveIndex == -1 ? null : Move.fromUci(puzzle.solution[moveIndex]); + Move? get lastMove => moveIndex == -1 ? null : Move.parse(puzzle.solution[moveIndex]); bool get isOver => moveIndex >= puzzle.solution.length - 1; - IMap> get validMoves => algebraicLegalMoves(position); + IMap> get validMoves => makeLegalMoves(position); } enum StormMode { initial, running, ended } @@ -357,10 +348,7 @@ enum ComboState { increase, reset, noChange } class StormCombo with _$StormCombo { const StormCombo._(); - const factory StormCombo({ - required int current, - required int best, - }) = _StormCombo; + const factory StormCombo({required int current, required int best}) = _StormCombo; /// List representing the bonus awared at each level static const levelBonus = [3, 5, 6, 10]; @@ -381,8 +369,7 @@ class StormCombo with _$StormCombo { /// Returns the level of the `current + 1` combo count int nextLevel() { - final lvl = - levelsAndBonus.indexWhere((element) => element.level > current + 1); + final lvl = levelsAndBonus.indexWhere((element) => element.level > current + 1); return lvl >= 0 ? lvl - 1 : levelsAndBonus.length - 1; } @@ -394,8 +381,7 @@ class StormCombo with _$StormCombo { final lvl = getNext ? nextLevel() : currentLevel(); final lastLevel = levelsAndBonus.last; if (lvl >= levelsAndBonus.length - 1) { - final range = - lastLevel.level - levelsAndBonus[levelsAndBonus.length - 2].level; + final range = lastLevel.level - levelsAndBonus[levelsAndBonus.length - 2].level; return (((currentCombo - lastLevel.level) / range) * 100) % 100; } final bounds = [levelsAndBonus[lvl].level, levelsAndBonus[lvl + 1].level]; diff --git a/lib/src/model/puzzle/streak_storage.dart b/lib/src/model/puzzle/streak_storage.dart new file mode 100644 index 0000000000..a28e6519a5 --- /dev/null +++ b/lib/src/model/puzzle/streak_storage.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'streak_storage.g.dart'; + +@Riverpod(keepAlive: true) +StreakStorage streakStorage(Ref ref, UserId? userId) { + return StreakStorage(userId); +} + +/// Local storage for the current puzzle streak. +class StreakStorage { + const StreakStorage(this.userId); + final UserId? userId; + + Future loadActiveStreak() async { + final stored = _store.getString(_storageKey); + if (stored == null) { + return null; + } + + return PuzzleStreak.fromJson(jsonDecode(stored) as Map); + } + + Future saveActiveStreak(PuzzleStreak streak) async { + await _store.setString(_storageKey, jsonEncode(streak)); + } + + Future clearActiveStreak() async { + await _store.remove(_storageKey); + } + + SharedPreferencesWithCache get _store => LichessBinding.instance.sharedPreferences; + + String get _storageKey => 'puzzle_streak.${userId ?? '**anon**'}'; +} diff --git a/lib/src/model/relation/online_friends.dart b/lib/src/model/relation/online_friends.dart index 7997c8b37c..be5f41498f 100644 --- a/lib/src/model/relation/online_friends.dart +++ b/lib/src/model/relation/online_friends.dart @@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'online_friends.g.dart'; @@ -21,9 +22,7 @@ class OnlineFriends extends _$OnlineFriends { final state = _socketClient.stream .firstWhere((e) => e.topic == 'following_onlines') - .then( - (event) => _parseFriendsList(event.data as List), - ); + .then((event) => _parseFriendsList(event.data as List)); await _socketClient.firstConnection; @@ -69,9 +68,7 @@ class OnlineFriends extends _$OnlineFriends { switch (event.topic) { case 'following_onlines': - state = AsyncValue.data( - _parseFriendsList(event.data as List), - ); + state = AsyncValue.data(_parseFriendsList(event.data as List)); case 'following_enters': final data = _parseFriend(event.data.toString()); @@ -79,9 +76,7 @@ class OnlineFriends extends _$OnlineFriends { case 'following_leaves': final data = _parseFriend(event.data.toString()); - state = AsyncValue.data( - state.requireValue.removeWhere((v) => v.id == data.id), - ); + state = AsyncValue.data(state.requireValue.removeWhere((v) => v.id == data.id)); } } @@ -91,11 +86,7 @@ class OnlineFriends extends _$OnlineFriends { final splitted = friend.split(' '); final name = splitted.length > 1 ? splitted[1] : splitted[0]; final title = splitted.length > 1 ? splitted[0] : null; - return LightUser( - id: UserId.fromUserName(name), - name: name, - title: title, - ); + return LightUser(id: UserId.fromUserName(name), name: name, title: title); } IList _parseFriendsList(List friends) { diff --git a/lib/src/model/relation/relation_repository.dart b/lib/src/model/relation/relation_repository.dart index e2baf82e5d..218babb3d3 100644 --- a/lib/src/model/relation/relation_repository.dart +++ b/lib/src/model/relation/relation_repository.dart @@ -1,8 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; class RelationRepository { const RelationRepository(this.client); @@ -22,10 +22,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to follow user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to follow user: ${response.statusCode}', uri); } } @@ -34,10 +31,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to unfollow user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to unfollow user: ${response.statusCode}', uri); } } @@ -46,10 +40,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to block user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to block user: ${response.statusCode}', uri); } } @@ -58,10 +49,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to unblock user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to unblock user: ${response.statusCode}', uri); } } } diff --git a/lib/src/model/relation/relation_repository_providers.dart b/lib/src/model/relation/relation_repository_providers.dart index 9357213429..7f20842f26 100644 --- a/lib/src/model/relation/relation_repository_providers.dart +++ b/lib/src/model/relation/relation_repository_providers.dart @@ -1,6 +1,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'relation_repository.dart'; @@ -8,7 +9,7 @@ import 'relation_repository.dart'; part 'relation_repository_providers.g.dart'; @riverpod -Future> following(FollowingRef ref) async { +Future> following(Ref ref) async { return ref.withClientCacheFor( (client) => RelationRepository(client).getFollowing(), const Duration(hours: 1), diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 9706fd8597..7cc12cafd0 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,104 +1,113 @@ -import 'dart:convert'; - import 'package:chessground/chessground.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'board_preferences.freezed.dart'; part 'board_preferences.g.dart'; -@Riverpod(keepAlive: true) -class BoardPreferences extends _$BoardPreferences { - static const String prefKey = 'preferences.board'; +const kBoardDefaultBrightnessFilter = 1.0; +const kBoardDefaultHueFilter = 0.0; + +@riverpod +class BoardPreferences extends _$BoardPreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + PrefCategory get prefCategory => PrefCategory.board; + + // ignore: avoid_public_notifier_properties + @override + BoardPrefs get defaults => BoardPrefs.defaults; + + @override + BoardPrefs fromJson(Map json) => BoardPrefs.fromJson(json); @override BoardPrefs build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); - return stored != null - ? BoardPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : BoardPrefs.defaults; + return fetch(); } Future setPieceSet(PieceSet pieceSet) { - return _save(state.copyWith(pieceSet: pieceSet)); + return save(state.copyWith(pieceSet: pieceSet)); } Future setBoardTheme(BoardTheme boardTheme) async { - await _save(state.copyWith(boardTheme: boardTheme)); + await save(state.copyWith(boardTheme: boardTheme)); } Future setPieceShiftMethod(PieceShiftMethod pieceShiftMethod) async { - await _save(state.copyWith(pieceShiftMethod: pieceShiftMethod)); + await save(state.copyWith(pieceShiftMethod: pieceShiftMethod)); } Future toggleHapticFeedback() { - return _save(state.copyWith(hapticFeedback: !state.hapticFeedback)); + return save(state.copyWith(hapticFeedback: !state.hapticFeedback)); } Future toggleImmersiveModeWhilePlaying() { - return _save( - state.copyWith( - immersiveModeWhilePlaying: !(state.immersiveModeWhilePlaying ?? false), - ), + return save( + state.copyWith(immersiveModeWhilePlaying: !(state.immersiveModeWhilePlaying ?? false)), ); } Future toggleShowLegalMoves() { - return _save(state.copyWith(showLegalMoves: !state.showLegalMoves)); + return save(state.copyWith(showLegalMoves: !state.showLegalMoves)); } Future toggleBoardHighlights() { - return _save(state.copyWith(boardHighlights: !state.boardHighlights)); + return save(state.copyWith(boardHighlights: !state.boardHighlights)); } Future toggleCoordinates() { - return _save(state.copyWith(coordinates: !state.coordinates)); + return save(state.copyWith(coordinates: !state.coordinates)); + } + + Future toggleBorder() { + return save(state.copyWith(showBorder: !state.showBorder)); } Future togglePieceAnimation() { - return _save(state.copyWith(pieceAnimation: !state.pieceAnimation)); + return save(state.copyWith(pieceAnimation: !state.pieceAnimation)); } Future toggleMagnifyDraggedPiece() { - return _save( - state.copyWith( - magnifyDraggedPiece: !state.magnifyDraggedPiece, - ), - ); + return save(state.copyWith(magnifyDraggedPiece: !state.magnifyDraggedPiece)); } - Future toggleShowMaterialDifference() { - return _save( - state.copyWith(showMaterialDifference: !state.showMaterialDifference), - ); + Future setDragTargetKind(DragTargetKind dragTargetKind) { + return save(state.copyWith(dragTargetKind: dragTargetKind)); + } + + Future setMaterialDifferenceFormat(MaterialDifferenceFormat materialDifferenceFormat) { + return save(state.copyWith(materialDifferenceFormat: materialDifferenceFormat)); + } + + Future setClockPosition(ClockPosition clockPosition) { + return save(state.copyWith(clockPosition: clockPosition)); } Future toggleEnableShapeDrawings() { - return _save( - state.copyWith(enableShapeDrawings: !state.enableShapeDrawings), - ); + return save(state.copyWith(enableShapeDrawings: !state.enableShapeDrawings)); } - Future _save(BoardPrefs newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; + Future setShapeColor(ShapeColor shapeColor) { + return save(state.copyWith(shapeColor: shapeColor)); + } + + Future adjustColors({double? brightness, double? hue}) { + return save(state.copyWith(brightness: brightness ?? state.brightness, hue: hue ?? state.hue)); } } @Freezed(fromJson: true, toJson: true) -class BoardPrefs with _$BoardPrefs { +class BoardPrefs with _$BoardPrefs implements Serializable { const BoardPrefs._(); + @Assert('brightness >= 0.2 && brightness <= 1.4, hue >= 0.0 && hue <= 360.0') const factory BoardPrefs({ required PieceSet pieceSet, required BoardTheme boardTheme, @@ -108,16 +117,25 @@ class BoardPrefs with _$BoardPrefs { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - required bool showMaterialDifference, @JsonKey( - defaultValue: PieceShiftMethod.either, - unknownEnumValue: PieceShiftMethod.either, + defaultValue: MaterialDifferenceFormat.materialDifference, + unknownEnumValue: MaterialDifferenceFormat.materialDifference, ) + required MaterialDifferenceFormat materialDifferenceFormat, + required ClockPosition clockPosition, + @JsonKey(defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either) required PieceShiftMethod pieceShiftMethod, /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey(defaultValue: DragTargetKind.circle, unknownEnumValue: DragTargetKind.circle) + required DragTargetKind dragTargetKind, + @JsonKey(defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green) + required ShapeColor shapeColor, + @JsonKey(defaultValue: false) required bool showBorder, + @JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double brightness, + @JsonKey(defaultValue: kBoardDefaultHueFilter) required double hue, }) = _BoardPrefs; static const defaults = BoardPrefs( @@ -129,144 +147,208 @@ class BoardPrefs with _$BoardPrefs { boardHighlights: true, coordinates: true, pieceAnimation: true, - showMaterialDifference: true, + materialDifferenceFormat: MaterialDifferenceFormat.materialDifference, + clockPosition: ClockPosition.right, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, + dragTargetKind: DragTargetKind.circle, + shapeColor: ShapeColor.green, + showBorder: false, + brightness: kBoardDefaultBrightnessFilter, + hue: kBoardDefaultHueFilter, ); - BoardSettings toBoardSettings() { - return BoardSettings( + bool get hasColorAdjustments => + brightness != kBoardDefaultBrightnessFilter || hue != kBoardDefaultHueFilter; + + ChessboardSettings toBoardSettings() { + return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, + brightness: brightness, + hue: hue, + border: + showBorder + ? BoardBorder(color: darken(boardTheme.colors.darkSquare, 0.2), width: 16.0) + : null, showValidMoves: showLegalMoves, showLastMove: boardHighlights, enableCoordinates: coordinates, animationDuration: pieceAnimationDuration, - dragFeedbackSize: magnifyDraggedPiece ? 2.0 : 1.0, + dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), + dragTargetKind: dragTargetKind, pieceShiftMethod: pieceShiftMethod, - drawShape: DrawShapeOptions( - enable: enableShapeDrawings, - ), + drawShape: DrawShapeOptions(enable: enableShapeDrawings, newShapeColor: shapeColor.color), ); } factory BoardPrefs.fromJson(Map json) { - try { - return _$BoardPrefsFromJson(json); - } catch (_) { - return defaults; - } + return _$BoardPrefsFromJson(json); } Duration get pieceAnimationDuration => pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; } +/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 +enum ShapeColor { + green, + red, + blue, + yellow; + + Color get color => Color(switch (this) { + ShapeColor.green => 0x15781B, + ShapeColor.red => 0x882020, + ShapeColor.blue => 0x003088, + ShapeColor.yellow => 0xe68f00, + }).withAlpha(0xAA); +} + /// The chessboard theme. enum BoardTheme { - system('System'), - blue('Blue'), - blue2('Blue2'), - blue3('Blue3'), - blueMarble('Blue Marble'), - canvas('Canvas'), - wood('Wood'), - wood2('Wood2'), - wood3('Wood3'), - wood4('Wood4'), - maple('Maple'), - maple2('Maple 2'), - brown('Brown'), - leather('Leather'), - green('Green'), - marble('Marble'), - greenPlastic('Green Plastic'), - grey('Grey'), - metal('Metal'), - olive('Olive'), - newspaper('Newspaper'), - purpleDiag('Purple-Diag'), - pinkPyramid('Pink'), - horsey('Horsey'); + system('System', 'system'), + blue('Blue', 'blue'), + blue2('Blue 2', 'blue2'), + blue3('Blue 3', 'blue3'), + blueMarble('Blue Marble', 'blue-marble'), + canvas('Canvas', 'canvas'), + wood('Wood', 'wood'), + wood2('Wood 2', 'wood2'), + wood3('Wood 3', 'wood3'), + wood4('Wood 4', 'wood4'), + maple('Maple', 'maple'), + maple2('Maple 2', 'maple2'), + brown('Brown', 'brown'), + leather('Leather', 'leather'), + green('Green', 'green'), + marble('Marble', 'marble'), + greenPlastic('Green Plastic', 'green-plastic'), + grey('Grey', 'grey'), + metal('Metal', 'metal'), + olive('Olive', 'olive'), + newspaper('Newspaper', 'newspaper'), + purpleDiag('Purple-Diag', 'purple-diag'), + pinkPyramid('Pink', 'pink'), + horsey('Horsey', 'horsey'); final String label; + final String gifApiName; - const BoardTheme(this.label); + const BoardTheme(this.label, this.gifApiName); - BoardColorScheme get colors { + ChessboardColorScheme get colors { switch (this) { case BoardTheme.system: - return getBoardColorScheme() ?? BoardColorScheme.brown; + return getBoardColorScheme() ?? ChessboardColorScheme.brown; case BoardTheme.blue: - return BoardColorScheme.blue; + return ChessboardColorScheme.blue; case BoardTheme.blue2: - return BoardColorScheme.blue2; + return ChessboardColorScheme.blue2; case BoardTheme.blue3: - return BoardColorScheme.blue3; + return ChessboardColorScheme.blue3; case BoardTheme.blueMarble: - return BoardColorScheme.blueMarble; + return ChessboardColorScheme.blueMarble; case BoardTheme.canvas: - return BoardColorScheme.canvas; + return ChessboardColorScheme.canvas; case BoardTheme.wood: - return BoardColorScheme.wood; + return ChessboardColorScheme.wood; case BoardTheme.wood2: - return BoardColorScheme.wood2; + return ChessboardColorScheme.wood2; case BoardTheme.wood3: - return BoardColorScheme.wood3; + return ChessboardColorScheme.wood3; case BoardTheme.wood4: - return BoardColorScheme.wood4; + return ChessboardColorScheme.wood4; case BoardTheme.maple: - return BoardColorScheme.maple; + return ChessboardColorScheme.maple; case BoardTheme.maple2: - return BoardColorScheme.maple2; + return ChessboardColorScheme.maple2; case BoardTheme.brown: - return BoardColorScheme.brown; + return ChessboardColorScheme.brown; case BoardTheme.leather: - return BoardColorScheme.leather; + return ChessboardColorScheme.leather; case BoardTheme.green: - return BoardColorScheme.green; + return ChessboardColorScheme.green; case BoardTheme.marble: - return BoardColorScheme.marble; + return ChessboardColorScheme.marble; case BoardTheme.greenPlastic: - return BoardColorScheme.greenPlastic; + return ChessboardColorScheme.greenPlastic; case BoardTheme.grey: - return BoardColorScheme.grey; + return ChessboardColorScheme.grey; case BoardTheme.metal: - return BoardColorScheme.metal; + return ChessboardColorScheme.metal; case BoardTheme.olive: - return BoardColorScheme.olive; + return ChessboardColorScheme.olive; case BoardTheme.newspaper: - return BoardColorScheme.newspaper; + return ChessboardColorScheme.newspaper; case BoardTheme.purpleDiag: - return BoardColorScheme.purpleDiag; + return ChessboardColorScheme.purpleDiag; case BoardTheme.pinkPyramid: - return BoardColorScheme.pinkPyramid; + return ChessboardColorScheme.pinkPyramid; case BoardTheme.horsey: - return BoardColorScheme.horsey; + return ChessboardColorScheme.horsey; } } - Widget get thumbnail => this == BoardTheme.system - ? SizedBox( - height: 44, - width: 44 * 6, - child: Row( - children: [ - for (final c in const [1, 2, 3, 4, 5, 6]) - Container( - width: 44, - color: c.isEven - ? BoardTheme.system.colors.darkSquare - : BoardTheme.system.colors.lightSquare, - ), - ], - ), - ) - : Image.asset( - 'assets/board-thumbnails/$name.jpg', - height: 44, - errorBuilder: (context, o, st) => const SizedBox.shrink(), - ); + Widget get thumbnail => + this == BoardTheme.system + ? SizedBox( + height: 44, + width: 44 * 6, + child: Row( + children: [ + for (final c in const [1, 2, 3, 4, 5, 6]) + Container( + width: 44, + color: + c.isEven + ? BoardTheme.system.colors.darkSquare + : BoardTheme.system.colors.lightSquare, + ), + ], + ), + ) + : Image.asset( + 'assets/board-thumbnails/$name.jpg', + height: 44, + errorBuilder: (context, o, st) => const SizedBox.shrink(), + ); +} + +enum MaterialDifferenceFormat { + materialDifference(label: 'Material difference'), + capturedPieces(label: 'Captured pieces'), + hidden(label: 'Hidden'); + + const MaterialDifferenceFormat({required this.label}); + + final String label; + + bool get visible => this != MaterialDifferenceFormat.hidden; + + String l10n(AppLocalizations l10n) => switch (this) { + //TODO: Add l10n + MaterialDifferenceFormat.materialDifference => materialDifference.label, + MaterialDifferenceFormat.capturedPieces => capturedPieces.label, + MaterialDifferenceFormat.hidden => hidden.label, + }; } + +enum ClockPosition { + left, + right; + + // TODO: l10n + String get label => switch (this) { + ClockPosition.left => 'Left', + ClockPosition.right => 'Right', + }; +} + +String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { + DragTargetKind.circle => 'Circle', + DragTargetKind.square => 'Square', + DragTargetKind.none => 'None', +}; diff --git a/lib/src/model/settings/brightness.dart b/lib/src/model/settings/brightness.dart index 163de4c988..6cc6f93b6c 100644 --- a/lib/src/model/settings/brightness.dart +++ b/lib/src/model/settings/brightness.dart @@ -1,33 +1,29 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show Brightness; +import 'package:flutter/widgets.dart' show WidgetsBinding; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'brightness.g.dart'; -@Riverpod(keepAlive: true) +@riverpod class CurrentBrightness extends _$CurrentBrightness { @override Brightness build() { - final themeMode = ref.watch( - generalPreferencesProvider.select( - (state) => state.themeMode, - ), - ); + final themeMode = ref.watch(generalPreferencesProvider.select((state) => state.themeMode)); - WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = - () { + WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () { WidgetsBinding.instance.handlePlatformBrightnessChanged(); - if (themeMode == ThemeMode.system) { + if (themeMode == BackgroundThemeMode.system) { state = WidgetsBinding.instance.platformDispatcher.platformBrightness; } }; switch (themeMode) { - case ThemeMode.dark: + case BackgroundThemeMode.dark: return Brightness.dark; - case ThemeMode.light: + case BackgroundThemeMode.light: return Brightness.light; - case ThemeMode.system: + case BackgroundThemeMode.system: return WidgetsBinding.instance.platformDispatcher.platformBrightness; } } diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 44cd15bb94..7fa2643f2d 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,137 +1,118 @@ -import 'dart:convert'; +import 'dart:ui' show Locale; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; part 'general_preferences.freezed.dart'; part 'general_preferences.g.dart'; -const _prefKey = 'preferences.general'; - -@Riverpod(keepAlive: true) -class GeneralPreferences extends _$GeneralPreferences { - static GeneralPrefsState fetchFromStorage(SharedPreferences prefs) { - final stored = prefs.getString(_prefKey); - return stored != null - ? GeneralPrefsState.fromJson( - jsonDecode(stored) as Map, - ) - : GeneralPrefsState.defaults; - } +@riverpod +class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.general; + // ignore: avoid_public_notifier_properties @override - GeneralPrefsState build() { - final prefs = ref.watch(sharedPreferencesProvider); - return fetchFromStorage(prefs); + GeneralPrefs get defaults => GeneralPrefs.defaults; + + @override + GeneralPrefs fromJson(Map json) => GeneralPrefs.fromJson(json); + + @override + GeneralPrefs build() { + return fetch(); } - Future setThemeMode(ThemeMode themeMode) { - return _save(state.copyWith(themeMode: themeMode)); + Future setBackgroundThemeMode(BackgroundThemeMode themeMode) { + return save(state.copyWith(themeMode: themeMode)); } Future toggleSoundEnabled() { - return _save(state.copyWith(isSoundEnabled: !state.isSoundEnabled)); + return save(state.copyWith(isSoundEnabled: !state.isSoundEnabled)); } Future setLocale(Locale? locale) { - return _save(state.copyWith(locale: locale)); + return save(state.copyWith(locale: locale)); } Future setSoundTheme(SoundTheme soundTheme) { - return _save(state.copyWith(soundTheme: soundTheme)); + return save(state.copyWith(soundTheme: soundTheme)); } Future setMasterVolume(double volume) { - return _save(state.copyWith(masterVolume: volume)); - } - - Future toggleSystemColors() async { - if (defaultTargetPlatform != TargetPlatform.android) { - return; - } - await _save(state.copyWith(systemColors: !state.systemColors)); - if (state.systemColors == false) { - final boardTheme = ref.read(boardPreferencesProvider).boardTheme; - if (boardTheme == BoardTheme.system) { - await ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.brown); - } - } else { - await ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.system); - } + return save(state.copyWith(masterVolume: volume)); } - Future _save(GeneralPrefsState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - _prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; + Future setAppThemeSeed(AppThemeSeed seed) { + return save(state.copyWith(appThemeSeed: seed)); } } @Freezed(fromJson: true, toJson: true) -class GeneralPrefsState with _$GeneralPrefsState { - const factory GeneralPrefsState({ - /// Background theme mode to use in the app - @JsonKey(unknownEnumValue: ThemeMode.system) required ThemeMode themeMode, +class GeneralPrefs with _$GeneralPrefs implements Serializable { + const factory GeneralPrefs({ + @JsonKey(unknownEnumValue: BackgroundThemeMode.system, defaultValue: BackgroundThemeMode.system) + required BackgroundThemeMode themeMode, required bool isSoundEnabled, - @JsonKey(unknownEnumValue: SoundTheme.standard) - required SoundTheme soundTheme, + @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, - /// Should enable system color palette (android 12+ only) - required bool systemColors, + @Deprecated('Use appThemeSeed instead') bool? systemColors, + + /// App theme seed + @JsonKey(unknownEnumValue: AppThemeSeed.board, defaultValue: AppThemeSeed.board) + required AppThemeSeed appThemeSeed, /// Locale to use in the app, use system locale if null - @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, - }) = _GeneralPrefsState; + @LocaleConverter() Locale? locale, + }) = _GeneralPrefs; - static const defaults = GeneralPrefsState( - themeMode: ThemeMode.system, + static const defaults = GeneralPrefs( + themeMode: BackgroundThemeMode.system, isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, - systemColors: true, + appThemeSeed: AppThemeSeed.board, ); - factory GeneralPrefsState.fromJson(Map json) { - try { - return _$GeneralPrefsStateFromJson(json); - } catch (e) { - debugPrint('Error parsing GeneralPrefsState: $e'); - return defaults; - } + factory GeneralPrefs.fromJson(Map json) { + return _$GeneralPrefsFromJson(json); } } -Map? _localeToJson(Locale? locale) { - return locale != null - ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } - : null; +enum AppThemeSeed { + /// The app theme is based on the user's system theme (only available on Android 10+). + system, + + /// The app theme is based on the chessboard. + board, } -Locale? _localeFromJson(Map? json) { - if (json == null) { - return null; - } - return Locale.fromSubtags( - languageCode: json['languageCode'] as String, - countryCode: json['countryCode'] as String?, - scriptCode: json['scriptCode'] as String?, - ); +/// Describes the background theme of the app. +enum BackgroundThemeMode { + /// Use either the light or dark theme based on what the user has selected in + /// the system settings. + system, + + /// Always use the light mode regardless of system preference. + light, + + /// Always use the dark mode (if available) regardless of system preference. + dark, +} + +enum SoundTheme { + standard('Standard'), + piano('Piano'), + nes('NES'), + sfx('SFX'), + futuristic('Futuristic'), + lisp('Lisp'); + + final String label; + + const SoundTheme(this.label); } diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index 19f5d65b5d..ff9fa70910 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -1,75 +1,52 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; part 'home_preferences.freezed.dart'; part 'home_preferences.g.dart'; -const _prefKey = 'preferences.home'; +@riverpod +class HomePreferences extends _$HomePreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + PrefCategory get prefCategory => PrefCategory.home; -@Riverpod(keepAlive: true) -class HomePreferences extends _$HomePreferences { - static HomePrefState fetchFromStorage(SharedPreferences prefs) { - final stored = prefs.getString(_prefKey); - return stored != null - ? HomePrefState.fromJson( - jsonDecode(stored) as Map, - ) - : HomePrefState.defaults; - } + // ignore: avoid_public_notifier_properties + @override + HomePrefs get defaults => HomePrefs.defaults; + + @override + HomePrefs fromJson(Map json) => HomePrefs.fromJson(json); @override - HomePrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - return fetchFromStorage(prefs); + HomePrefs build() { + return fetch(); } Future toggleWidget(EnabledWidget widget) { final newState = state.copyWith( - enabledWidgets: state.enabledWidgets.contains(widget) - ? state.enabledWidgets.difference({widget}) - : state.enabledWidgets.union({widget}), + enabledWidgets: + state.enabledWidgets.contains(widget) + ? state.enabledWidgets.difference({widget}) + : state.enabledWidgets.union({widget}), ); - return _save(newState); - } - - Future _save(HomePrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - _prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; + return save(newState); } } -enum EnabledWidget { - hello, - perfCards, - quickPairing, -} +enum EnabledWidget { hello, perfCards, quickPairing } @Freezed(fromJson: true, toJson: true) -class HomePrefState with _$HomePrefState { - const factory HomePrefState({ - required Set enabledWidgets, - }) = _HomePrefState; +class HomePrefs with _$HomePrefs implements Serializable { + const factory HomePrefs({required Set enabledWidgets}) = _HomePrefs; - static const defaults = HomePrefState( - enabledWidgets: { - EnabledWidget.hello, - EnabledWidget.perfCards, - EnabledWidget.quickPairing, - }, + static const defaults = HomePrefs( + enabledWidgets: {EnabledWidget.hello, EnabledWidget.perfCards, EnabledWidget.quickPairing}, ); - factory HomePrefState.fromJson(Map json) { + factory HomePrefs.fromJson(Map json) { try { - return _$HomePrefStateFromJson(json); + return _$HomePrefsFromJson(json); } catch (_) { return defaults; } diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart new file mode 100644 index 0000000000..4fe2b72f7c --- /dev/null +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -0,0 +1,50 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_preferences.freezed.dart'; +part 'over_the_board_preferences.g.dart'; + +@riverpod +class OverTheBoardPreferences extends _$OverTheBoardPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + PrefCategory get prefCategory => PrefCategory.overTheBoard; + + // ignore: avoid_public_notifier_properties + @override + OverTheBoardPrefs get defaults => OverTheBoardPrefs.defaults; + + @override + OverTheBoardPrefs fromJson(Map json) => OverTheBoardPrefs.fromJson(json); + + @override + OverTheBoardPrefs build() { + return fetch(); + } + + Future toggleFlipPiecesAfterMove() { + return save(state.copyWith(flipPiecesAfterMove: !state.flipPiecesAfterMove)); + } + + Future toggleSymmetricPieces() { + return save(state.copyWith(symmetricPieces: !state.symmetricPieces)); + } +} + +@Freezed(fromJson: true, toJson: true) +class OverTheBoardPrefs with _$OverTheBoardPrefs implements Serializable { + const OverTheBoardPrefs._(); + + const factory OverTheBoardPrefs({ + required bool flipPiecesAfterMove, + required bool symmetricPieces, + }) = _OverTheBoardPrefs; + + static const defaults = OverTheBoardPrefs(flipPiecesAfterMove: false, symmetricPieces: false); + + factory OverTheBoardPrefs.fromJson(Map json) { + return _$OverTheBoardPrefsFromJson(json); + } +} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart new file mode 100644 index 0000000000..9234927cde --- /dev/null +++ b/lib/src/model/settings/preferences_storage.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('PreferencesStorage'); + +abstract class Serializable { + Map toJson(); +} + +/// A preference category with its storage key +enum PrefCategory { + general('preferences.general'), + home('preferences.home'), + board('preferences.board'), + analysis('preferences.analysis'), + study('preferences.study'), + overTheBoard('preferences.overTheBoard'), + challenge('preferences.challenge'), + gameSetup('preferences.gameSetup'), + game('preferences.game'), + coordinateTraining('preferences.coordinateTraining'), + openingExplorer('preferences.opening_explorer'), + puzzle('preferences.puzzle'), + broadcast('preferences.broadcast'); + + const PrefCategory(this.storageKey); + + final String storageKey; +} + +/// A [Notifier] mixin to provide a way to store and retrieve preferences. +mixin PreferencesStorage on AutoDisposeNotifier { + T fromJson(Map json); + T get defaults; + + PrefCategory get prefCategory; + + Future save(T value) async { + await LichessBinding.instance.sharedPreferences.setString( + prefCategory.storageKey, + jsonEncode(value.toJson()), + ); + + state = value; + } + + T fetch() { + final stored = LichessBinding.instance.sharedPreferences.getString(prefCategory.storageKey); + if (stored == null) { + return defaults; + } + try { + return fromJson(jsonDecode(stored) as Map); + } catch (e) { + _logger.warning('Failed to decode $prefCategory preferences: $e'); + return defaults; + } + } +} + +/// A [Notifier] mixin to provide a way to store and retrieve preferences per session. +mixin SessionPreferencesStorage on AutoDisposeNotifier { + T fromJson(Map json); + T defaults({LightUser? user}); + + PrefCategory get prefCategory; + + Future save(T value) async { + final session = ref.read(authSessionProvider); + await LichessBinding.instance.sharedPreferences.setString( + key(prefCategory.storageKey, session), + jsonEncode(value.toJson()), + ); + + state = value; + } + + T fetch() { + final session = ref.watch(authSessionProvider); + final stored = LichessBinding.instance.sharedPreferences.getString( + key(prefCategory.storageKey, session), + ); + if (stored == null) { + return defaults(user: session?.user); + } + try { + return fromJson(jsonDecode(stored) as Map); + } catch (e) { + _logger.warning('Failed to decode $prefCategory preferences: $e'); + return defaults(user: session?.user); + } + } + + static String key(String key, AuthSessionState? session) => + '$key.${session?.user.id ?? '**anon**'}'; +} diff --git a/lib/src/model/settings/sound_theme.dart b/lib/src/model/settings/sound_theme.dart deleted file mode 100644 index 53b5ddd252..0000000000 --- a/lib/src/model/settings/sound_theme.dart +++ /dev/null @@ -1,12 +0,0 @@ -enum SoundTheme { - standard('Standard'), - piano('Piano'), - nes('NES'), - sfx('SFX'), - futuristic('Futuristic'), - lisp('Lisp'); - - final String label; - - const SoundTheme(this.label); -} diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart new file mode 100644 index 0000000000..2a45906358 --- /dev/null +++ b/lib/src/model/study/study.dart @@ -0,0 +1,165 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; + +part 'study.freezed.dart'; +part 'study.g.dart'; + +@freezed +class Study with _$Study { + const Study._(); + + const factory Study({ + required StudyId id, + required String name, + required bool liked, + required int likes, + required UserId? ownerId, + required StudyFeatures features, + required IList topics, + required IList chapters, + required StudyChapter chapter, + + /// Hints to display in "gamebook"/"interactive" mode + /// Index corresponds to the current ply. + required IList hints, + + /// Comment to display when deviating from the mainline in "gamebook" mode + /// (i.e. when making a wrong move). + /// Index corresponds to the current ply. + required IList deviationComments, + }) = _Study; + + StudyChapterMeta get currentChapterMeta => chapters.firstWhere((c) => c.id == chapter.id); + + factory Study.fromServerJson(Map json) { + return _studyFromPick(pick(json).required()); + } +} + +Study _studyFromPick(RequiredPick pick) { + final treeParts = pick('analysis', 'treeParts').asListOrThrow((part) => part); + + final hints = []; + final deviationComments = []; + + for (final part in treeParts) { + hints.add(part('gamebook', 'hint').asStringOrNull()); + deviationComments.add(part('gamebook', 'deviation').asStringOrNull()); + } + + final study = pick('study'); + return Study( + id: study('id').asStudyIdOrThrow(), + name: study('name').asStringOrThrow(), + liked: study('liked').asBoolOrThrow(), + likes: study('likes').asIntOrThrow(), + ownerId: study('ownerId').asUserIdOrNull(), + features: ( + cloneable: study('features', 'cloneable').asBoolOrFalse(), + chat: study('features', 'chat').asBoolOrFalse(), + sticky: study('features', 'sticky').asBoolOrFalse(), + ), + topics: study('topics').asListOrThrow((pick) => pick.asStringOrThrow()).lock, + chapters: + study( + 'chapters', + ).asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())).lock, + chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), + hints: hints.lock, + deviationComments: deviationComments.lock, + ); +} + +typedef StudyFeatures = ({bool cloneable, bool chat, bool sticky}); + +@Freezed(fromJson: true) +class StudyChapter with _$StudyChapter { + const StudyChapter._(); + + const factory StudyChapter({ + required StudyChapterId id, + required StudyChapterSetup setup, + @JsonKey(defaultValue: false) required bool practise, + required int? conceal, + @JsonKey(defaultValue: false) required bool gamebook, + @JsonKey(fromJson: studyChapterFeaturesFromJson) required StudyChapterFeatures features, + }) = _StudyChapter; + + factory StudyChapter.fromJson(Map json) => _$StudyChapterFromJson(json); +} + +typedef StudyChapterFeatures = ({bool computer, bool explorer}); + +StudyChapterFeatures studyChapterFeaturesFromJson(Map json) { + return ( + computer: json['computer'] as bool? ?? false, + explorer: json['explorer'] as bool? ?? false, + ); +} + +@Freezed(fromJson: true) +class StudyChapterSetup with _$StudyChapterSetup { + const StudyChapterSetup._(); + + const factory StudyChapterSetup({ + required GameId? id, + required Side orientation, + @JsonKey(fromJson: _variantFromJson) required Variant variant, + required bool? fromFen, + }) = _StudyChapterSetup; + + factory StudyChapterSetup.fromJson(Map json) => + _$StudyChapterSetupFromJson(json); +} + +Variant _variantFromJson(Map json) { + return Variant.values.firstWhereOrNull((v) => v.name == json['key'])!; +} + +@Freezed(fromJson: true) +class StudyChapterMeta with _$StudyChapterMeta { + const StudyChapterMeta._(); + + const factory StudyChapterMeta({ + required StudyChapterId id, + required String name, + required String? fen, + }) = _StudyChapterMeta; + + factory StudyChapterMeta.fromJson(Map json) => _$StudyChapterMetaFromJson(json); +} + +@Freezed(fromJson: true) +class StudyPageData with _$StudyPageData { + const StudyPageData._(); + + const factory StudyPageData({ + required StudyId id, + required String name, + required bool liked, + required int likes, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) required DateTime updatedAt, + required LightUser? owner, + required IList topics, + required IList members, + required IList chapters, + required String? flair, + }) = _StudyPageData; + + factory StudyPageData.fromJson(Map json) => _$StudyPageDataFromJson(json); +} + +@Freezed(fromJson: true) +class StudyMember with _$StudyMember { + const StudyMember._(); + + const factory StudyMember({required LightUser user, required String role}) = _StudyMember; + + factory StudyMember.fromJson(Map json) => _$StudyMemberFromJson(json); +} diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart new file mode 100644 index 0000000000..7d44097006 --- /dev/null +++ b/lib/src/model/study/study_controller.dart @@ -0,0 +1,695 @@ +import 'dart:async'; + +import 'package:chessground/chessground.dart'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_controller.freezed.dart'; +part 'study_controller.g.dart'; + +@riverpod +class StudyController extends _$StudyController implements PgnTreeNotifier { + late Root _root; + + final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + + Timer? _startEngineEvalTimer; + Timer? _opponentFirstMoveTimer; + StreamSubscription? _socketSubscription; + final _likeDebouncer = Debouncer(const Duration(milliseconds: 500)); + + late final SocketClient _socketClient; + + @override + Future build(StudyId id) async { + final socketPool = ref.watch(socketPoolProvider); + final evaluationService = ref.watch(evaluationServiceProvider); + _socketClient = socketPool.open(Uri(path: '/study/$id/socket/v6')); + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _opponentFirstMoveTimer?.cancel(); + _engineEvalDebounce.dispose(); + _socketSubscription?.cancel(); + _likeDebouncer.dispose(); + evaluationService.disposeEngine(); + }); + final chapter = await _fetchChapter(id); + + _socketSubscription?.cancel(); + _socketSubscription = _socketClient.stream.listen(_handleSocketEvent); + + return chapter; + } + + Future nextChapter() async { + if (state.hasValue) { + final chapters = state.requireValue.study.chapters; + final currentChapterIndex = chapters.indexWhere( + (chapter) => chapter.id == state.requireValue.study.chapter.id, + ); + goToChapter(chapters[currentChapterIndex + 1].id); + } + } + + Future goToChapter(StudyChapterId chapterId) async { + state = AsyncValue.data(await _fetchChapter(state.requireValue.study.id, chapterId: chapterId)); + _ensureItsOurTurnIfGamebook(); + } + + Future _fetchChapter(StudyId id, {StudyChapterId? chapterId}) async { + final (study, pgn) = await ref + .read(studyRepositoryProvider) + .getStudy(id: id, chapterId: chapterId); + + final game = PgnGame.parsePgn(pgn); + + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + final variant = study.chapter.setup.variant; + final orientation = study.chapter.setup.orientation; + + // Some studies have illegal starting positions. This is usually the case for introductory chapters. + // We do not treat this as an error, but display a static board instead. + try { + _root = Root.fromPgnGame(game); + } on PositionSetupException { + return StudyState( + variant: variant, + study: study, + currentPath: UciPath.empty, + isOnMainline: true, + root: null, + currentNode: StudyCurrentNode.illegalPosition(), + pgnRootComments: rootComments, + pov: orientation, + isComputerAnalysisAllowed: false, + isLocalEvaluationEnabled: false, + gamebookActive: false, + pgn: pgn, + ); + } + + // don't use ref.watch here: we don't want to invalidate state when the + // analysis preferences change + final prefs = ref.read(analysisPreferencesProvider); + + const currentPath = UciPath.empty; + Move? lastMove; + + final studyState = StudyState( + variant: variant, + study: study, + currentPath: currentPath, + isOnMainline: true, + root: _root.view, + currentNode: StudyCurrentNode.fromNode(_root), + pgnRootComments: rootComments, + lastMove: lastMove, + pov: orientation, + isComputerAnalysisAllowed: study.chapter.features.computer && !study.chapter.gamebook, + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + gamebookActive: study.chapter.gamebook, + pgn: pgn, + ); + + final evaluationService = ref.watch(evaluationServiceProvider); + if (studyState.isEngineAvailable) { + await evaluationService.disposeEngine(); + + evaluationService + .initEngine(_evaluationContext(studyState.variant), options: _evaluationOptions) + .then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); + } + + return studyState; + } + + void toggleLike() { + _likeDebouncer(() { + if (!state.hasValue) return; + final liked = state.requireValue.study.liked; + _socketClient.send('like', {'liked': !liked}); + state = AsyncValue.data( + state.requireValue.copyWith(study: state.requireValue.study.copyWith(liked: !liked)), + ); + }); + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) { + assert(false, 'received a game SocketEvent while StudyState is null'); + return; + } + switch (event.topic) { + case 'liking': + final data = (event.data as Map)['l'] as Map; + final likes = data['likes'] as int; + final bool meLiked = data['me'] as bool; + state = AsyncValue.data( + state.requireValue.copyWith( + study: state.requireValue.study.copyWith(liked: meLiked, likes: likes), + ), + ); + } + } + + // The PGNs of some gamebook studies start with the opponent's turn, so trigger their move after a delay + void _ensureItsOurTurnIfGamebook() { + _opponentFirstMoveTimer?.cancel(); + if (state.requireValue.isAtStartOfChapter && + state.requireValue.gamebookActive && + state.requireValue.gamebookComment == null && + state.requireValue.position!.turn != state.requireValue.pov) { + _opponentFirstMoveTimer = Timer(const Duration(milliseconds: 750), () { + userNext(); + }); + } + } + + EvaluationContext _evaluationContext(Variant variant) => + EvaluationContext(variant: variant, initialPosition: _root.position); + + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + + void onUserMove(NormalMove move) { + if (!state.hasValue || state.requireValue.position == null) return; + + if (!state.requireValue.position!.isLegal(move)) return; + + if (isPromotionPawnMove(state.requireValue.position!, move)) { + state = AsyncValue.data(state.requireValue.copyWith(promotionMove: move)); + return; + } + + final (newPath, isNewNode) = _root.addMoveAt(state.requireValue.currentPath, move); + if (newPath != null) { + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); + } + + if (state.requireValue.gamebookActive) { + final comment = state.requireValue.gamebookComment; + // If there's no explicit comment why the move was good/bad, trigger next/previous move automatically + if (comment == null) { + Timer(const Duration(milliseconds: 750), () { + if (state.requireValue.isOnMainline) { + userNext(); + } else { + userPrevious(); + } + }); + } + } + } + + void onPromotionSelection(Role? role) { + final state = this.state.valueOrNull; + if (state == null) return; + + if (role == null) { + this.state = AsyncValue.data(state.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void showGamebookSolution() { + onUserMove(state.requireValue.currentNode.children.first as NormalMove); + } + + void userNext() { + final state = this.state.valueOrNull; + if (state!.currentNode.children.isEmpty) return; + _setPath( + state.currentPath + _root.nodeAt(state.currentPath).children.first.id, + replaying: true, + ); + } + + void jumpToNthNodeOnMainline(int n) { + UciPath path = _root.mainlinePath; + while (!path.penultimate.isEmpty) { + path = path.penultimate; + } + Node? node = _root.nodeAt(path); + int count = 0; + + while (node != null && count < n) { + if (node.children.isNotEmpty) { + path = path + node.children.first.id; + node = _root.nodeAt(path); + count++; + } else { + break; + } + } + + if (node != null) { + userJump(path); + } + } + + void toggleBoard() { + final state = this.state.valueOrNull; + if (state != null) { + this.state = AsyncValue.data(state.copyWith(pov: state.pov.opposite)); + } + } + + void userPrevious() { + if (state.hasValue) { + _setPath(state.requireValue.currentPath.penultimate, replaying: true); + } + } + + void reset() { + if (state.hasValue) { + _setPath(UciPath.empty); + _ensureItsOurTurnIfGamebook(); + } + } + + @override + void userJump(UciPath path) { + _setPath(path); + } + + @override + void expandVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + final childrenToShow = _root.isOnMainline(path) ? node.children.skip(1) : node.children; + + for (final child in childrenToShow) { + child.isCollapsed = false; + for (final grandChild in child.children) { + grandChild.isCollapsed = false; + } + } + state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); + } + + @override + void collapseVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isCollapsed = true; + } + + state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); + } + + @override + void promoteVariation(UciPath path, bool toMainline) { + final state = this.state.valueOrNull; + if (state == null) return; + _root.promoteAt(path, toMainline: toMainline); + this.state = AsyncValue.data( + state.copyWith(isOnMainline: _root.isOnMainline(state.currentPath), root: _root.view), + ); + } + + @override + void deleteFromHere(UciPath path) { + if (!state.hasValue) return; + + _root.deleteAt(path); + _setPath(path.penultimate, shouldRecomputeRootView: true); + } + + Future toggleLocalEvaluation() async { + if (!state.hasValue) return; + + ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); + + state = AsyncValue.data( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), + ); + + if (state.requireValue.isEngineAvailable) { + await ref + .read(evaluationServiceProvider) + .initEngine(_evaluationContext(state.requireValue.variant), options: _evaluationOptions); + _startEngineEval(); + } else { + _stopEngineEval(); + ref.read(evaluationServiceProvider).disposeEngine(); + } + } + + void setNumEvalLines(int numEvalLines) { + if (!state.hasValue) return; + + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _root.updateAll((node) => node.eval = null); + + state = AsyncValue.data( + state.requireValue.copyWith( + currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + + _startEngineEval(); + } + + void setEngineCores(int numEngineCores) { + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _startEngineEval(); + } + + void setEngineSearchTime(Duration searchTime) { + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); + + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); + + _startEngineEval(); + } + + void _setPath( + UciPath path, { + bool shouldForceShowVariation = false, + bool shouldRecomputeRootView = false, + bool replaying = false, + }) { + final state = this.state.valueOrNull; + if (state == null) return; + + final pathChange = state.currentPath != path; + final currentNode = _root.nodeAt(path); + + // always show variation if the user plays a move + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { + _root.updateAt(path, (node) { + if (node is Branch) node.isCollapsed = false; + }); + } + + // root view is only used to display move list, so we need to + // recompute the root view only when the nodelist length changes + // or a variation is hidden/shown + final rootView = shouldForceShowVariation || shouldRecomputeRootView ? _root.view : state.root; + + final isForward = path.size > state.currentPath.size; + if (currentNode is Branch) { + if (!replaying) { + if (isForward) { + final isCheck = currentNode.sanMove.isCheck; + if (currentNode.sanMove.isCapture) { + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } + } else if (isForward) { + final soundService = ref.read(soundServiceProvider); + if (currentNode.sanMove.isCapture) { + soundService.play(Sound.capture); + } else { + soundService.play(Sound.move); + } + } + + this.state = AsyncValue.data( + state.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: StudyCurrentNode.fromNode(currentNode), + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + ), + ); + } else { + this.state = AsyncValue.data( + state.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: StudyCurrentNode.fromNode(currentNode), + lastMove: null, + promotionMove: null, + root: rootView, + ), + ); + } + + if (pathChange) { + _debouncedStartEngineEval(); + } + } + + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + + void _startEngineEval() { + final curState = state.valueOrNull; + if (curState == null || !curState.isEngineAvailable) return; + + ref + .read(evaluationServiceProvider) + .start( + curState.currentPath, + _root.branchesOn(curState.currentPath).map(Step.fromNode), + // Note: AnalysisController passes _root.eval as initialPositionEval here, + // but for studies this leads to false positive cache hits when switching between chapters. + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, + ) + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); + } + + void _debouncedStartEngineEval() { + _engineEvalDebounce(() { + _startEngineEval(); + }); + } + + void _stopEngineEval() { + ref.read(evaluationServiceProvider).stop(); + + if (!state.hasValue) return; + + // update the current node with last cached eval + _refreshCurrentNode(); + } +} + +enum GamebookState { startLesson, findTheMove, correctMove, incorrectMove, lessonComplete } + +@freezed +class StudyState with _$StudyState { + const StudyState._(); + + const factory StudyState({ + required Study study, + required String pgn, + + /// The variant of the current chapter + required Variant variant, + + /// Immutable view of the whole tree. Null if the chapter's starting position is illegal. + required ViewRoot? root, + + /// The current node in the study tree view. + /// + /// This is an immutable copy of the actual [Node] at the `currentPath`. + /// We don't want to use [Node.view] here because it'd copy the whole tree + /// under the current node and it's expensive. + required StudyCurrentNode currentNode, + + /// The path to the current node in the analysis view. + required UciPath currentPath, + + /// Whether the current path is on the mainline. + required bool isOnMainline, + + /// The side to display the board from. + required Side pov, + + /// Whether local evaluation is allowed for this study. + required bool isComputerAnalysisAllowed, + + /// Whether we're currently in gamebook mode, where the user has to find the right moves. + required bool gamebookActive, + + /// Whether the user has enabled local evaluation. + required bool isLocalEvaluationEnabled, + + /// The last move played. + Move? lastMove, + + /// Possible promotion move to be played. + NormalMove? promotionMove, + + /// The PGN root comments of the study + IList? pgnRootComments, + }) = _StudyState; + + IMap> get validMoves => + currentNode.position != null ? makeLegalMoves(currentNode.position!) : const IMap.empty(); + + /// Whether the engine is available for evaluation + bool get isEngineAvailable => + isComputerAnalysisAllowed && + engineSupportedVariants.contains(variant) && + isLocalEvaluationEnabled; + + bool get isOpeningExplorerAvailable => !gamebookActive && study.chapter.features.explorer; + + EngineGaugeParams? get engineGaugeParams => + isEngineAvailable + ? ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position!, + savedEval: currentNode.eval, + ) + : null; + + Position? get position => currentNode.position; + StudyChapter get currentChapter => study.chapter; + bool get canGoNext => currentNode.children.isNotEmpty; + bool get canGoBack => currentPath.size > UciPath.empty.size; + + String get currentChapterTitle => + study.chapters.firstWhere((chapter) => chapter.id == currentChapter.id).name; + bool get hasNextChapter => study.chapter.id != study.chapters.last.id; + + bool get isAtEndOfChapter => isOnMainline && currentNode.children.isEmpty; + + bool get isAtStartOfChapter => currentPath.isEmpty; + + String? get gamebookComment { + final comment = (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.text) + .nonNulls + .join('\n'); + return comment?.isNotEmpty == true + ? comment + : gamebookState == GamebookState.incorrectMove + ? gamebookDeviationComment + : null; + } + + String? get gamebookHint => study.hints.getOrNull(currentPath.size); + + String? get gamebookDeviationComment => study.deviationComments.getOrNull(currentPath.size); + + GamebookState get gamebookState { + if (isAtEndOfChapter) return GamebookState.lessonComplete; + + final bool myTurn = currentNode.position!.turn == pov; + if (isAtStartOfChapter && !myTurn) return GamebookState.startLesson; + + return myTurn + ? GamebookState.findTheMove + : isOnMainline + ? GamebookState.correctMove + : GamebookState.incorrectMove; + } + + bool get isIntroductoryChapter => currentNode.isRoot && currentNode.children.isEmpty; + + IList get pgnShapes => IList( + (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.shapes) + .flattened, + ); + + PlayerSide get playerSide => + gamebookActive ? (pov == Side.white ? PlayerSide.white : PlayerSide.black) : PlayerSide.both; +} + +@freezed +class StudyCurrentNode with _$StudyCurrentNode { + const StudyCurrentNode._(); + + const factory StudyCurrentNode({ + // Null if the chapter's starting position is illegal. + required Position? position, + required List children, + required bool isRoot, + SanMove? sanMove, + IList? startingComments, + IList? comments, + IList? nags, + ClientEval? eval, + }) = _StudyCurrentNode; + + factory StudyCurrentNode.illegalPosition() { + return const StudyCurrentNode(position: null, children: [], isRoot: true); + } + + factory StudyCurrentNode.fromNode(Node node) { + final children = node.children.map((n) => n.sanMove.move).toList(); + if (node is Branch) { + return StudyCurrentNode( + sanMove: node.sanMove, + position: node.position, + isRoot: false, + children: children, + eval: node.eval, + startingComments: IList(node.startingComments), + comments: IList(node.comments), + nags: IList(node.nags), + ); + } else { + return StudyCurrentNode( + position: node.position, + children: children, + eval: node.eval, + isRoot: true, + ); + } + } +} diff --git a/lib/src/model/study/study_filter.dart b/lib/src/model/study/study_filter.dart new file mode 100644 index 0000000000..7cefc08f35 --- /dev/null +++ b/lib/src/model/study/study_filter.dart @@ -0,0 +1,60 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_filter.freezed.dart'; +part 'study_filter.g.dart'; + +enum StudyCategory { + all, + mine, + member, + public, + private, + likes; + + String l10n(AppLocalizations l10n) => switch (this) { + StudyCategory.all => l10n.studyAllStudies, + StudyCategory.mine => l10n.studyMyStudies, + StudyCategory.member => l10n.studyStudiesIContributeTo, + StudyCategory.public => l10n.studyMyPublicStudies, + StudyCategory.private => l10n.studyMyPrivateStudies, + StudyCategory.likes => l10n.studyMyFavoriteStudies, + }; +} + +enum StudyListOrder { + hot, + popular, + newest, + oldest, + updated; + + String l10n(AppLocalizations l10n) => switch (this) { + StudyListOrder.hot => l10n.studyHot, + StudyListOrder.newest => l10n.studyDateAddedNewest, + StudyListOrder.oldest => l10n.studyDateAddedOldest, + StudyListOrder.updated => l10n.studyRecentlyUpdated, + StudyListOrder.popular => l10n.studyMostPopular, + }; +} + +@riverpod +class StudyFilter extends _$StudyFilter { + @override + StudyFilterState build() => const StudyFilterState(); + + void setCategory(StudyCategory category) => state = state.copyWith(category: category); + + void setOrder(StudyListOrder order) => state = state.copyWith(order: order); +} + +@freezed +class StudyFilterState with _$StudyFilterState { + const StudyFilterState._(); + + const factory StudyFilterState({ + @Default(StudyCategory.all) StudyCategory category, + @Default(StudyListOrder.hot) StudyListOrder order, + }) = _StudyFilterState; +} diff --git a/lib/src/model/study/study_list_paginator.dart b/lib/src/model/study/study_list_paginator.dart new file mode 100644 index 0000000000..0a24a1999d --- /dev/null +++ b/lib/src/model/study/study_list_paginator.dart @@ -0,0 +1,39 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_list_paginator.g.dart'; + +typedef StudyList = ({IList studies, int? nextPage}); + +/// Gets a list of studies from the paginated API. +@riverpod +class StudyListPaginator extends _$StudyListPaginator { + @override + Future build({required StudyFilterState filter, String? search}) async { + return _nextPage(); + } + + Future next() async { + final studyList = state.requireValue; + if (studyList.nextPage == null) return; + + final newStudyPage = await _nextPage(); + + state = AsyncData(( + nextPage: newStudyPage.nextPage, + studies: studyList.studies.addAll(newStudyPage.studies), + )); + } + + Future _nextPage() async { + final nextPage = state.value?.nextPage ?? 1; + + final repo = ref.read(studyRepositoryProvider); + return search == null + ? repo.getStudies(category: filter.category, order: filter.order, page: nextPage) + : repo.searchStudies(query: search!, page: nextPage); + } +} diff --git a/lib/src/model/study/study_preferences.dart b/lib/src/model/study/study_preferences.dart new file mode 100644 index 0000000000..d309eeb7ab --- /dev/null +++ b/lib/src/model/study/study_preferences.dart @@ -0,0 +1,42 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_preferences.freezed.dart'; +part 'study_preferences.g.dart'; + +@riverpod +class StudyPreferences extends _$StudyPreferences with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.study; + + // ignore: avoid_public_notifier_properties + @override + StudyPrefs get defaults => StudyPrefs.defaults; + + @override + StudyPrefs fromJson(Map json) => StudyPrefs.fromJson(json); + + @override + StudyPrefs build() { + return fetch(); + } + + Future toggleShowVariationArrows() { + return save(state.copyWith(showVariationArrows: !state.showVariationArrows)); + } +} + +@Freezed(fromJson: true, toJson: true) +class StudyPrefs with _$StudyPrefs implements Serializable { + const StudyPrefs._(); + + const factory StudyPrefs({required bool showVariationArrows}) = _StudyPrefs; + + static const defaults = StudyPrefs(showVariationArrows: false); + + factory StudyPrefs.fromJson(Map json) { + return _$StudyPrefsFromJson(json); + } +} diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart new file mode 100644 index 0000000000..19cad44f07 --- /dev/null +++ b/lib/src/model/study/study_repository.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_repository.g.dart'; + +@Riverpod(keepAlive: true) +StudyRepository studyRepository(Ref ref) { + return StudyRepository(ref, ref.read(lichessClientProvider)); +} + +class StudyRepository { + StudyRepository(this.ref, this.client); + + final Client client; + final Ref ref; + + Future getStudies({ + required StudyCategory category, + required StudyListOrder order, + int page = 1, + }) { + return _requestStudies( + path: '${category.name}/${order.name}', + queryParameters: {'page': page.toString()}, + ); + } + + Future searchStudies({required String query, int page = 1}) { + return _requestStudies(path: 'search', queryParameters: {'page': page.toString(), 'q': query}); + } + + Future _requestStudies({ + required String path, + required Map queryParameters, + }) { + return client.readJson( + Uri(path: '/study/$path', queryParameters: queryParameters), + headers: {'Accept': 'application/json'}, + mapper: (Map json) { + final paginator = pick(json, 'paginator').asMapOrThrow(); + + return ( + studies: + pick( + paginator, + 'currentPageResults', + ).asListOrThrow((pick) => StudyPageData.fromJson(pick.asMapOrThrow())).toIList(), + nextPage: pick(paginator, 'nextPage').asIntOrNull(), + ); + }, + ); + } + + Future<(Study study, String pgn)> getStudy({ + required StudyId id, + StudyChapterId? chapterId, + }) async { + final study = await client.readJson( + Uri( + path: (chapterId != null) ? '/study/$id/$chapterId' : '/study/$id', + queryParameters: {'chapters': '1'}, + ), + headers: {'Accept': 'application/json'}, + mapper: Study.fromServerJson, + ); + + final pgnBytes = await client.readBytes( + Uri(path: '/api/study/$id/${chapterId ?? study.chapter.id}.pgn'), + headers: {'Accept': 'application/x-chess-pgn'}, + ); + + return (study, utf8.decode(pgnBytes)); + } + + Future getStudyPgn(StudyId id) async { + final pgnBytes = await client.readBytes( + Uri(path: '/api/study/$id.pgn'), + headers: {'Accept': 'application/x-chess-pgn'}, + ); + + return utf8.decode(pgnBytes); + } +} diff --git a/lib/src/model/tv/featured_player.dart b/lib/src/model/tv/featured_player.dart index aa1f431395..070b3171cc 100644 --- a/lib/src/model/tv/featured_player.dart +++ b/lib/src/model/tv/featured_player.dart @@ -29,8 +29,7 @@ class FeaturedPlayer with _$FeaturedPlayer { } Player get asPlayer => Player( - user: - LightUser(id: UserId(name.toLowerCase()), name: name, title: title), - rating: rating, - ); + user: LightUser(id: UserId(name.toLowerCase()), name: name, title: title), + rating: rating, + ); } diff --git a/lib/src/model/tv/live_tv_channels.dart b/lib/src/model/tv/live_tv_channels.dart index 0bd44d76f6..e7df4cf9e2 100644 --- a/lib/src/model/tv/live_tv_channels.dart +++ b/lib/src/model/tv/live_tv_channels.dart @@ -3,9 +3,10 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'featured_player.dart'; @@ -47,19 +48,16 @@ class LiveTvChannels extends _$LiveTvChannels { } Future> _doStartWatching() async { - final repoGames = - await ref.withClient((client) => TvRepository(client).channels()); + final repoGames = await ref.withClient((client) => TvRepository(client).channels()); - _socketClient = - ref.read(socketPoolProvider).open(Uri(path: kDefaultSocketRoute)); + _socketClient = ref.read(socketPoolProvider).open(Uri(path: kDefaultSocketRoute)); await _socketClient.firstConnection; _socketWatch(repoGames); _socketReadySubscription?.cancel(); _socketReadySubscription = _socketClient.connectedStream.listen((_) async { - final repoGames = - await ref.withClient((client) => TvRepository(client).channels()); + final repoGames = await ref.withClient((client) => TvRepository(client).channels()); _socketWatch(repoGames); }); @@ -88,19 +86,13 @@ class LiveTvChannels extends _$LiveTvChannels { _socketClient.send('startWatchingTvChannels', null); _socketClient.send( 'startWatching', - games.entries - .where((e) => TvChannel.values.contains(e.key)) - .map((e) => e.value.id) - .join(' '), + games.entries.where((e) => TvChannel.values.contains(e.key)).map((e) => e.value.id).join(' '), ); } void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) { - assert( - false, - 'received a SocketEvent while LiveTvChannels state is null', - ); + assert(false, 'received a SocketEvent while LiveTvChannels state is null'); return; } @@ -108,18 +100,15 @@ class LiveTvChannels extends _$LiveTvChannels { case 'fen': final json = event.data as Map; final fenEvent = FenSocketEvent.fromJson(json); - final snapshots = - state.requireValue.values.where((s) => s.id == fenEvent.id); + final snapshots = state.requireValue.values.where((s) => s.id == fenEvent.id); if (snapshots.isNotEmpty) { state = AsyncValue.data( state.requireValue.updateAll( - (key, value) => value.id == fenEvent.id - ? value.copyWith( - fen: fenEvent.fen, - lastMove: fenEvent.lastMove, - ) - : value, + (key, value) => + value.id == fenEvent.id + ? value.copyWith(fen: fenEvent.fen, lastMove: fenEvent.lastMove) + : value, ), ); } diff --git a/lib/src/model/tv/tv_channel.dart b/lib/src/model/tv/tv_channel.dart index 3f70ea4203..72aa16e279 100644 --- a/lib/src/model/tv/tv_channel.dart +++ b/lib/src/model/tv/tv_channel.dart @@ -26,8 +26,7 @@ enum TvChannel { final String label; final IconData icon; - static final IMap nameMap = - IMap(TvChannel.values.asNameMap()); + static final IMap nameMap = IMap(TvChannel.values.asNameMap()); } extension TvChannelExtension on Pick { @@ -41,9 +40,7 @@ extension TvChannelExtension on Pick { return TvChannel.nameMap[value]!; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to TvChannel", - ); + throw PickException("value $value at $debugParsingExit can't be casted to TvChannel"); } TvChannel? asTvChannelOrNull() { diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 3fda475b7c..c445844d75 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -4,7 +4,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -16,6 +15,8 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'tv_controller.freezed.dart'; @@ -29,10 +30,7 @@ class TvController extends _$TvController { int? _socketEventVersion; @override - Future build( - TvChannel channel, - (GameId id, Side orientation)? initialGame, - ) async { + Future build(TvChannel channel, (GameId id, Side orientation)? initialGame) async { ref.onDispose(() { _socketSubscription?.cancel(); }); @@ -51,9 +49,7 @@ class TvController extends _$TvController { _socketSubscription?.cancel(); } - Future _connectWebsocket( - (GameId id, Side orientation)? game, - ) async { + Future _connectWebsocket((GameId id, Side orientation)? game) async { GameId id; Side orientation; @@ -61,29 +57,22 @@ class TvController extends _$TvController { id = game.$1; orientation = game.$2; } else { - final channels = - await ref.withClient((client) => TvRepository(client).channels()); + final channels = await ref.withClient((client) => TvRepository(client).channels()); final channelGame = channels[channel]!; id = channelGame.id; orientation = channelGame.side ?? Side.white; } - final socketClient = ref.read(socketPoolProvider).open( - Uri( - path: '/watch/$id/${orientation.name}/v6', - ), - forceReconnect: true, - ); + final socketClient = ref + .read(socketPoolProvider) + .open(Uri(path: '/watch/$id/${orientation.name}/v6'), forceReconnect: true); _socketSubscription?.cancel(); _socketEventVersion = null; _socketSubscription = socketClient.stream.listen(_handleSocketEvent); - return socketClient.stream - .firstWhere((e) => e.topic == 'full') - .then((event) { - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + return socketClient.stream.firstWhere((e) => e.topic == 'full').then((event) { + final fullEvent = GameFullEvent.fromJson(event.data as Map); _socketEventVersion = fullEvent.socketEventVersion; @@ -100,21 +89,15 @@ class TvController extends _$TvController { state = AsyncValue.data(newState); } - bool canGoBack() => - state.mapOrNull(data: (d) => d.value.stepCursor > 0) ?? false; + bool canGoBack() => state.mapOrNull(data: (d) => d.value.stepCursor > 0) ?? false; bool canGoForward() => - state.mapOrNull( - data: (d) => d.value.stepCursor < d.value.game.steps.length - 1, - ) ?? - false; + state.mapOrNull(data: (d) => d.value.stepCursor < d.value.game.steps.length - 1) ?? false; void toggleBoard() { if (state.hasValue) { final curState = state.requireValue; - state = AsyncValue.data( - curState.copyWith(orientation: curState.orientation.opposite), - ); + state = AsyncValue.data(curState.copyWith(orientation: curState.orientation.opposite)); } } @@ -122,9 +105,7 @@ class TvController extends _$TvController { if (state.hasValue) { final curState = state.requireValue; if (curState.stepCursor < curState.game.steps.length - 1) { - state = AsyncValue.data( - curState.copyWith(stepCursor: curState.stepCursor + 1), - ); + state = AsyncValue.data(curState.copyWith(stepCursor: curState.stepCursor + 1)); final san = curState.game.stepAt(curState.stepCursor + 1).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -137,9 +118,7 @@ class TvController extends _$TvController { if (state.hasValue) { final curState = state.requireValue; if (curState.stepCursor > 0) { - state = AsyncValue.data( - curState.copyWith(stepCursor: curState.stepCursor - 1), - ); + state = AsyncValue.data(curState.copyWith(stepCursor: curState.stepCursor - 1)); final san = curState.game.stepAt(curState.stepCursor - 1).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -180,10 +159,7 @@ class TvController extends _$TvController { void _handleSocketTopic(SocketEvent event) { if (!state.hasValue) { - assert( - false, - 'received a game SocketEvent while TvState is null', - ); + assert(false, 'received a game SocketEvent while TvState is null'); return; } @@ -192,7 +168,7 @@ class TvController extends _$TvController { final curState = state.requireValue; final data = MoveEvent.fromJson(event.data as Map); final lastPos = curState.game.lastPosition; - final move = Move.fromUci(data.uci)!; + final move = Move.parse(data.uci)!; final sanMove = SanMove(data.san, move); final newPos = lastPos.playUnchecked(move); final newStep = GameStep( @@ -202,21 +178,19 @@ class TvController extends _$TvController { ); TvState newState = curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.add(newStep), - ), + game: curState.game.copyWith(steps: curState.game.steps.add(newStep)), ); if (newState.game.clock != null && data.clock != null) { newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, + lag: data.clock!.lag, + at: data.clock!.at, ); } if (!curState.isReplaying) { - newState = newState.copyWith( - stepCursor: newState.stepCursor + 1, - ); + newState = newState.copyWith(stepCursor: newState.stepCursor + 1); if (data.san.contains('x')) { _soundService.play(Sound.capture); @@ -227,6 +201,21 @@ class TvController extends _$TvController { state = AsyncData(newState); + case 'endData': + final endData = GameEndEvent.fromJson(event.data as Map); + TvState newState = state.requireValue.copyWith( + game: state.requireValue.game.copyWith(status: endData.status, winner: endData.winner), + ); + if (endData.clock != null) { + newState = newState.copyWith.game.clock!( + white: endData.clock!.white, + black: endData.clock!.black, + at: DateTime.now(), + lag: null, + ); + } + state = AsyncData(newState); + case 'tvSelect': final json = event.data as Map; final eventChannel = pick(json, 'channel').asTvChannelOrNull(); diff --git a/lib/src/model/tv/tv_repository.dart b/lib/src/model/tv/tv_repository.dart index 804454ce27..480c1c721d 100644 --- a/lib/src/model/tv/tv_repository.dart +++ b/lib/src/model/tv/tv_repository.dart @@ -2,9 +2,9 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import './tv_channel.dart'; import './tv_game.dart'; @@ -17,10 +17,7 @@ class TvRepository { final http.Client client; Future channels() { - return client.readJson( - Uri(path: '/api/tv/channels'), - mapper: _tvGamesFromJson, - ); + return client.readJson(Uri(path: '/api/tv/channels'), mapper: _tvGamesFromJson); } } @@ -33,12 +30,11 @@ TvChannels _tvGamesFromJson(Map json) { }); } -TvGame _tvGameFromJson(Map json) => - _tvGameFromPick(pick(json).required()); +TvGame _tvGameFromJson(Map json) => _tvGameFromPick(pick(json).required()); TvGame _tvGameFromPick(RequiredPick pick) => TvGame( - user: pick('user').asLightUserOrThrow(), - rating: pick('rating').asIntOrNull(), - id: pick('gameId').asGameIdOrThrow(), - side: pick('color').asSideOrNull(), - ); + user: pick('user').asLightUserOrThrow(), + rating: pick('rating').asIntOrNull(), + id: pick('gameId').asGameIdOrThrow(), + side: pick('color').asSideOrNull(), +); diff --git a/lib/src/model/user/leaderboard.dart b/lib/src/model/user/leaderboard.dart index dc4b73d822..1df7f17760 100644 --- a/lib/src/model/user/leaderboard.dart +++ b/lib/src/model/user/leaderboard.dart @@ -39,11 +39,6 @@ class LeaderboardUser with _$LeaderboardUser { required int progress, }) = _LeaderboardUser; - LightUser get lightUser => LightUser( - id: id, - name: username, - title: title, - flair: flair, - isPatron: patron, - ); + LightUser get lightUser => + LightUser(id: id, name: username, title: title, flair: flair, isPatron: patron); } diff --git a/lib/src/model/user/profile.dart b/lib/src/model/user/profile.dart index 932325ea91..e737faa535 100644 --- a/lib/src/model/user/profile.dart +++ b/lib/src/model/user/profile.dart @@ -28,55 +28,49 @@ class Profile with _$Profile { factory Profile.fromPick(RequiredPick pick) { const lineSplitter = LineSplitter(); - final rawLinks = pick('links') - .letOrNull((e) => lineSplitter.convert(e.asStringOrThrow())); + final rawLinks = pick('links').letOrNull((e) => lineSplitter.convert(e.asStringOrThrow())); return Profile( - country: - pick('flag').asStringOrNull() ?? pick('country').asStringOrNull(), + country: pick('flag').asStringOrNull() ?? pick('country').asStringOrNull(), location: pick('location').asStringOrNull(), bio: pick('bio').asStringOrNull(), realName: pick('realName').asStringOrNull(), fideRating: pick('fideRating').asIntOrNull(), uscfRating: pick('uscfRating').asIntOrNull(), ecfRating: pick('ecfRating').asIntOrNull(), - links: rawLinks - ?.where((e) => e.trim().isNotEmpty) - .map((e) { - final link = SocialLink.fromUrl(e); - if (link == null) { - final uri = Uri.tryParse(e); - if (uri != null) { - return SocialLink(site: null, url: uri); - } - return null; - } - return link; - }) - .whereNotNull() - .toIList(), + links: + rawLinks + ?.where((e) => e.trim().isNotEmpty) + .map((e) { + final link = SocialLink.fromUrl(e); + if (link == null) { + final uri = Uri.tryParse(e); + if (uri != null) { + return SocialLink(site: null, url: uri); + } + return null; + } + return link; + }) + .nonNulls + .toIList(), ); } } @freezed class SocialLink with _$SocialLink { - const factory SocialLink({ - required LinkSite? site, - required Uri url, - }) = _SocialLink; + const factory SocialLink({required LinkSite? site, required Uri url}) = _SocialLink; const SocialLink._(); static SocialLink? fromUrl(String url) { - final updatedUrl = url.startsWith('http://') || url.startsWith('https://') - ? url - : 'https://$url'; + final updatedUrl = + url.startsWith('http://') || url.startsWith('https://') ? url : 'https://$url'; final uri = Uri.tryParse(updatedUrl); if (uri == null) return null; final host = uri.host.replaceAll(RegExp(r'www\.'), ''); - final site = - LinkSite.values.firstWhereOrNull((e) => e.domains.contains(host)); + final site = LinkSite.values.firstWhereOrNull((e) => e.domains.contains(host)); return site != null ? SocialLink(site: site, url: uri) : null; } @@ -101,6 +95,7 @@ enum LinkSite { ]), ), twitter('Twitter', IListConst(['twitter.com'])), + bluesky('Bluesky', IListConst(['bsky.app'])), facebook('Facebook', IListConst(['facebook.com'])), instagram('Instagram', IListConst(['instagram.com'])), youtube('YouTube', IListConst(['youtube.com'])), diff --git a/lib/src/model/user/search_history.dart b/lib/src/model/user/search_history.dart index 6ad5d6771e..70f5e6b6a8 100644 --- a/lib/src/model/user/search_history.dart +++ b/lib/src/model/user/search_history.dart @@ -2,26 +2,30 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'search_history.g.dart'; part 'search_history.freezed.dart'; @riverpod class SearchHistory extends _$SearchHistory { - static const prefKey = 'search.history'; static const maxHistory = 10; + String _storageKey(AuthSessionState? session) => + 'search.history.${session?.user.id ?? '**anon**'}'; + + SharedPreferencesWithCache get _prefs => LichessBinding.instance.sharedPreferences; + @override SearchHistoryState build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); + final session = ref.watch(authSessionProvider); + final stored = _prefs.getString(_storageKey(session)); return stored != null - ? SearchHistoryState.fromJson( - jsonDecode(stored) as Map, - ) + ? SearchHistoryState.fromJson(jsonDecode(stored) as Map) : SearchHistoryState(history: IList()); } @@ -35,24 +39,22 @@ class SearchHistory extends _$SearchHistory { } currentList.insert(0, term); final newState = SearchHistoryState(history: currentList.toIList()); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(prefKey, jsonEncode(newState.toJson())); + final session = ref.read(authSessionProvider); + await _prefs.setString(_storageKey(session), jsonEncode(newState.toJson())); state = newState; } Future clear() async { final newState = state.copyWith(history: IList()); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(prefKey, jsonEncode(newState.toJson())); + final prefKey = _storageKey(ref.read(authSessionProvider)); + await _prefs.setString(prefKey, jsonEncode(newState.toJson())); state = newState; } } @Freezed(fromJson: true, toJson: true) class SearchHistoryState with _$SearchHistoryState { - const factory SearchHistoryState({ - required IList history, - }) = _SearchHistoryState; + const factory SearchHistoryState({required IList history}) = _SearchHistoryState; factory SearchHistoryState.fromJson(Map json) => _$SearchHistoryStateFromJson(json); diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index 38fc137c5c..4df1287c7c 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -22,8 +22,7 @@ class LightUser with _$LightUser { bool? isOnline, }) = _LightUser; - factory LightUser.fromJson(Map json) => - _$LightUserFromJson(json); + factory LightUser.fromJson(Map json) => _$LightUserFromJson(json); } extension LightUserExtension on Pick { @@ -34,8 +33,8 @@ extension LightUserExtension on Pick { return value; } if (value is Map) { - final name = requiredPick('username').asStringOrNull() ?? - requiredPick('name').asStringOrThrow(); + final name = + requiredPick('username').asStringOrNull() ?? requiredPick('name').asStringOrThrow(); return LightUser( id: requiredPick('id').asUserIdOrThrow(), @@ -46,9 +45,7 @@ extension LightUserExtension on Pick { isOnline: requiredPick('online').asBoolOrNull(), ); } - throw PickException( - "value $value at $debugParsingExit can't be casted to LightUser", - ); + throw PickException("value $value at $debugParsingExit can't be casted to LightUser"); } LightUser? asLightUserOrNull() { @@ -85,24 +82,16 @@ class User with _$User { bool? followable, bool? following, bool? blocking, - bool? followsYou, bool? canChallenge, }) = _User; - LightUser get lightUser => LightUser( - id: id, - name: username, - title: title, - isPatron: isPatron, - flair: flair, - ); + LightUser get lightUser => + LightUser(id: id, name: username, title: title, isPatron: isPatron, flair: flair); - factory User.fromServerJson(Map json) => - User.fromPick(pick(json).required()); + factory User.fromServerJson(Map json) => User.fromPick(pick(json).required()); factory User.fromPick(RequiredPick pick) { - final receivedPerfsMap = - pick('perfs').asMapOrEmpty>(); + final receivedPerfsMap = pick('perfs').asMapOrEmpty>(); return User( id: pick('id').asUserIdOrThrow(), username: pick('username').asStringOrThrow(), @@ -128,7 +117,6 @@ class User with _$User { followable: pick('followable').asBoolOrNull(), following: pick('following').asBoolOrNull(), blocking: pick('blocking').asBoolOrNull(), - followsYou: pick('followsYou').asBoolOrNull(), canChallenge: pick('canChallenge').asBoolOrNull(), ); } @@ -157,32 +145,28 @@ class UserGameCount with _$UserGameCount { UserGameCount.fromPick(pick(json).required()); factory UserGameCount.fromPick(RequiredPick pick) => UserGameCount( - all: pick('all').asIntOrThrow(), - // TODO(#454): enable rest of fields when needed for filtering - // rated: pick('rated').asIntOrThrow(), - // ai: pick('ai').asIntOrThrow(), - // draw: pick('draw').asIntOrThrow(), - // drawH: pick('drawH').asIntOrThrow(), - // win: pick('win').asIntOrThrow(), - // winH: pick('winH').asIntOrThrow(), - // loss: pick('loss').asIntOrThrow(), - // lossH: pick('lossH').asIntOrThrow(), - // bookmark: pick('bookmark').asIntOrThrow(), - // playing: pick('playing').asIntOrThrow(), - // imported: pick('import').asIntOrThrow(), - // me: pick('me').asIntOrThrow(), - ); + all: pick('all').asIntOrThrow(), + // TODO(#454): enable rest of fields when needed for filtering + // rated: pick('rated').asIntOrThrow(), + // ai: pick('ai').asIntOrThrow(), + // draw: pick('draw').asIntOrThrow(), + // drawH: pick('drawH').asIntOrThrow(), + // win: pick('win').asIntOrThrow(), + // winH: pick('winH').asIntOrThrow(), + // loss: pick('loss').asIntOrThrow(), + // lossH: pick('lossH').asIntOrThrow(), + // bookmark: pick('bookmark').asIntOrThrow(), + // playing: pick('playing').asIntOrThrow(), + // imported: pick('import').asIntOrThrow(), + // me: pick('me').asIntOrThrow(), + ); } @freezed class PlayTime with _$PlayTime { - const factory PlayTime({ - required Duration total, - required Duration tv, - }) = _PlayTime; + const factory PlayTime({required Duration total, required Duration tv}) = _PlayTime; - factory PlayTime.fromJson(Map json) => - PlayTime.fromPick(pick(json).required()); + factory PlayTime.fromJson(Map json) => PlayTime.fromPick(pick(json).required()); factory PlayTime.fromPick(RequiredPick pick) { return PlayTime( @@ -205,25 +189,24 @@ class UserPerf with _$UserPerf { bool? provisional, }) = _UserPerf; - factory UserPerf.fromJson(Map json) => - UserPerf.fromPick(pick(json).required()); + factory UserPerf.fromJson(Map json) => UserPerf.fromPick(pick(json).required()); factory UserPerf.fromPick(RequiredPick pick) => UserPerf( - rating: pick('rating').asIntOrThrow(), - ratingDeviation: pick('rd').asIntOrThrow(), - progression: pick('prog').asIntOrThrow(), - games: pick('games').asIntOrNull(), - runs: pick('runs').asIntOrNull(), - provisional: pick('prov').asBoolOrNull(), - ); + rating: pick('rating').asIntOrThrow(), + ratingDeviation: pick('rd').asIntOrThrow(), + progression: pick('prog').asIntOrThrow(), + games: pick('games').asIntOrNull(), + runs: pick('runs').asIntOrNull(), + provisional: pick('prov').asBoolOrNull(), + ); factory UserPerf.fromJsonStreak(Map json) => UserPerf( - rating: UserActivityStreak.fromJson(json).score, - ratingDeviation: 0, - progression: 0, - runs: UserActivityStreak.fromJson(json).runs, - provisional: null, - ); + rating: UserActivityStreak.fromJson(json).score, + ratingDeviation: 0, + progression: 0, + runs: UserActivityStreak.fromJson(json).runs, + provisional: null, + ); int get numberOfGamesOrRuns => games ?? runs ?? 0; } @@ -241,11 +224,11 @@ class UserStatus with _$UserStatus { UserStatus.fromPick(pick(json).required()); factory UserStatus.fromPick(RequiredPick pick) => UserStatus( - id: pick('id').asUserIdOrThrow(), - name: pick('name').asStringOrThrow(), - online: pick('online').asBoolOrNull(), - playing: pick('playing').asBoolOrNull(), - ); + id: pick('id').asUserIdOrThrow(), + name: pick('name').asStringOrThrow(), + online: pick('online').asBoolOrNull(), + playing: pick('playing').asBoolOrNull(), + ); } @freezed @@ -262,31 +245,25 @@ class UserActivityTournament with _$UserActivityTournament { factory UserActivityTournament.fromJson(Map json) => UserActivityTournament.fromPick(pick(json).required()); - factory UserActivityTournament.fromPick(RequiredPick pick) => - UserActivityTournament( - id: pick('tournament', 'id').asStringOrThrow(), - name: pick('tournament', 'name').asStringOrThrow(), - nbGames: pick('nbGames').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - rank: pick('rank').asIntOrThrow(), - rankPercent: pick('rankPercent').asIntOrThrow(), - ); + factory UserActivityTournament.fromPick(RequiredPick pick) => UserActivityTournament( + id: pick('tournament', 'id').asStringOrThrow(), + name: pick('tournament', 'name').asStringOrThrow(), + nbGames: pick('nbGames').asIntOrThrow(), + score: pick('score').asIntOrThrow(), + rank: pick('rank').asIntOrThrow(), + rankPercent: pick('rankPercent').asIntOrThrow(), + ); } @freezed class UserActivityStreak with _$UserActivityStreak { - const factory UserActivityStreak({ - required int runs, - required int score, - }) = _UserActivityStreak; + const factory UserActivityStreak({required int runs, required int score}) = _UserActivityStreak; factory UserActivityStreak.fromJson(Map json) => UserActivityStreak.fromPick(pick(json).required()); - factory UserActivityStreak.fromPick(RequiredPick pick) => UserActivityStreak( - runs: pick('runs').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - ); + factory UserActivityStreak.fromPick(RequiredPick pick) => + UserActivityStreak(runs: pick('runs').asIntOrThrow(), score: pick('score').asIntOrThrow()); } @freezed @@ -303,12 +280,12 @@ class UserActivityScore with _$UserActivityScore { UserActivityScore.fromPick(pick(json).required()); factory UserActivityScore.fromPick(RequiredPick pick) => UserActivityScore( - win: pick('win').asIntOrThrow(), - loss: pick('loss').asIntOrThrow(), - draw: pick('draw').asIntOrThrow(), - ratingBefore: pick('rp', 'before').asIntOrThrow(), - ratingAfter: pick('rp', 'after').asIntOrThrow(), - ); + win: pick('win').asIntOrThrow(), + loss: pick('loss').asIntOrThrow(), + draw: pick('draw').asIntOrThrow(), + ratingBefore: pick('rp', 'before').asIntOrThrow(), + ratingAfter: pick('rp', 'after').asIntOrThrow(), + ); } @freezed @@ -412,13 +389,10 @@ class UserPerfGame with _$UserPerfGame { String? opponentTitle, }) = _UserPerfGame; - LightUser? get opponent => opponentId != null && opponentName != null - ? LightUser( - id: UserId(opponentId!), - name: opponentName!, - title: opponentTitle, - ) - : null; + LightUser? get opponent => + opponentId != null && opponentName != null + ? LightUser(id: UserId(opponentId!), name: opponentName!, title: opponentTitle) + : null; } @immutable @@ -426,10 +400,7 @@ class UserRatingHistoryPerf { final Perf perf; final IList points; - const UserRatingHistoryPerf({ - required this.perf, - required this.points, - }); + const UserRatingHistoryPerf({required this.perf, required this.points}); } @immutable @@ -437,8 +408,5 @@ class UserRatingHistoryPoint { final DateTime date; final int elo; - const UserRatingHistoryPoint({ - required this.date, - required this.elo, - }); + const UserRatingHistoryPoint({required this.date, required this.elo}); } diff --git a/lib/src/model/user/user_repository.dart b/lib/src/model/user/user_repository.dart index d77c4a7416..0fc6598b88 100644 --- a/lib/src/model/user/user_repository.dart +++ b/lib/src/model/user/user_repository.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'streamer.dart'; @@ -17,19 +17,13 @@ class UserRepository { Future getUser(UserId id, {bool withCanChallenge = false}) { return client.readJson( - Uri( - path: '/api/user/$id', - queryParameters: withCanChallenge ? {'challenge': 'true'} : null, - ), + Uri(path: '/api/user/$id', queryParameters: withCanChallenge ? {'challenge': 'true'} : null), mapper: User.fromServerJson, ); } Future> getOnlineBots() { - return client.readNdJsonList( - Uri(path: '/api/bot/online'), - mapper: User.fromServerJson, - ); + return client.readNdJsonList(Uri(path: '/api/bot/online'), mapper: User.fromServerJson); } Future getPerfStats(UserId id, Perf perf) { @@ -41,40 +35,25 @@ class UserRepository { Future> getUsersStatuses(ISet ids) { return client.readJsonList( - Uri( - path: '/api/users/status', - queryParameters: {'ids': ids.join(',')}, - ), + Uri(path: '/api/users/status', queryParameters: {'ids': ids.join(',')}), mapper: UserStatus.fromJson, ); } Future> getActivity(UserId id) { - return client.readJsonList( - Uri(path: '/api/user/$id/activity'), - mapper: _userActivityFromJson, - ); + return client.readJsonList(Uri(path: '/api/user/$id/activity'), mapper: _userActivityFromJson); } Future> getLiveStreamers() { - return client.readJsonList( - Uri(path: '/api/streamer/live'), - mapper: _streamersFromJson, - ); + return client.readJsonList(Uri(path: '/api/streamer/live'), mapper: _streamersFromJson); } Future> getTop1() { - return client.readJson( - Uri(path: '/api/player/top/1/standard'), - mapper: _top1FromJson, - ); + return client.readJson(Uri(path: '/api/player/top/1/standard'), mapper: _top1FromJson); } Future getLeaderboard() { - return client.readJson( - Uri(path: '/api/player'), - mapper: _leaderboardFromJson, - ); + return client.readJson(Uri(path: '/api/player'), mapper: _leaderboardFromJson); } Future> autocompleteUser(String term) { @@ -95,14 +74,10 @@ class UserRepository { } } -UserRatingHistoryPerf? _ratingHistoryFromJson( - Map json, -) => +UserRatingHistoryPerf? _ratingHistoryFromJson(Map json) => _ratingHistoryFromPick(pick(json).required()); -UserRatingHistoryPerf? _ratingHistoryFromPick( - RequiredPick pick, -) { +UserRatingHistoryPerf? _ratingHistoryFromPick(RequiredPick pick) { final perf = pick('name').asPerfOrNull(); if (perf == null) { @@ -111,17 +86,14 @@ UserRatingHistoryPerf? _ratingHistoryFromPick( return UserRatingHistoryPerf( perf: perf, - points: pick('points').asListOrThrow((point) { - final values = point.asListOrThrow((point) => point.asIntOrThrow()); - return UserRatingHistoryPoint( - date: DateTime.utc( - values[0], - values[1] + 1, - values[2], - ), - elo: values[3], - ); - }).toIList(), + points: + pick('points').asListOrThrow((point) { + final values = point.asListOrThrow((point) => point.asIntOrThrow()); + return UserRatingHistoryPoint( + date: DateTime.utc(values[0], values[1] + 1, values[2]), + elo: values[3], + ); + }).toIList(), ); } @@ -130,17 +102,14 @@ IList _autocompleteFromJson(Map json) => _autocompleteFromPick(pick(json).required()); IList _autocompleteFromPick(RequiredPick pick) { - return pick('result') - .asListOrThrow((userPick) => userPick.asLightUserOrThrow()) - .toIList(); + return pick('result').asListOrThrow((userPick) => userPick.asLightUserOrThrow()).toIList(); } UserActivity _userActivityFromJson(Map json) => _userActivityFromPick(pick(json).required()); UserActivity _userActivityFromPick(RequiredPick pick) { - final receivedGamesMap = - pick('games').asMapOrEmpty>(); + final receivedGamesMap = pick('games').asMapOrEmpty>(); final games = IMap({ for (final entry in receivedGamesMap.entries) @@ -148,8 +117,10 @@ UserActivity _userActivityFromPick(RequiredPick pick) { Perf.nameMap.get(entry.key)!: UserActivityScore.fromJson(entry.value), }); - final bestTour = pick('tournaments', 'best') - .asListOrNull((p0) => UserActivityTournament.fromPick(p0)); + final bestTour = pick( + 'tournaments', + 'best', + ).asListOrNull((p0) => UserActivityTournament.fromPick(p0)); return UserActivity( startTime: pick('interval', 'start').asDateTimeFromMillisecondsOrThrow(), @@ -162,12 +133,10 @@ UserActivity _userActivityFromPick(RequiredPick pick) { puzzles: pick('puzzles', 'score').letOrNull(UserActivityScore.fromPick), streak: pick('streak').letOrNull(UserActivityStreak.fromPick), storm: pick('storm').letOrNull(UserActivityStreak.fromPick), - correspondenceEnds: pick('correspondenceEnds', 'score') - .letOrNull(UserActivityScore.fromPick), + correspondenceEnds: pick('correspondenceEnds', 'score').letOrNull(UserActivityScore.fromPick), correspondenceMovesNb: pick('correspondenceMoves', 'nb').asIntOrNull(), - correspondenceGamesNb: pick('correspondenceMoves', 'games') - .asListOrNull((p) => p('id').asStringOrThrow()) - ?.length, + correspondenceGamesNb: + pick('correspondenceMoves', 'games').asListOrNull((p) => p('id').asStringOrThrow())?.length, ); } @@ -214,11 +183,8 @@ UserPerfStats _userPerfStatsFromPick(RequiredPick pick) { maxPlayStreak: playStreak('nb', 'max').letOrNull(_userStreakFromPick), curTimeStreak: playStreak('time', 'cur').letOrNull(_userStreakFromPick), maxTimeStreak: playStreak('time', 'max').letOrNull(_userStreakFromPick), - worstLosses: IList( - stat('worstLosses', 'results').asListOrNull(_userPerfGameFromPick), - ), - bestWins: - IList(stat('bestWins', 'results').asListOrNull(_userPerfGameFromPick)), + worstLosses: IList(stat('worstLosses', 'results').asListOrNull(_userPerfGameFromPick)), + bestWins: IList(stat('bestWins', 'results').asListOrNull(_userPerfGameFromPick)), ); } @@ -272,8 +238,7 @@ UserPerfGame _userPerfGameFromPick(RequiredPick pick) { ); } -Streamer _streamersFromJson(Map json) => - _streamersFromPick(pick(json).required()); +Streamer _streamersFromJson(Map json) => _streamersFromPick(pick(json).required()); Streamer _streamersFromPick(RequiredPick pick) { final stream = pick('stream'); @@ -306,8 +271,7 @@ Leaderboard _leaderBoardFromPick(RequiredPick pick) { ultrabullet: pick('ultraBullet').asListOrEmpty(_leaderboardUserFromPick), crazyhouse: pick('crazyhouse').asListOrEmpty(_leaderboardUserFromPick), chess960: pick('chess960').asListOrEmpty(_leaderboardUserFromPick), - kingOfThehill: - pick('kingOfTheHill').asListOrEmpty(_leaderboardUserFromPick), + kingOfThehill: pick('kingOfTheHill').asListOrEmpty(_leaderboardUserFromPick), threeCheck: pick('threeCheck').asListOrEmpty(_leaderboardUserFromPick), antichess: pick('antichess').asListOrEmpty(_leaderboardUserFromPick), atomic: pick('atomic').asListOrEmpty(_leaderboardUserFromPick), @@ -326,14 +290,14 @@ LeaderboardUser _leaderboardUserFromPick(RequiredPick pick) { flair: pick('flair').asStringOrNull(), patron: pick('patron').asBoolOrNull(), online: pick('online').asBoolOrNull(), - rating: pick('perfs') - .letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating')) - .asIntOrThrow(), - progress: pick('perfs') - .letOrThrow( - (prefsPick) => prefsPick(prefMap.keys.first, 'progress'), - ) - .asIntOrThrow(), + rating: + pick( + 'perfs', + ).letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating')).asIntOrThrow(), + progress: + pick( + 'perfs', + ).letOrThrow((prefsPick) => prefsPick(prefMap.keys.first, 'progress')).asIntOrThrow(), ); } diff --git a/lib/src/model/user/user_repository_providers.dart b/lib/src/model/user/user_repository_providers.dart index 9e45d0917d..c950b4d469 100644 --- a/lib/src/model/user/user_repository_providers.dart +++ b/lib/src/model/user/user_repository_providers.dart @@ -1,8 +1,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'streamer.dart'; @@ -14,56 +15,46 @@ part 'user_repository_providers.g.dart'; const _kAutoCompleteDebounceTimer = Duration(milliseconds: 300); @riverpod -Future user(UserRef ref, {required UserId id}) async { - return ref.withClient( - (client) => UserRepository(client).getUser(id), - ); +Future user(Ref ref, {required UserId id}) async { + return ref.withClient((client) => UserRepository(client).getUser(id)); } @riverpod -Future<(User, UserStatus)> userAndStatus( - UserAndStatusRef ref, { - required UserId id, -}) async { - return ref.withClient( - (client) async { - final repo = UserRepository(client); - return Future.wait( - [ - repo.getUser(id, withCanChallenge: true), - repo.getUsersStatuses({id}.lock), - ], - eagerError: true, - ).then( - (value) => (value[0] as User, (value[1] as IList).first), - ); - }, +Future> userActivity(Ref ref, {required UserId id}) async { + return ref.withClientCacheFor( + (client) => UserRepository(client).getActivity(id), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), ); } @riverpod -Future userPerfStats( - UserPerfStatsRef ref, { - required UserId id, - required Perf perf, -}) async { - return ref.withClient( - (client) => UserRepository(client).getPerfStats(id, perf), - ); +Future<(User, UserStatus)> userAndStatus(Ref ref, {required UserId id}) async { + return ref.withClient((client) async { + final repo = UserRepository(client); + return Future.wait([ + repo.getUser(id, withCanChallenge: true), + repo.getUsersStatuses({id}.lock), + ], eagerError: true).then((value) => (value[0] as User, (value[1] as IList).first)); + }); } @riverpod -Future> userStatuses( - UserStatusesRef ref, { - required ISet ids, -}) async { - return ref.withClient( - (client) => UserRepository(client).getUsersStatuses(ids), - ); +Future userPerfStats(Ref ref, {required UserId id, required Perf perf}) async { + return ref.withClient((client) => UserRepository(client).getPerfStats(id, perf)); } @riverpod -Future> liveStreamers(LiveStreamersRef ref) async { +Future> userStatuses(Ref ref, {required ISet ids}) async { + return ref.withClient((client) => UserRepository(client).getUsersStatuses(ids)); +} + +@riverpod +Future> liveStreamers(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getLiveStreamers(), const Duration(minutes: 1), @@ -71,7 +62,7 @@ Future> liveStreamers(LiveStreamersRef ref) async { } @riverpod -Future> top1(Top1Ref ref) async { +Future> top1(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getTop1(), const Duration(hours: 12), @@ -79,7 +70,7 @@ Future> top1(Top1Ref ref) async { } @riverpod -Future leaderboard(LeaderboardRef ref) async { +Future leaderboard(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getLeaderboard(), const Duration(hours: 2), @@ -87,10 +78,7 @@ Future leaderboard(LeaderboardRef ref) async { } @riverpod -Future> autoCompleteUser( - AutoCompleteUserRef ref, - String term, -) async { +Future> autoCompleteUser(Ref ref, String term) async { // debounce calls as user might be typing var didDispose = false; ref.onDispose(() => didDispose = true); @@ -99,16 +87,11 @@ Future> autoCompleteUser( throw Exception('Cancelled'); } - return ref.withClient( - (client) => UserRepository(client).autocompleteUser(term), - ); + return ref.withClient((client) => UserRepository(client).autocompleteUser(term)); } @riverpod -Future> userRatingHistory( - UserRatingHistoryRef ref, { - required UserId id, -}) async { +Future> userRatingHistory(Ref ref, {required UserId id}) async { return ref.withClientCacheFor( (client) => UserRepository(client).getRatingHistory(id), const Duration(minutes: 1), diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index ccfb8e605d..26ff184dd6 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/home/home_tab_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart'; @@ -18,7 +18,6 @@ enum BottomTab { watch, settings; - // TODO use translations when short strings are available String label(AppLocalizations strings) { switch (this) { case BottomTab.home: @@ -65,8 +64,7 @@ enum BottomTab { } } -final currentBottomTabProvider = - StateProvider((ref) => BottomTab.home); +final currentBottomTabProvider = StateProvider((ref) => BottomTab.home); final currentNavigatorKeyProvider = Provider>((ref) { final currentTab = ref.watch(currentBottomTabProvider); @@ -112,8 +110,7 @@ final toolsScrollController = ScrollController(debugLabel: 'ToolsScroll'); final watchScrollController = ScrollController(debugLabel: 'WatchScroll'); final settingsScrollController = ScrollController(debugLabel: 'SettingsScroll'); -final RouteObserver> rootNavPageRouteObserver = - RouteObserver>(); +final RouteObserver> rootNavPageRouteObserver = RouteObserver>(); final _cupertinoTabController = CupertinoTabController(); @@ -133,17 +130,10 @@ class BottomNavScaffold extends ConsumerWidget { switch (Theme.of(context).platform) { case TargetPlatform.android: return Scaffold( - body: _TabSwitchingView( - currentTab: currentTab, - tabBuilder: _androidTabBuilder, - ), + body: _TabSwitchingView(currentTab: currentTab, tabBuilder: _androidTabBuilder), bottomNavigationBar: Consumer( builder: (context, ref, _) { - final isOnline = ref - .watch(connectivityChangesProvider) - .valueOrNull - ?.isOnline ?? - true; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? true; return NavigationBar( selectedIndex: currentTab.index, destinations: [ @@ -153,16 +143,13 @@ class BottomNavScaffold extends ConsumerWidget { label: tab.label(context.l10n), ), ], - onDestinationSelected: (i) => - _onItemTapped(ref, i, isOnline: isOnline), + onDestinationSelected: (i) => _onItemTapped(ref, i, isOnline: isOnline), ); }, ), ); case TargetPlatform.iOS: - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? - true; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? true; return CupertinoTabScaffold( tabBuilder: _iOSTabBuilder, controller: _cupertinoTabController, @@ -192,11 +179,7 @@ class BottomNavScaffold extends ConsumerWidget { void _onItemTapped(WidgetRef ref, int index, {required bool isOnline}) { if (index == BottomTab.watch.index && !isOnline) { _cupertinoTabController.index = ref.read(currentBottomTabProvider).index; - showPlatformSnackbar( - ref.context, - 'Not available in offline mode', - type: SnackBarType.info, - ); + showPlatformSnackbar(ref.context, 'Not available in offline mode', type: SnackBarType.info); return; } @@ -307,10 +290,7 @@ Widget _iOSTabBuilder(BuildContext context, int index) { /// A widget laying out multiple tabs with only one active tab being built /// at a time and on stage. Off stage tabs' animations are stopped. class _TabSwitchingView extends StatefulWidget { - const _TabSwitchingView({ - required this.currentTab, - required this.tabBuilder, - }); + const _TabSwitchingView({required this.currentTab, required this.tabBuilder}); final BottomTab currentTab; final IndexedWidgetBuilder tabBuilder; @@ -353,24 +333,19 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { if (tabFocusNodes.length != BottomTab.values.length) { if (tabFocusNodes.length > BottomTab.values.length) { discardedNodes.addAll(tabFocusNodes.sublist(BottomTab.values.length)); - tabFocusNodes.removeRange( - BottomTab.values.length, - tabFocusNodes.length, - ); + tabFocusNodes.removeRange(BottomTab.values.length, tabFocusNodes.length); } else { tabFocusNodes.addAll( List.generate( BottomTab.values.length - tabFocusNodes.length, (int index) => FocusScopeNode( - debugLabel: - '$BottomNavScaffold Tab ${index + tabFocusNodes.length}', + debugLabel: '$BottomNavScaffold Tab ${index + tabFocusNodes.length}', ), ), ); } } - FocusScope.of(context) - .setFirstFocus(tabFocusNodes[widget.currentTab.index]); + FocusScope.of(context).setFirstFocus(tabFocusNodes[widget.currentTab.index]); } @override @@ -402,9 +377,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { node: tabFocusNodes[index], child: Builder( builder: (BuildContext context) { - return shouldBuildTab[index] - ? widget.tabBuilder(context, index) - : Container(); + return shouldBuildTab[index] ? widget.tabBuilder(context, index) : Container(); }, ), ), @@ -421,20 +394,20 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { class _MaterialTabView extends ConsumerStatefulWidget { const _MaterialTabView({ - // ignore: unused_element + // ignore: unused_element_parameter super.key, required this.tab, this.builder, this.navigatorKey, - // ignore: unused_element + // ignore: unused_element_parameter this.routes, - // ignore: unused_element + // ignore: unused_element_parameter this.onGenerateRoute, - // ignore: unused_element + // ignore: unused_element_parameter this.onUnknownRoute, - // ignore: unused_element + // ignore: unused_element_parameter this.navigatorObservers = const [], - // ignore: unused_element + // ignore: unused_element_parameter this.restorationScopeId, }); @@ -491,11 +464,12 @@ class _MaterialTabViewState extends ConsumerState<_MaterialTabView> { final currentTab = ref.watch(currentBottomTabProvider); final enablePopHandler = currentTab == widget.tab; return NavigatorPopHandler( - onPop: enablePopHandler - ? () { - widget.navigatorKey?.currentState?.maybePop(); - } - : null, + onPopWithResult: + enablePopHandler + ? (_) { + widget.navigatorKey?.currentState?.maybePop(); + } + : null, enabled: enablePopHandler, child: Navigator( key: widget.navigatorKey, @@ -516,10 +490,7 @@ class _MaterialTabViewState extends ConsumerState<_MaterialTabView> { routeBuilder = widget.routes![name]; } if (routeBuilder != null) { - return MaterialPageRoute( - builder: routeBuilder, - settings: settings, - ); + return MaterialPageRoute(builder: routeBuilder, settings: settings); } if (widget.onGenerateRoute != null) { return widget.onGenerateRoute!(settings); diff --git a/lib/src/network/connectivity.dart b/lib/src/network/connectivity.dart new file mode 100644 index 0000000000..f85dbce3cf --- /dev/null +++ b/lib/src/network/connectivity.dart @@ -0,0 +1,191 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connectivity.g.dart'; + +final _logger = Logger('Connectivity'); + +/// A provider that exposes a [Connectivity] instance. +@Riverpod(keepAlive: true) +Connectivity connectivityPlugin(Ref _) => Connectivity(); + +/// This provider is used to check the device's connectivity status, reacting to +/// changes in connectivity and app lifecycle events. +/// +/// - Uses the [Connectivity] plugin to listen to connectivity changes +/// - Uses [AppLifecycleListener] to check connectivity on app resume +@Riverpod(keepAlive: true) +class ConnectivityChanges extends _$ConnectivityChanges { + StreamSubscription>? _connectivitySubscription; + AppLifecycleListener? _appLifecycleListener; + + final _connectivityChangesDebouncer = Debouncer(const Duration(seconds: 5)); + + Client get _defaultClient => ref.read(defaultClientProvider); + Connectivity get _connectivity => ref.read(connectivityPluginProvider); + + @override + Future build() { + ref.onDispose(() { + _connectivitySubscription?.cancel(); + _appLifecycleListener?.dispose(); + _connectivityChangesDebouncer.dispose(); + }); + + _connectivitySubscription?.cancel(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen((result) { + _connectivityChangesDebouncer(() => _onConnectivityChange(result)); + }); + + final AppLifecycleState? appState = WidgetsBinding.instance.lifecycleState; + + _appLifecycleListener = AppLifecycleListener(onStateChange: _onAppLifecycleChange); + + return _connectivity.checkConnectivity().then((r) => _getConnectivityStatus(r, appState)); + } + + Future _onAppLifecycleChange(AppLifecycleState appState) async { + if (!state.hasValue) { + return; + } + + if (appState == AppLifecycleState.resumed) { + final newConn = await _connectivity.checkConnectivity().then( + (r) => _getConnectivityStatus(r, appState), + ); + + state = AsyncValue.data(newConn); + } else { + final (:isOnline, appState: _) = state.requireValue; + state = AsyncValue.data((isOnline: isOnline, appState: appState)); + } + } + + Future _onConnectivityChange(List result) async { + if (!state.hasValue) { + return; + } + + final wasOnline = state.requireValue.isOnline; + + _logger.fine('Connectivity changed: $result'); + final newIsOnline = await isOnline(_defaultClient); + _logger.fine('Online check result: $isOnline'); + + if (newIsOnline != wasOnline) { + _logger.info('Connectivity status: $result, isOnline: $isOnline'); + state = AsyncValue.data((isOnline: newIsOnline, appState: state.valueOrNull?.appState)); + } + } + + Future _getConnectivityStatus( + List result, + AppLifecycleState? appState, + ) async { + final status = (isOnline: await isOnline(_defaultClient), appState: appState); + _logger.info('Connectivity status: $result, isOnline: ${status.isOnline}'); + return status; + } +} + +typedef ConnectivityStatus = ({bool isOnline, AppLifecycleState? appState}); + +final _internetCheckUris = [ + Uri.parse('https://www.gstatic.com/generate_204'), + Uri.parse('$kLichessCDNHost/assets/logo/lichess-favicon-32.png'), +]; + +/// Checks if the device is online by making a HEAD request to a list of URIs. +Future isOnline(Client client) { + final completer = Completer(); + try { + int remaining = _internetCheckUris.length; + final futures = _internetCheckUris.map( + (uri) => client + .head(uri) + .timeout(const Duration(seconds: 10)) + .then((response) => true, onError: (_) => false), + ); + for (final future in futures) { + future.then((value) { + remaining--; + if (!completer.isCompleted) { + if (value == true) { + completer.complete(true); + } else if (remaining == 0) { + completer.complete(false); + } + } + }); + } + } catch (_) { + completer.complete(false); + } + return completer.future; +} + +extension AsyncValueConnectivity on AsyncValue { + /// Switches between device's connectivity status. + /// + /// Using this method assumes the the device is offline when the status is + /// not yet available (i.e. [AsyncValue.isLoading]. + /// If you want to handle the loading state separately, use + /// [whenIsLoading] instead. + /// + /// This method is similar to [AsyncValueX.maybeWhen], but it takes two + /// functions, one for when the device is online and another for when it is + /// offline. + /// + /// Example: + /// ```dart + /// final status = ref.watch(connectivityChangesProvider); + /// final result = status.whenIs( + /// online: () => 'Online', + /// offline: () => 'Offline', + /// ); + /// ``` + R whenIs({required R Function() online, required R Function() offline}) { + return maybeWhen( + skipLoadingOnReload: true, + data: (status) => status.isOnline ? online() : offline(), + orElse: offline, + ); + } + + /// Switches between device's connectivity status, but handling the loading state. + /// + /// This method is similar to [AsyncValueX.when], but it takes three + /// functions, one for when the device is online, another for when it is + /// offline, and the last for when the status is still loading. + /// + /// Example: + /// ```dart + /// final status = ref.watch(connectivityChangesProvider); + /// final result = status.whenIsLoading( + /// online: () => 'Online', + /// offline: () => 'Offline', + /// loading: () => 'Loading', + /// ); + /// ``` + R whenIsLoading({ + required R Function() online, + required R Function() offline, + required R Function() loading, + }) { + return when( + skipLoadingOnReload: true, + data: (status) => status.isOnline ? online() : offline(), + loading: loading, + error: (error, stack) => offline(), + ); + } +} diff --git a/lib/src/model/common/http.dart b/lib/src/network/http.dart similarity index 66% rename from lib/src/model/common/http.dart rename to lib/src/network/http.dart index 0c308bf32f..fb0c115f04 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/network/http.dart @@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show + BaseClient, BaseRequest, BaseResponse, Client, @@ -23,10 +24,8 @@ import 'package:http/retry.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/utils/device_info.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -46,35 +45,41 @@ Uri lichessUri(String unencodedPath, [Map? queryParameters]) => /// Creates the appropriate http client for the platform. /// /// Do not use directly, use [defaultClient] or [lichessClient] instead. -Client httpClientFactory() { - const userAgent = 'Lichess Mobile'; - if (Platform.isAndroid) { - final engine = CronetEngine.build( - cacheMode: CacheMode.memory, - cacheMaxSize: _maxCacheSize, - userAgent: userAgent, - ); - return CronetClient.fromCronetEngine(engine); - } +class HttpClientFactory { + Client call() { + const userAgent = 'Lichess Mobile'; + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: _maxCacheSize, + userAgent: userAgent, + ); + return CronetClient.fromCronetEngine(engine); + } - if (Platform.isIOS || Platform.isMacOS) { - final config = URLSessionConfiguration.ephemeralSessionConfiguration() - ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) - ..httpAdditionalHeaders = {'User-Agent': userAgent}; - return CupertinoClient.fromSessionConfiguration(config); - } + if (Platform.isIOS || Platform.isMacOS) { + final config = + URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) + ..httpAdditionalHeaders = {'User-Agent': userAgent}; + return CupertinoClient.fromSessionConfiguration(config); + } - return IOClient(HttpClient()..userAgent = userAgent); + return IOClient(HttpClient()..userAgent = userAgent); + } } +@Riverpod(keepAlive: true) +HttpClientFactory httpClientFactory(Ref _) => HttpClientFactory(); + /// The default http client. /// /// This client is used for all requests that don't go to the lichess server, for /// example, requests to lichess CDN, or other APIs. /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -Client defaultClient(DefaultClientRef ref) { - final client = httpClientFactory(); +Client defaultClient(Ref ref) { + final client = LoggingClient(ref.read(httpClientFactoryProvider)()); ref.onDispose(() => client.close()); return client; } @@ -83,11 +88,11 @@ Client defaultClient(DefaultClientRef ref) { /// /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -LichessClient lichessClient(LichessClientRef ref) { +LichessClient lichessClient(Ref ref) { final client = LichessClient( // Retry just once, after 500ms, on 429 Too Many Requests. RetryClient( - httpClientFactory(), + ref.read(httpClientFactoryProvider)(), retries: 1, delay: _defaultDelay, when: (response) => response.statusCode == 429, @@ -102,26 +107,20 @@ Duration _defaultDelay(int retryCount) => const Duration(milliseconds: 900) * math.pow(1.5, retryCount); @Riverpod(keepAlive: true) -String userAgent(UserAgentRef ref) { +String userAgent(Ref ref) { final session = ref.watch(authSessionProvider); return makeUserAgent( - ref.read(packageInfoProvider), - ref.read(deviceInfoProvider), - ref.read(sriProvider), + ref.read(preloadedDataProvider).requireValue.packageInfo, + ref.read(preloadedDataProvider).requireValue.deviceInfo, + ref.read(preloadedDataProvider).requireValue.sri, session?.user, ); } /// Creates a user-agent string with the app version, build number, and device info and possibly the user ID if a user is logged in. -String makeUserAgent( - PackageInfo info, - BaseDeviceInfo deviceInfo, - String sri, - LightUser? user, -) { - final base = - 'Lichess Mobile/${info.version} as:${user?.id ?? 'anon'} sri:$sri'; +String makeUserAgent(PackageInfo info, BaseDeviceInfo deviceInfo, String sri, LightUser? user) { + final base = 'Lichess Mobile/${info.version} as:${user?.id ?? 'anon'} sri:$sri'; if (deviceInfo is AndroidDeviceInfo) { return '$base os:Android/${deviceInfo.version.release} dev:${deviceInfo.model}'; @@ -132,16 +131,33 @@ String makeUserAgent( return base; } +/// A [Client] that logs all requests. +class LoggingClient extends BaseClient { + LoggingClient(this._inner); + + final Client _inner; + + @override + Future send(BaseRequest request) { + _logger.info('${request.method} ${request.url}'); + return _inner.send(request); + } +} + /// Lichess HTTP client. /// -/// - All requests go to the lichess server, defined in [kLichessHost]. -/// - Sets the Authorization header when a token has been stored. -/// - Sets the user-agent header with the app version, build number, and device info. If the user is logged in, it also includes the user's id. -/// - Logs all requests and responses with status code >= 400. +/// * All requests made with [head], [get], [post], [put], [patch], [delete] target +/// the lichess server, defined in [kLichessHost]. It does not apply to the low-level +/// [send] method. +/// * Sets the Authorization header when a token has been stored. +/// * Sets the user-agent header with the app version, build number, and device info. If the user is logged in, it also includes the user's id. +/// * Logs all requests and responses with status code >= 400. +/// * When a response has the 401 status, checks if the session token is still valid, +/// and deletes the session if it's not. class LichessClient implements Client { LichessClient(this._inner, this._ref); - final LichessClientRef _ref; + final Ref _ref; final Client _inner; @override @@ -153,21 +169,23 @@ class LichessClient implements Client { request.headers['Authorization'] = 'Bearer $bearer'; } request.headers['User-Agent'] = makeUserAgent( - _ref.read(packageInfoProvider), - _ref.read(deviceInfoProvider), - _ref.read(sriProvider), + _ref.read(preloadedDataProvider).requireValue.packageInfo, + _ref.read(preloadedDataProvider).requireValue.deviceInfo, + _ref.read(preloadedDataProvider).requireValue.sri, session?.user, ); - _logger.info( - '${request.method} ${request.url} ${request.headers['User-Agent']}', - ); + _logger.info('${request.method} ${request.url} ${request.headers['User-Agent']}'); try { final response = await _inner.send(request); _logIfError(response); + if (response.statusCode == 401 && session != null) { + _checkSessionToken(session); + } + return response; } catch (e, st) { _logger.warning('Request to ${request.url} failed: $e', e, st); @@ -175,6 +193,18 @@ class LichessClient implements Client { } } + /// Checks if the session token is still valid, and delete session if it's not. + Future _checkSessionToken(AuthSessionState session) async { + final defaultClient = _ref.read(defaultClientProvider); + final data = await defaultClient + .postReadJson(lichessUri('/api/token/test'), mapper: (json) => json, body: session.token) + .timeout(const Duration(seconds: 5)); + if (data[session.token] == null) { + _logger.fine('Session is not active. Deleting it.'); + await _ref.read(authSessionProvider.notifier).delete(); + } + } + void _logIfError(BaseResponse response) { if (response.request != null && response.statusCode >= 400) { final request = response.request!; @@ -192,17 +222,11 @@ class LichessClient implements Client { } @override - Future head( - Uri url, { - Map? headers, - }) => + Future head(Uri url, {Map? headers}) => _sendUnstreamed('HEAD', url, headers); @override - Future get( - Uri url, { - Map? headers, - }) => + Future get(Uri url, {Map? headers}) => _sendUnstreamed('GET', url, headers); @override @@ -211,16 +235,10 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('POST', url, headers, body, encoding); + }) => _sendUnstreamed('POST', url, headers, body, encoding); @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => + Future put(Uri url, {Map? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('PUT', url, headers, body, encoding); @override @@ -229,8 +247,7 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('PATCH', url, headers, body, encoding); + }) => _sendUnstreamed('PATCH', url, headers, body, encoding); @override Future delete( @@ -238,8 +255,7 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('DELETE', url, headers, body, encoding); + }) => _sendUnstreamed('DELETE', url, headers, body, encoding); @override Future read(Uri url, {Map? headers}) async { @@ -291,12 +307,7 @@ class ServerException extends ClientException { final int statusCode; final Map? jsonError; - ServerException( - this.statusCode, - super.message, - Uri super.url, - this.jsonError, - ); + ServerException(this.statusCode, super.message, Uri super.url, this.jsonError); } /// Throws an error if [response] is not successful. @@ -345,20 +356,14 @@ extension ClientExtension on Client { _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! Map) { - _logger.severe('Could not read json object as $T: expected an object.'); - throw ClientException( - 'Could not read json object as $T: expected an object.', - url, - ); + _logger.severe('Could not read JSON object as $T: expected an object.'); + throw ClientException('Could not read JSON object as $T: expected an object.', url); } try { return mapper(json); } catch (e, st) { - _logger.severe('Could not read json object as $T: $e', e, st); - throw ClientException( - 'Could not read json object as $T: $e\n$st', - url, - ); + _logger.severe('Could not read JSON object as $T: $e', e, st); + throw ClientException('Could not read JSON object as $T: $e\n$st', url); } } @@ -377,21 +382,15 @@ extension ClientExtension on Client { _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! List) { - _logger.severe('Could not read json object as List: expected a list.'); - throw ClientException( - 'Could not read json object as List: expected a list.', - url, - ); + _logger.severe('Could not read JSON object as List: expected a list.'); + throw ClientException('Could not read JSON object as List: expected a list.', url); } final List list = []; for (final e in json) { if (e is! Map) { - _logger.severe('Could not read json object as $T: expected an object.'); - throw ClientException( - 'Could not read json object as $T: expected an object.', - url, - ); + _logger.severe('Could not read JSON object as $T: expected an object.'); + throw ClientException('Could not read JSON object as $T: expected an object.', url); } try { final mapped = mapper(e); @@ -399,8 +398,8 @@ extension ClientExtension on Client { list.add(mapped); } } catch (e, st) { - _logger.severe('Could not read json object as $T: $e', e, st); - throw ClientException('Could not read json object as $T: $e', url); + _logger.severe('Could not read JSON object as $T: $e', e, st); + throw ClientException('Could not read JSON object as $T: $e', url); } } return IList(list); @@ -419,17 +418,39 @@ extension ClientExtension on Client { }) async { final response = await get(url, headers: headers); _checkResponseSuccess(url, response); + return _readNdJsonList(response, mapper); + } + + /// Sends an HTTP GET request with the given headers to the given URL and + /// returns a Future that completes to the stream of the response as a ND-JSON + /// object mapped to T. + /// + /// The Future will emit a [ClientException] if the response doesn't have a + /// success status code or if an object in the response body can't be read + /// as ND-JSON. + Future> readNdJsonStream( + Uri url, { + Map? headers, + required T Function(Map) mapper, + }) async { + final request = Request('GET', url); + if (headers != null) request.headers.addAll(headers); + final response = await send(request); + if (response.statusCode >= 400) { + var message = 'Request to $url failed with status ${response.statusCode}'; + if (response.reasonPhrase != null) { + message = '$message: ${response.reasonPhrase}'; + } + throw ServerException(response.statusCode, '$message.', url, null); + } try { - final json = LineSplitter.split(utf8.decode(response.bodyBytes)) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) => jsonDecode(e) as Map); - return IList(json.map(mapper)); + return response.stream.map(utf8.decode).where((e) => e.isNotEmpty && e != '\n').map((e) { + final json = jsonDecode(e) as Map; + return mapper(json); + }); } catch (e) { - _logger.severe('Could not read nd-json objects as List<$T>.'); - throw ClientException( - 'Could not read nd-json objects as List<$T>: $e', - url, - ); + _logger.severe('Could not read nd-json object as $T.'); + throw ClientException('Could not read nd-json object as $T: $e', url); } } @@ -446,25 +467,18 @@ extension ClientExtension on Client { Encoding? encoding, required T Function(Map) mapper, }) async { - final response = - await post(url, headers: headers, body: body, encoding: encoding); + final response = await post(url, headers: headers, body: body, encoding: encoding); _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! Map) { _logger.severe('Could not read json object as $T: expected an object.'); - throw ClientException( - 'Could not read json object as $T: expected an object.', - url, - ); + throw ClientException('Could not read json object as $T: expected an object.', url); } try { return mapper(json); } catch (e, st) { _logger.severe('Could not read json as $T: $e', e, st); - throw ClientException( - 'Could not read json as $T: $e', - url, - ); + throw ClientException('Could not read json as $T: $e', url); } } @@ -481,19 +495,26 @@ extension ClientExtension on Client { Encoding? encoding, required T Function(Map) mapper, }) async { - final response = - await post(url, headers: headers, body: body, encoding: encoding); + final response = await post(url, headers: headers, body: body, encoding: encoding); _checkResponseSuccess(url, response); + return _readNdJsonList(response, mapper); + } + + IList _readNdJsonList(Response response, T Function(Map) mapper) { try { - final json = LineSplitter.split(utf8.decode(response.bodyBytes)) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) => jsonDecode(e) as Map); - return IList(json.map(mapper)); + return IList( + LineSplitter.split( + utf8.decode(response.bodyBytes), + ).where((e) => e.isNotEmpty && e != '\n').map((e) { + final json = jsonDecode(e) as Map; + return mapper(json); + }), + ); } catch (e) { _logger.severe('Could not read nd-json objects as List<$T>.'); throw ClientException( - 'Could not read nd-json objects as List<$T>.', - url, + 'Could not read nd-json objects as List<$T>: $e', + response.request?.url, ); } } @@ -513,19 +534,14 @@ extension ClientRefExtension on Ref { final client = read(lichessClientProvider); return await fn(client); } -} -extension ClientAutoDisposeRefExtension on AutoDisposeRef { /// Runs [fn] with a [LichessClient] and keeps the provider alive for [duration]. /// /// This is primarily used for caching network requests in a [FutureProvider]. /// /// If [fn] throws with a [SocketException], the provider is not kept alive, this /// allows to retry the request later. - Future withClientCacheFor( - Future Function(LichessClient) fn, - Duration duration, - ) async { + Future withClientCacheFor(Future Function(LichessClient) fn, Duration duration) async { final link = keepAlive(); final timer = Timer(duration, link.close); final client = read(lichessClientProvider); diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart new file mode 100644 index 0000000000..93aacb93b2 --- /dev/null +++ b/lib/src/network/socket.dart @@ -0,0 +1,619 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/bearer.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:logging/logging.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +part 'socket.g.dart'; + +const kDefaultSocketRoute = '/socket/v5'; + +const _kDefaultConnectTimeout = Duration(seconds: 10); +const _kPingDelay = Duration(milliseconds: 2500); +const _kPingMaxLag = Duration(seconds: 9); +const _kAutoReconnectDelay = Duration(milliseconds: 3500); +const _kResendAckDelay = Duration(milliseconds: 1500); +const _kIdleTimeout = Duration(seconds: 2); +const _kDisconnectOnBackgroundTimeout = Duration(minutes: 5); + +final _logger = Logger('Socket'); + +/// Set of topics that are allowed to be broadcasted to the global stream. +const _globalSocketStreamAllowedTopics = {'n', 'message', 'challenges'}; + +final _globalStreamController = StreamController.broadcast(); + +/// The global socket broadcast stream. +/// +/// Only a subset of topics are allowed to be broadcasted to the global stream: +/// - 'n' (number of players and games currently on the server) +/// - 'message' +/// - 'challenges' +final socketGlobalStream = _globalStreamController.stream; + +/// Creates a WebSocket URI for the lichess server. +Uri lichessWSUri(String unencodedPath, [Map? queryParameters]) => + kLichessWSHost.startsWith('localhost') + ? Uri( + scheme: 'ws', + host: kLichessWSHost.split(':')[0], + port: int.parse(kLichessWSHost.split(':')[1]), + path: unencodedPath, + queryParameters: queryParameters, + ) + : Uri( + scheme: 'wss', + host: kLichessWSHost, + path: unencodedPath, + queryParameters: queryParameters, + ); + +/// A lichess WebSocket client. +/// +/// Handles authentication: +/// - adds the following headers on connect: +/// - Authorization header when a token has been stored, +/// - User-Agent header +/// +/// Handles low-level ping/pong protocol, message acks, and automatic reconnections. +class SocketClient { + SocketClient( + this.route, { + required this.channelFactory, + required this.getSession, + required this.packageInfo, + required this.deviceInfo, + required this.sri, + this.onStreamListen, + this.onStreamCancel, + this.pingDelay = _kPingDelay, + this.pingMaxLag = _kPingMaxLag, + this.autoReconnectDelay = _kAutoReconnectDelay, + this.resendAckDelay = _kResendAckDelay, + }); + + final WebSocketChannelFactory channelFactory; + + final AuthSessionState? Function() getSession; + + final PackageInfo packageInfo; + + final BaseDeviceInfo deviceInfo; + + /// The Socket Random Identifier. + final String sri; + + /// The route to connect to. + final Uri route; + + /// The delay between the next ping after receiving a pong. + final Duration pingDelay; + + /// The maximum lag before considering the connection as lost. + final Duration pingMaxLag; + + /// The delay before reconnecting after a connection failure. + final Duration autoReconnectDelay; + + /// The delay before resending an ack. + final Duration resendAckDelay; + + /// Called when the first listener is added to the socket stream. + final VoidCallback? onStreamListen; + + /// Called when the last listener is removed from the socket stream. + final VoidCallback? onStreamCancel; + + late final StreamController _streamController = + StreamController.broadcast(onListen: onStreamListen, onCancel: onStreamCancel); + + late final StreamController _socketOpenController = StreamController.broadcast(); + + Completer _firstConnection = Completer(); + + Timer? _pingTimer; + Timer? _reconnectTimer; + Timer? _ackResendTimer; + int _pongCount = 0; + DateTime _lastPing = DateTime.now(); + + final _averageLag = ValueNotifier(Duration.zero); + + StreamSubscription? _socketStreamSubscription; + + /// The list of acknowledgeable messages. + final List<(DateTime, int, Map)> _acks = []; + + /// The current number of connections attempted. + int nbConnectionAttempts = 0; + + /// The current number of successful connections. + int nbConnectionSuccess = 0; + + /// The current ack id. Incremented for each ack. + int _ackId = 1; + + /// The current WebSocket channel. + WebSocketChannel? _channel; + + /// Gets the current WebSocket sink + WebSocketSink? get _sink => _channel?.sink; + + /// The socket events broadcast stream. + Stream get stream => _streamController.stream; + + /// The stream that emits each time the socket is (re)connected. + Stream get connectedStream => _socketOpenController.stream; + + /// The average lag computed from ping/pong protocol. + /// + /// A duration of zero means the socket is not connected. + ValueListenable get averageLag => _averageLag; + + /// Whether the socket is actively trying to connect or is connected. + bool get isActive => nbConnectionAttempts > 0; + + /// Whether the socket is connected. + bool get isConnected => averageLag.value != Duration.zero; + + /// Whether the client is disposed. If true the client cannot be reconnected, or + /// be listened to. + bool isDisposed = false; + + /// A [Future] that completes when the first connection is established. + Future get firstConnection => _firstConnection.future; + + /// Connect or reconnect the WebSocket. + Future connect() async { + if (isDisposed) { + throw StateError('SocketClient is disposed, cannot connect.'); + } + + _disconnect(); + _pongCount = 0; + _reconnectTimer?.cancel(); + _ackResendTimer?.cancel(); + _ackResendTimer = Timer.periodic(resendAckDelay, (_) => _resendAcks()); + + final session = getSession(); + final uri = lichessWSUri(route.path); + final Map headers = + session != null ? {'Authorization': 'Bearer ${signBearerToken(session.token)}'} : {}; + WebSocket.userAgent = makeUserAgent(packageInfo, deviceInfo, sri, session?.user); + + _logger.info('Creating WebSocket connection to $route'); + + nbConnectionAttempts++; + + try { + final channel = await channelFactory.create( + uri.toString(), + headers: headers, + timeout: _kDefaultConnectTimeout, + ); + + _channel = channel; + + _socketStreamSubscription?.cancel(); + _socketStreamSubscription = channel.stream + .map((raw) { + if (raw == '0') { + return SocketEvent.pong; + } + return SocketEvent.fromJson(jsonDecode(raw as String) as Map); + }) + .listen(_handleEvent); + + _logger.fine('WebSocket connection to $route established.'); + + nbConnectionSuccess++; + + if (nbConnectionSuccess == 1) { + _firstConnection.complete(); + } + + _averageLag.value = Duration.zero; + _sendPing(); + _schedulePing(pingDelay); + + if (_socketOpenController.hasListener) { + _socketOpenController.add(null); + } + _resendAcks(); + } catch (error) { + _logger.severe('WebSocket connection failed: $error', error); + _averageLag.value = Duration.zero; + _scheduleReconnect(autoReconnectDelay); + } + } + + /// Sends a message to the websocket. + void send(String topic, Object? data, {bool? ackable, bool? withLag}) { + Map message; + + if (ackable == true) { + final ackId = _ackId++; + message = { + 't': topic, + 'd': { + if (data != null && data is Map) ...data, + 'a': ackId, + if (withLag == true) 'l': _averageLag.value.inMilliseconds, + }, + }; + _acks.add((DateTime.now(), ackId, message)); + } else { + message = { + 't': topic, + if (data != null && data is Map) + 'd': {...data, if (withLag == true) 'l': _averageLag.value.inMilliseconds} + else if (data != null) + 'd': data, + }; + } + + _sink?.add(jsonEncode(message)); + } + + /// Closes the WebSocket connection and disposes the client. + /// + /// The [SocketPool] will call this method when the client is no longer needed. + void _dispose() { + _socketStreamSubscription?.cancel(); + _pingTimer?.cancel(); + _reconnectTimer?.cancel(); + _ackResendTimer?.cancel(); + _streamController.close(); + _averageLag.dispose(); + isDisposed = true; + _disconnect(); + } + + /// Closes the WebSocket connection when temporarily not needed (by default + /// this is when we open another one). + /// + /// The connection can be reopend later by calling [connect]. This will reset + /// the [firstConnection] future and the [nbConnectionAttempts] and [nbConnectionSuccess] counters. + Future close() { + nbConnectionAttempts = 0; + nbConnectionSuccess = 0; + _firstConnection = Completer(); + return _disconnect(); + } + + /// Disconnects websocket connection. + /// + /// Returns a [Future] that completes when the connection is closed. + Future _disconnect() { + final future = + _sink + ?.close() + .then((_) { + _logger.fine('WebSocket connection to $route was properly closed.'); + if (isDisposed) { + return; + } + _averageLag.value = Duration.zero; + }) + .catchError((Object? error) { + _logger.warning('WebSocket connection to $route could not be closed: $error', error); + if (isDisposed) { + return; + } + _averageLag.value = Duration.zero; + }) ?? + Future.value(); + _channel = null; + _socketStreamSubscription?.cancel(); + _pingTimer?.cancel(); + _reconnectTimer?.cancel(); + _ackResendTimer?.cancel(); + + return future; + } + + void _handleEvent(SocketEvent event) { + switch (event.topic) { + case '_pong': + case 'n': + _handlePong(pingDelay); + case 'ack': + _onServerAck(event); + } + + if (event != SocketEvent.pong && event.topic != 'ack') { + if (_streamController.hasListener) { + _streamController.add(event); + } + if (_globalStreamController.hasListener && + _globalSocketStreamAllowedTopics.contains(event.topic)) { + _globalStreamController.add(event); + } + } + } + + void _schedulePing(Duration delay) { + _pingTimer?.cancel(); + _pingTimer = Timer(delay, _sendPing); + } + + /// Sends a ping to the server. + void _sendPing() { + _sink?.add( + _pongCount % 10 == 2 + ? jsonEncode({'t': 'p', 'l': (_averageLag.value.inMilliseconds * 0.1).round()}) + : 'p', + ); + _lastPing = DateTime.now(); + _scheduleReconnect(pingMaxLag); + } + + void _handlePong(Duration pingDelay) { + _reconnectTimer?.cancel(); + if (_pongCount == 0) { + _logger.fine('Ping/pong protocol for $route established.'); + } + _schedulePing(pingDelay); + _pongCount++; + final currentLag = Duration( + milliseconds: math.min(DateTime.now().difference(_lastPing).inMilliseconds, 10000), + ); + + // Average first 4 pings, then switch to decaying average. + final mix = _pongCount > 4 ? 0.1 : 1 / _pongCount; + _averageLag.value += (currentLag - _averageLag.value) * mix; + } + + void _scheduleReconnect(Duration delay) { + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(delay, () { + if (!isDisposed) { + _logger.fine('Reconnecting WebSocket.'); + _averageLag.value = Duration.zero; + connect(); + } else { + _logger.warning('Scheduled reconnect after $delay failed since client is disposed.'); + } + }); + } + + void _onServerAck(SocketEvent event) { + if (event.data is! int) { + return; + } + _acks.removeWhere((rec) => rec.$2 == event.data); + } + + void _resendAcks() { + final resendCutoff = DateTime.now().subtract(const Duration(milliseconds: 2500)); + for (final (at, _, ack) in _acks) { + if (at.isBefore(resendCutoff)) { + _sink?.add(jsonEncode(ack)); + } + } + } +} + +/// Service that manages a pool of socket clients. +/// +/// The pool is used to manage multiple socket connections to different routes. +/// It ensures that only one connection is active at a time, and that a client +/// created for a route other than the lichess default socket route is disposed +/// when it becomes idle. +/// +/// A client for the default route is created upon initialization and is never +/// disposed. +/// The pool is responsible for creating and disposing other clients and that +/// there is always an active client. +/// When a requested client is disposed, the pool will automatically reconnect +/// the default client. +class SocketPool { + SocketPool(this._ref, {this.idleTimeout = _kIdleTimeout}) { + // Create a default socket client. This one is never disposed. + final client = SocketClient( + _currentRoute, + sri: _ref.read(preloadedDataProvider).requireValue.sri, + channelFactory: _ref.read(webSocketChannelFactoryProvider), + getSession: () => _ref.read(authSessionProvider), + packageInfo: _ref.read(preloadedDataProvider).requireValue.packageInfo, + deviceInfo: _ref.read(preloadedDataProvider).requireValue.deviceInfo, + pingDelay: const Duration(seconds: 25), + ); + + client.averageLag.addListener(() { + if (_currentRoute == client.route) { + _averageLag.value = client.averageLag.value; + } + }); + + _pool[_currentRoute] = client; + } + + final Ref _ref; + + /// The delay before closing the socket if idle (no subscription). + final Duration idleTimeout; + + final _averageLag = ValueNotifier(Duration.zero); + + /// The average lag computed from ping/pong protocol of the current active route. + /// + /// A duration of zero means the socket is not connected. + ValueListenable get averageLag => _averageLag; + + /// The current socket route. + Uri _currentRoute = Uri(path: kDefaultSocketRoute); + + /// The current socket client. + SocketClient get currentClient => _pool[_currentRoute]!; + + /// The socket clients pool. + final Map _pool = {}; + final Map _disposeTimers = {}; + + /// Opens a socket connection to the given [route]. + /// + /// It will use an existing connection if it is already active, unless + /// [forceReconnect] is set to true. + /// Any other active connection will be closed. + SocketClient open(Uri route, {bool? forceReconnect}) { + _currentRoute = route; + + if (_pool[route] == null) { + _pool[route] = SocketClient( + route, + channelFactory: _ref.read(webSocketChannelFactoryProvider), + getSession: () => _ref.read(authSessionProvider), + packageInfo: _ref.read(preloadedDataProvider).requireValue.packageInfo, + deviceInfo: _ref.read(preloadedDataProvider).requireValue.deviceInfo, + sri: _ref.read(preloadedDataProvider).requireValue.sri, + onStreamListen: () { + _disposeTimers[route]?.cancel(); + }, + onStreamCancel: () { + // Schedule the socket to be closed if idle, after a short delay to + // avoid unnecessary reconnections. + _disposeTimers[route]?.cancel(); + _disposeTimers[route] = Timer(idleTimeout, () { + _logger.fine('Disposing idle socket on $route.'); + _pool[route]?._dispose(); + _pool.remove(route); + // if during the idle time no new socket is requested, we reconnect + // the default socket + if (route == _currentRoute) { + _currentRoute = Uri(path: kDefaultSocketRoute); + if (!currentClient.isActive) { + currentClient.connect(); + } + } + }); + }, + ); + } + + final client = _pool[route]!; + + client.averageLag.addListener(() { + if (_currentRoute == client.route) { + _averageLag.value = client.averageLag.value; + } + }); + + // ensure there is only one active connection + _pool.forEach((k, c) { + if (k != route) { + c.close(); + } + }); + + if (forceReconnect == true || !client.isActive) { + client.connect(); + } + + return client; + } + + /// Disposes the pool and all its clients and resources. + void dispose() { + _averageLag.dispose(); + _disposeTimers.forEach((_, t) => t?.cancel()); + _pool.forEach((_, c) => c._dispose()); + } +} + +@Riverpod(keepAlive: true) +SocketPool socketPool(Ref ref) { + final pool = SocketPool(ref); + Timer? closeInBackgroundTimer; + + final appLifecycleListener = AppLifecycleListener( + onHide: () { + closeInBackgroundTimer?.cancel(); + closeInBackgroundTimer = Timer(_kDisconnectOnBackgroundTimeout, () { + _logger.info( + 'App is in background for ${_kDisconnectOnBackgroundTimeout.inMinutes}m, closing socket.', + ); + pool.currentClient.close(); + }); + }, + onShow: () { + closeInBackgroundTimer?.cancel(); + if (!pool.currentClient.isActive) { + pool.currentClient.connect(); + } + }, + ); + + ref.onDispose(() { + pool.dispose(); + closeInBackgroundTimer?.cancel(); + appLifecycleListener.dispose(); + }); + + return pool; +} + +/// Average lag computed from WebSocket ping/pong protocol. +@riverpod +class AverageLag extends _$AverageLag { + @override + Duration build() { + final listenable = ref.watch(socketPoolProvider).averageLag; + + listenable.addListener(_listener); + + ref.onDispose(() { + listenable.removeListener(_listener); + }); + + return listenable.value; + } + + void _listener() { + final newLag = ref.read(socketPoolProvider).averageLag.value; + if (state != newLag) { + state = newLag; + } + } +} + +@Riverpod(keepAlive: true) +WebSocketChannelFactory webSocketChannelFactory(Ref ref) { + return const WebSocketChannelFactory(); +} + +/// A factory to create a [WebSocketChannel]. +/// +/// This is useful to be able to mock the [WebSocketChannel] in tests. +class WebSocketChannelFactory { + const WebSocketChannelFactory(); + + /// Creates a [WebSocketChannel] from the given [url]. + /// + /// Throws a [TimeoutException] if the connection takes too long. + /// Throws a [SocketException] if the connection fails. + Future create( + String url, { + Map? headers, + Duration timeout = const Duration(seconds: 10), + }) async { + final socket = await WebSocket.connect(url, headers: headers).timeout(timeout); + + return IOWebSocketChannel(socket); + } +} diff --git a/lib/src/notification_service.dart b/lib/src/notification_service.dart deleted file mode 100644 index c3381d1c13..0000000000 --- a/lib/src/notification_service.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:convert'; - -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'notification_service.g.dart'; - -@Riverpod(keepAlive: true) -NotificationService notificationService( - NotificationServiceRef ref, -) { - return NotificationService( - Logger('NotificationServiceService'), - ref: ref, - ); -} - -class NotificationService { - NotificationService(this._log, {required this.ref}); - - final NotificationServiceRef ref; - final Logger _log; - - Future registerDevice() async { - final token = await FirebaseMessaging.instance.getToken(); - if (token != null) { - await registerToken(token); - } - } - - Future registerToken(String token) async { - final settings = await FirebaseMessaging.instance.getNotificationSettings(); - if (settings.authorizationStatus == AuthorizationStatus.denied) { - return; - } - _log.info('will register fcmToken: $token'); - final session = ref.read(authSessionProvider); - if (session == null) { - return; - } - try { - await ref.withClient( - (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), - ); - } catch (e, st) { - _log.severe('could not register device; $e', e, st); - } - } - - Future unregister() async { - _log.info('will unregister'); - final session = ref.read(authSessionProvider); - if (session == null) { - return; - } - try { - await ref.withClient( - (client) => client.post(Uri(path: '/mobile/unregister')), - ); - } catch (e, st) { - _log.severe('could not unregister device; $e', e, st); - } - } - - /// Process a message received while the app is in the foreground. - Future processDataMessage(RemoteMessage message) async { - _log.fine('processing message received in foreground: $message'); - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; - // update correspondence game - if (gameFullId != null && round != null) { - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - // opponent just played, invalidate ongoing games - if (game.sideToMove == game.youAre) { - ref.invalidate(ongoingGamesProvider); - } - ref.read(correspondenceServiceProvider).updateGame(fullId, game); - } - - // update badge - final badge = message.data['lichess.iosBadge'] as String?; - if (badge != null) { - try { - BadgeService.instance.setBadge(int.parse(badge)); - } catch (e) { - _log.severe('Could not parse badge: $badge'); - } - } - } -} diff --git a/lib/src/styles/lichess_colors.dart b/lib/src/styles/lichess_colors.dart index dd9cbabc68..83e8a0ecaf 100644 --- a/lib/src/styles/lichess_colors.dart +++ b/lib/src/styles/lichess_colors.dart @@ -6,11 +6,10 @@ class LichessColors { LichessColors._(); // material colors palette generated with: - // http://mcg.mbitson.com + // http://mmbitson.com // primary: blue - static const MaterialColor primary = - MaterialColor(_primaryPrimaryValue, { + static const MaterialColor primary = MaterialColor(_primaryPrimaryValue, { 50: Color(0xFFE4EFF9), 100: Color(0xFFBBD7F1), 200: Color(0xFF8DBCE8), @@ -25,8 +24,7 @@ class LichessColors { static const int _primaryPrimaryValue = 0xFF1B78D0; // secondary: green - static const MaterialColor secondary = - MaterialColor(_secondaryPrimaryValue, { + static const MaterialColor secondary = MaterialColor(_secondaryPrimaryValue, { 50: Color(0xFFECF3E5), 100: Color(0xFFD0E0BD), 200: Color(0xFFB1CC92), @@ -41,8 +39,7 @@ class LichessColors { static const int _secondaryPrimaryValue = 0xFF629924; // accent: orange - static const MaterialColor accent = - MaterialColor(_accentPrimaryValue, { + static const MaterialColor accent = MaterialColor(_accentPrimaryValue, { 50: Color(0xFFFAEAE0), 100: Color(0xFFF3CAB3), 200: Color(0xFFEBA780), diff --git a/lib/src/styles/lichess_icons.dart b/lib/src/styles/lichess_icons.dart index 628ffcc5fa..67675461d1 100644 --- a/lib/src/styles/lichess_icons.dart +++ b/lib/src/styles/lichess_icons.dart @@ -11,7 +11,7 @@ /// fonts: /// - asset: fonts/LichessIcons.ttf /// -/// +/// /// * Font Awesome 4, Copyright (C) 2016 by Dave Gandy /// Author: Dave Gandy /// License: SIL () @@ -33,6 +33,7 @@ class LichessIcons { static const _kFontFam = 'LichessIcons'; static const String? _kFontPkg = null; + // dart format off static const IconData patron = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData target = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData blitz = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); @@ -66,6 +67,8 @@ class LichessIcons { static const IconData radio_tower_lichess = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData crossed_swords = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData book_lichess = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData study = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData body_cut = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData tag = IconData(0xf02b, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData tags = IconData(0xf02c, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData step_backward = IconData(0xf048, fontFamily: _kFontFam, fontPackage: _kFontPkg); diff --git a/lib/src/styles/puzzle_icons.dart b/lib/src/styles/puzzle_icons.dart index 04d37141b7..2405640dad 100644 --- a/lib/src/styles/puzzle_icons.dart +++ b/lib/src/styles/puzzle_icons.dart @@ -6,118 +6,189 @@ class PuzzleIcons { static const _kFontFam = 'LichessPuzzleIcons'; static const String? _kFontPkg = null; - static const IconData clearance = - IconData(0xe000, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queensideAttack = - IconData(0xe001, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData bishopEndgame = - IconData(0xe002, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData short = - IconData(0xe003, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData backRankMate = - IconData(0xe004, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData advancedPawn = - IconData(0xe005, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData doubleCheck = - IconData(0xe006, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData mate = - IconData(0xe007, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData rookEndgame = - IconData(0xe008, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData sacrifice = - IconData(0xe009, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData promotion = - IconData(0xe00a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData knightEndgame = - IconData(0xe00b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData skewer = - IconData(0xe00c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData master = - IconData(0xe00d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData hookMate = - IconData(0xe00e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData exposedKing = - IconData(0xe00f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData intermezzo = - IconData(0xe010, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData interference = - IconData(0xe011, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData doubleBishopMate = - IconData(0xe012, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData hangingPiece = - IconData(0xe013, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData superGM = - IconData(0xe014, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData equality = - IconData(0xe015, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData castling = - IconData(0xe016, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData underPromotion = - IconData(0xe017, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData discoveredAttack = - IconData(0xe018, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData pin = - IconData(0xe019, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData endgame = - IconData(0xe01a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData defensiveMove = - IconData(0xe01b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData advantage = - IconData(0xe01c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData mix = - IconData(0xe01d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData oneMove = - IconData(0xe01e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData anastasiaMate = - IconData(0xe01f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData middlegame = - IconData(0xe020, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData fork = - IconData(0xe021, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData masterVsMaster = - IconData(0xe022, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData quietMove = - IconData(0xe023, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData long = - IconData(0xe024, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData smotheredMate = - IconData(0xe025, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData bodenMate = - IconData(0xe026, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData kingsideAttack = - IconData(0xe027, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData playerGames = - IconData(0xe028, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData pawnEndgame = - IconData(0xe029, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData zugzwang = - IconData(0xe02a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData deflection = - IconData(0xe02b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData trappedPiece = - IconData(0xe02c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData capturingDefender = - IconData(0xe02d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queenEndgame = - IconData(0xe02e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData arabianMate = - IconData(0xe02f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData enPassant = - IconData(0xe030, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData attackingF2F7 = - IconData(0xe031, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData veryLong = - IconData(0xe032, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData xRayAttack = - IconData(0xe033, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queenRookEndgame = - IconData(0xe034, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData attraction = - IconData(0xe035, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData crushing = - IconData(0xe036, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData opening = - IconData(0xe037, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData dovetailMate = - IconData(0xe038, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData clearance = IconData(0xe000, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData queensideAttack = IconData( + 0xe001, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData bishopEndgame = IconData( + 0xe002, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData short = IconData(0xe003, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData backRankMate = IconData( + 0xe004, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData advancedPawn = IconData( + 0xe005, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData doubleCheck = IconData( + 0xe006, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData mate = IconData(0xe007, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData rookEndgame = IconData( + 0xe008, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData sacrifice = IconData(0xe009, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData promotion = IconData(0xe00a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData knightEndgame = IconData( + 0xe00b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData skewer = IconData(0xe00c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData master = IconData(0xe00d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData hookMate = IconData(0xe00e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData exposedKing = IconData( + 0xe00f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData intermezzo = IconData( + 0xe010, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData interference = IconData( + 0xe011, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData doubleBishopMate = IconData( + 0xe012, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData hangingPiece = IconData( + 0xe013, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData superGM = IconData(0xe014, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData equality = IconData(0xe015, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData castling = IconData(0xe016, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData underPromotion = IconData( + 0xe017, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData discoveredAttack = IconData( + 0xe018, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData pin = IconData(0xe019, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData endgame = IconData(0xe01a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData defensiveMove = IconData( + 0xe01b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData advantage = IconData(0xe01c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData mix = IconData(0xe01d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData oneMove = IconData(0xe01e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData anastasiaMate = IconData( + 0xe01f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData middlegame = IconData( + 0xe020, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData fork = IconData(0xe021, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData masterVsMaster = IconData( + 0xe022, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData quietMove = IconData(0xe023, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData long = IconData(0xe024, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData smotheredMate = IconData( + 0xe025, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData bodenMate = IconData(0xe026, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData kingsideAttack = IconData( + 0xe027, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData playerGames = IconData( + 0xe028, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData pawnEndgame = IconData( + 0xe029, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData zugzwang = IconData(0xe02a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData deflection = IconData( + 0xe02b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData trappedPiece = IconData( + 0xe02c, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData capturingDefender = IconData( + 0xe02d, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData queenEndgame = IconData( + 0xe02e, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData arabianMate = IconData( + 0xe02f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData enPassant = IconData(0xe030, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData attackingF2F7 = IconData( + 0xe031, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData veryLong = IconData(0xe032, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData xRayAttack = IconData( + 0xe033, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData queenRookEndgame = IconData( + 0xe034, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData attraction = IconData( + 0xe035, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData crushing = IconData(0xe036, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData opening = IconData(0xe037, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData dovetailMate = IconData( + 0xe038, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); } diff --git a/lib/src/styles/social_icons.dart b/lib/src/styles/social_icons.dart index 7712fe8c39..9363012338 100644 --- a/lib/src/styles/social_icons.dart +++ b/lib/src/styles/social_icons.dart @@ -29,8 +29,6 @@ class SocialIcons { static const _kFontFam = 'SocialIcons'; static const String? _kFontPkg = null; - static const IconData youtube = - IconData(0xf167, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData twitch = - IconData(0xf1e8, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData youtube = IconData(0xf167, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData twitch = IconData(0xf1e8, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index bbcc12ca30..6959840530 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -8,14 +8,8 @@ import 'package:lichess_mobile/src/styles/lichess_colors.dart'; abstract class Styles { // text static const bold = TextStyle(fontWeight: FontWeight.bold); - static const title = TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - ); - static const subtitle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ); + static const title = TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold); + static const subtitle = TextStyle(fontSize: 16, fontWeight: FontWeight.w500); static final callout = TextStyle( fontSize: defaultTargetPlatform == TargetPlatform.iOS ? 20 : 18, letterSpacing: defaultTargetPlatform == TargetPlatform.iOS ? -0.41 : null, @@ -32,30 +26,24 @@ abstract class Styles { letterSpacing: defaultTargetPlatform == TargetPlatform.iOS ? -0.41 : null, fontWeight: FontWeight.bold, ); - static const boardPreviewTitle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ); + static const boardPreviewTitle = TextStyle(fontSize: 16, fontWeight: FontWeight.bold); static const subtitleOpacity = 0.7; - static const timeControl = TextStyle( - letterSpacing: 1.2, - ); - static const formLabel = TextStyle( - fontWeight: FontWeight.bold, - ); - static const formError = TextStyle( - color: LichessColors.red, - ); + static const timeControl = TextStyle(letterSpacing: 1.2); + static const formLabel = TextStyle(fontWeight: FontWeight.bold); + static const formError = TextStyle(color: LichessColors.red); static const formDescription = TextStyle(fontSize: 12); // padding - static const cupertinoAppBarTrailingWidgetPadding = - EdgeInsetsDirectional.only( - end: 8.0, - ); - static const bodyPadding = - EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0); + static const cupertinoAppBarTrailingWidgetPadding = EdgeInsetsDirectional.only(end: 8.0); + static const bodyPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0); static const verticalBodyPadding = EdgeInsets.symmetric(vertical: 16.0); + static const horizontalBodyPadding = EdgeInsets.symmetric(horizontal: 16.0); + static const sectionBottomPadding = EdgeInsets.only(bottom: 16.0); + static const sectionTopPadding = EdgeInsets.only(top: 16.0); + static const bodySectionPadding = EdgeInsets.all(16.0); + + /// Horizontal and bottom padding for the body section. + static const bodySectionBottomPadding = EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0); // colors static Color? expansionTileColor(BuildContext context) => @@ -66,6 +54,10 @@ abstract class Styles { color: Color(0xE6F9F9F9), darkColor: Color.fromARGB(210, 36, 36, 38), ); + static const cupertinoTabletAppBarColor = CupertinoDynamicColor.withBrightness( + color: Color(0xFFF9F9F9), + darkColor: Color.fromARGB(255, 36, 36, 36), + ); static const cupertinoScaffoldColor = CupertinoDynamicColor.withBrightness( color: Color.fromARGB(255, 242, 242, 247), darkColor: Color.fromARGB(255, 23, 23, 23), @@ -188,30 +180,15 @@ abstract class Styles { ), ); - /// Gets horizontal padding according to platform. - static const horizontalBodyPadding = EdgeInsets.symmetric(horizontal: 16.0); - static const sectionBottomPadding = EdgeInsets.only(bottom: 16); - static const sectionTopPadding = EdgeInsets.only(top: 16); - - /// Horizontal and bottom padding for a body section - static EdgeInsetsGeometry get bodySectionPadding => - horizontalBodyPadding.add(sectionBottomPadding).add(sectionTopPadding); - - static EdgeInsetsGeometry get bodySectionBottomPadding => - horizontalBodyPadding.add(sectionBottomPadding); - // from: // https://github.com/flutter/flutter/blob/796c8ef79279f9c774545b3771238c3098dbefab/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart#L17 static const CupertinoDynamicColor cupertinoDefaultTabBarBorderColor = - CupertinoDynamicColor.withBrightness( - color: Color(0x4D000000), - darkColor: Color(0x29000000), - ); + CupertinoDynamicColor.withBrightness(color: Color(0x4D000000), darkColor: Color(0x29000000)); } /// Retrieve the default text color and apply an opacity to it. Color? textShade(BuildContext context, double opacity) => - DefaultTextStyle.of(context).style.color?.withOpacity(opacity); + DefaultTextStyle.of(context).style.color?.withValues(alpha: opacity); Color? dividerColor(BuildContext context) => defaultTargetPlatform == TargetPlatform.iOS @@ -220,23 +197,12 @@ Color? dividerColor(BuildContext context) => Color darken(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); - final f = 1 - amount; - return Color.fromARGB( - c.alpha, - (c.red * f).round(), - (c.green * f).round(), - (c.blue * f).round(), - ); + return Color.lerp(c, Colors.black, amount) ?? c; } Color lighten(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); - return Color.fromARGB( - c.alpha, - c.red + ((255 - c.red) * amount).round(), - c.green + ((255 - c.green) * amount).round(), - c.blue + ((255 - c.blue) * amount).round(), - ); + return Color.lerp(c, Colors.white, amount) ?? c; } @immutable @@ -320,6 +286,5 @@ const lichessCustomColors = CustomColors( ); extension CustomColorsBuildContext on BuildContext { - CustomColors get lichessColors => - Theme.of(this).extension() ?? lichessCustomColors; + CustomColors get lichessColors => Theme.of(this).extension() ?? lichessCustomColors; } diff --git a/lib/src/styles/transparent_image.dart b/lib/src/styles/transparent_image.dart deleted file mode 100644 index 774d642d8b..0000000000 --- a/lib/src/styles/transparent_image.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:typed_data'; - -final Uint8List transparentImage = Uint8List.fromList([ - 0x89, - 0x50, - 0x4E, - 0x47, - 0x0D, - 0x0A, - 0x1A, - 0x0A, - 0x00, - 0x00, - 0x00, - 0x0D, - 0x49, - 0x48, - 0x44, - 0x52, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x01, - 0x08, - 0x06, - 0x00, - 0x00, - 0x00, - 0x1F, - 0x15, - 0xC4, - 0x89, - 0x00, - 0x00, - 0x00, - 0x0A, - 0x49, - 0x44, - 0x41, - 0x54, - 0x78, - 0x9C, - 0x63, - 0x00, - 0x01, - 0x00, - 0x00, - 0x05, - 0x00, - 0x01, - 0x0D, - 0x0A, - 0x2D, - 0xB4, - 0x00, - 0x00, - 0x00, - 0x00, - 0x49, - 0x45, - 0x4E, - 0x44, - 0xAE, - 0x42, - 0x60, - 0x82, -]); diff --git a/lib/src/utils/async_value.dart b/lib/src/utils/async_value.dart index 76d71b967b..b7aba0b226 100644 --- a/lib/src/utils/async_value.dart +++ b/lib/src/utils/async_value.dart @@ -7,9 +7,7 @@ extension AsyncValueUI on AsyncValue { if (!isRefreshing && hasError) { switch (Theme.of(context).platform) { case TargetPlatform.android: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString()))); case TargetPlatform.iOS: showCupertinoSnackBar( context: context, @@ -18,7 +16,6 @@ extension AsyncValueUI on AsyncValue { ); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - break; } } } diff --git a/lib/src/utils/badge_service.dart b/lib/src/utils/badge_service.dart index 9fb7a8403f..67a8c3e809 100644 --- a/lib/src/utils/badge_service.dart +++ b/lib/src/utils/badge_service.dart @@ -19,9 +19,7 @@ class BadgeService { } try { - await _channel.invokeMethod('setBadge', { - 'badge': value, - }); + await _channel.invokeMethod('setBadge', {'badge': value}); } on PlatformException catch (e) { _log.severe(e); } diff --git a/lib/src/utils/chessboard.dart b/lib/src/utils/chessboard.dart new file mode 100644 index 0000000000..af26a57636 --- /dev/null +++ b/lib/src/utils/chessboard.dart @@ -0,0 +1,21 @@ +import 'package:chessground/chessground.dart'; +import 'package:flutter/widgets.dart'; + +/// Preload piece images from the specified [PieceSet] into Chessground's image cache. +/// +/// This method clears the cache before loading the images. +Future precachePieceImages(PieceSet pieceSet) async { + try { + final devicePixelRatio = + WidgetsBinding.instance.platformDispatcher.implicitView?.devicePixelRatio ?? 1.0; + + ChessgroundImages.instance.clear(); + + for (final asset in pieceSet.assets.values) { + await ChessgroundImages.instance.load(asset, devicePixelRatio: devicePixelRatio); + debugPrint('Preloaded piece image: ${asset.assetName}'); + } + } catch (e) { + debugPrint('Failed to preload piece images: $e'); + } +} diff --git a/lib/src/utils/chessground_compat.dart b/lib/src/utils/chessground_compat.dart deleted file mode 100644 index a00873c05e..0000000000 --- a/lib/src/utils/chessground_compat.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:chessground/chessground.dart' as chessground; -import 'package:dartchess/dartchess.dart'; - -extension ChessgroundSideCompat on Side { - chessground.Side get cg => - this == Side.white ? chessground.Side.white : chessground.Side.black; -} - -extension ChessgroundMoveCompat on Move { - chessground.Move get cg { - // !! Chessground doesn't support drop moves yet - if (this is DropMove) { - return chessground.Move(from: toAlgebraic(to), to: toAlgebraic(to)); - } - - return chessground.Move( - from: toAlgebraic((this as NormalMove).from), - to: toAlgebraic(to), - promotion: (this as NormalMove).promotion?.cg, - ); - } -} - -extension ChessgroundRoleCompat on Role { - chessground.Role get cg { - switch (this) { - case Role.pawn: - return chessground.Role.pawn; - case Role.knight: - return chessground.Role.knight; - case Role.bishop: - return chessground.Role.bishop; - case Role.rook: - return chessground.Role.rook; - case Role.king: - return chessground.Role.king; - case Role.queen: - return chessground.Role.queen; - } - } -} diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 51b272b952..6ecdd18d82 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -1,11 +1,12 @@ import 'dart:ui'; import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; CorePalette? _corePalette; -BoardColorScheme? _boardColorScheme; +ChessboardColorScheme? _boardColorScheme; /// Set the system core palette if available (android 12+ only). /// @@ -17,32 +18,29 @@ void setCorePalette(CorePalette? palette) { final darkSquare = Color(palette.secondary.get(60)); final lightSquare = Color(palette.primary.get(95)); - _boardColorScheme = BoardColorScheme( + _boardColorScheme = ChessboardColorScheme( darkSquare: darkSquare, lightSquare: lightSquare, - background: SolidColorBackground( - lightSquare: lightSquare, - darkSquare: darkSquare, - ), - whiteCoordBackground: SolidColorBackground( + background: SolidColorChessboardBackground(lightSquare: lightSquare, darkSquare: darkSquare), + whiteCoordBackground: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, coordinates: true, ), - blackCoordBackground: SolidColorBackground( + blackCoordBackground: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, coordinates: true, orientation: Side.black, ), lastMove: HighlightDetails( - solidColor: Color(palette.tertiary.get(80)).withOpacity(0.6), + solidColor: Color(palette.tertiary.get(80)).withValues(alpha: 0.6), ), selected: HighlightDetails( - solidColor: Color(palette.neutral.get(40)).withOpacity(0.80), + solidColor: Color(palette.neutral.get(40)).withValues(alpha: 0.80), ), - validMoves: Color(palette.neutral.get(40)).withOpacity(0.40), - validPremoves: Color(palette.error.get(40)).withOpacity(0.30), + validMoves: Color(palette.neutral.get(40)).withValues(alpha: 0.40), + validPremoves: Color(palette.error.get(40)).withValues(alpha: 0.30), ); } } @@ -53,6 +51,6 @@ CorePalette? getCorePalette() { } /// Get the board colors based on the core palette, if available (android 12+). -BoardColorScheme? getBoardColorScheme() { +ChessboardColorScheme? getBoardColorScheme() { return _boardColorScheme; } diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart deleted file mode 100644 index c6d37e3483..0000000000 --- a/lib/src/utils/connectivity.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:http/http.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'connectivity.freezed.dart'; -part 'connectivity.g.dart'; - -final _logger = Logger('Connectivity'); - -/// This provider is used to check the device's connectivity status, reacting to -/// changes in connectivity and app lifecycle events. -/// -/// - Uses the [Connectivity] plugin to listen to connectivity changes -/// - Uses [AppLifecycleListener] to check connectivity on app resume -@riverpod -class ConnectivityChanges extends _$ConnectivityChanges { - StreamSubscription>? _socketSubscription; - AppLifecycleListener? _appLifecycleListener; - - final _connectivityChangesDebouncer = Debouncer(const Duration(seconds: 5)); - - Client get _defaultClient => ref.read(defaultClientProvider); - - @override - Future build() { - ref.onDispose(() { - _socketSubscription?.cancel(); - _appLifecycleListener?.dispose(); - _connectivityChangesDebouncer.dispose(); - }); - - _socketSubscription?.cancel(); - _socketSubscription = Connectivity().onConnectivityChanged.listen((result) { - _connectivityChangesDebouncer(() => _onConnectivityChange(result)); - }); - - final AppLifecycleState? appState = WidgetsBinding.instance.lifecycleState; - - _appLifecycleListener = AppLifecycleListener( - onStateChange: _onAppLifecycleChange, - ); - - return Connectivity() - .checkConnectivity() - .then((r) => _getConnectivityStatus(r, appState)); - } - - Future _onAppLifecycleChange(AppLifecycleState appState) async { - if (!state.hasValue) { - return; - } - - if (appState == AppLifecycleState.resumed) { - final newConn = await Connectivity() - .checkConnectivity() - .then((r) => _getConnectivityStatus(r, appState)); - - state = AsyncValue.data(newConn); - } else { - state = AsyncValue.data(state.requireValue.copyWith(appState: appState)); - } - } - - Future _onConnectivityChange(List result) async { - if (!state.hasValue) { - return; - } - - final wasOnline = state.requireValue.isOnline; - - _logger.fine('Connectivity changed: $result'); - final newIsOnline = await isOnline(_defaultClient); - _logger.fine('Online check result: $isOnline'); - - if (newIsOnline != wasOnline) { - _logger.info('Connectivity status: $result, isOnline: $isOnline'); - state = AsyncValue.data( - ConnectivityStatus( - isOnline: newIsOnline, - appState: state.valueOrNull?.appState, - ), - ); - } - } - - Future _getConnectivityStatus( - List result, - AppLifecycleState? appState, - ) async { - final status = ConnectivityStatus( - isOnline: await isOnline(_defaultClient), - appState: appState, - ); - _logger.info('Connectivity status: $result, isOnline: ${status.isOnline}'); - return status; - } -} - -@freezed -class ConnectivityStatus with _$ConnectivityStatus { - const factory ConnectivityStatus({ - required bool isOnline, - AppLifecycleState? appState, - }) = _ConnectivityStatus; -} - -final _internetCheckUris = [ - Uri.parse('https://www.gstatic.com/generate_204'), - Uri.parse('$kLichessCDNHost/assets/logo/lichess-favicon-32.png'), -]; - -/// Checks if the device is online by making a HEAD request to a list of URIs. -Future isOnline(Client client) { - final completer = Completer(); - try { - int remaining = _internetCheckUris.length; - final futures = _internetCheckUris.map( - (uri) => client.head(uri).timeout(const Duration(seconds: 10)).then( - (response) => true, - onError: (_) => false, - ), - ); - for (final future in futures) { - future.then((value) { - remaining--; - if (!completer.isCompleted) { - if (value == true) { - completer.complete(true); - } else if (remaining == 0) { - completer.complete(false); - } - } - }); - } - } catch (_) { - completer.complete(false); - } - return completer.future; -} diff --git a/lib/src/utils/device_info.dart b/lib/src/utils/device_info.dart deleted file mode 100644 index 9ffb9fd5ed..0000000000 --- a/lib/src/utils/device_info.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'device_info.g.dart'; - -@Riverpod(keepAlive: true) -BaseDeviceInfo deviceInfo(DeviceInfoRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.deviceInfo; -} diff --git a/lib/src/utils/duration.dart b/lib/src/utils/duration.dart index eb799f6840..0cdbf34af0 100644 --- a/lib/src/utils/duration.dart +++ b/lib/src/utils/duration.dart @@ -7,7 +7,7 @@ extension DurationExtensions on Duration { /// representation is H:MM:SS.mm, otherwise it is H:MM:SS. String toHoursMinutesSeconds({bool showTenths = false}) { if (inHours == 0) { - return '${inMinutes.remainder(60)}:${inSeconds.remainder(60).toString().padLeft(2, '0')}${showTenths ? '.${inMilliseconds.remainder(1000) ~/ 10 % 10}' : ''}'; + return '${inMinutes.remainder(60).toString().padLeft(2, '0')}:${inSeconds.remainder(60).toString().padLeft(2, '0')}${showTenths ? '.${inMilliseconds.remainder(1000) ~/ 10 % 10}' : ''}'; } else { return '$inHours:${inMinutes.remainder(60).toString().padLeft(2, '0')}:${inSeconds.remainder(60).toString().padLeft(2, '0')}'; } diff --git a/lib/src/utils/focus_detector.dart b/lib/src/utils/focus_detector.dart index 40d5800eed..25cc261294 100644 --- a/lib/src/utils/focus_detector.dart +++ b/lib/src/utils/focus_detector.dart @@ -52,8 +52,7 @@ class FocusDetector extends StatefulWidget { _FocusDetectorState createState() => _FocusDetectorState(); } -class _FocusDetectorState extends State - with WidgetsBindingObserver { +class _FocusDetectorState extends State with WidgetsBindingObserver { final _visibilityDetectorKey = UniqueKey(); /// Counter to keep track of the visibility changes. @@ -101,13 +100,13 @@ class _FocusDetectorState extends State @override Widget build(BuildContext context) => VisibilityDetector( - key: _visibilityDetectorKey, - onVisibilityChanged: (visibilityInfo) { - final visibleFraction = visibilityInfo.visibleFraction; - _notifyVisibilityStatusChange(visibleFraction); - }, - child: widget.child, - ); + key: _visibilityDetectorKey, + onVisibilityChanged: (visibilityInfo) { + final visibleFraction = visibilityInfo.visibleFraction; + _notifyVisibilityStatusChange(visibleFraction); + }, + child: widget.child, + ); /// Notifies changes in the widget's visibility. void _notifyVisibilityStatusChange(double newVisibleFraction) { diff --git a/lib/src/utils/gestures_exclusion.dart b/lib/src/utils/gestures_exclusion.dart index bbcb139c6c..b8e0b410a2 100644 --- a/lib/src/utils/gestures_exclusion.dart +++ b/lib/src/utils/gestures_exclusion.dart @@ -47,10 +47,7 @@ class AndroidGesturesExclusionWidget extends StatelessWidget { return FocusDetector( onFocusGained: () { if (shouldExcludeGesturesOnFocusGained?.call() ?? true) { - setAndroidBoardGesturesExclusion( - boardKey, - withImmersiveMode: shouldSetImmersiveMode, - ); + setAndroidBoardGesturesExclusion(boardKey, withImmersiveMode: shouldSetImmersiveMode); } }, onFocusLost: () { @@ -126,8 +123,7 @@ Future clearAndroidBoardGesturesExclusion() async { } class GesturesExclusion { - static const _channel = - MethodChannel('mobile.lichess.org/gestures_exclusion'); + static const _channel = MethodChannel('mobile.lichess.org/gestures_exclusion'); const GesturesExclusion._(); @@ -138,24 +134,23 @@ class GesturesExclusion { return; } - final rectsAsMaps = rects - .map( - (r) => r.hasNaN || r.isInfinite - ? null - : { - 'left': r.left.floor(), - 'top': r.top.floor(), - 'right': r.right.floor(), - 'bottom': r.bottom.floor(), - }, - ) - .toList(); + final rectsAsMaps = + rects + .map( + (r) => + r.hasNaN || r.isInfinite + ? null + : { + 'left': r.left.floor(), + 'top': r.top.floor(), + 'right': r.right.floor(), + 'bottom': r.bottom.floor(), + }, + ) + .toList(); try { - await _channel.invokeMethod( - 'setSystemGestureExclusionRects', - rectsAsMaps, - ); + await _channel.invokeMethod('setSystemGestureExclusionRects', rectsAsMaps); } on PlatformException catch (e) { debugPrint('Failed to set rects: ${e.message}'); } @@ -167,8 +162,7 @@ class GesturesExclusion { } try { - await _channel - .invokeMethod('setSystemGestureExclusionRects', []); + await _channel.invokeMethod('setSystemGestureExclusionRects', []); } on PlatformException catch (e) { debugPrint('Failed to clear rects: ${e.message}'); } diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart new file mode 100644 index 0000000000..9208bb5e1e --- /dev/null +++ b/lib/src/utils/image.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:material_color_utilities/material_color_utilities.dart'; + +typedef ImageColors = ({int primaryContainer, int onPrimaryContainer}); + +/// A worker that quantizes an image and returns a minimal color scheme associated +/// with the image. +/// +/// It is the responsibility of the caller to provide a scaled down version of the +/// image to the worker to avoid a costly quantization process. +/// +/// The worker is created by calling [ImageColorWorker.spawn], and the computation +/// is run in a separate isolate. +class ImageColorWorker { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + bool get closed => _closed; + + /// Returns a minimal color scheme associated with the given [image]. + Future getImageColors(Uint32List image) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, image)); + return await completer.future; + } + + static Future spawn() async { + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (dynamic initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete((ReceivePort.fromRawReceivePort(initPort), commandPort)); + }; + + try { + await Isolate.spawn(_startRemoteIsolate, initPort.sendPort); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; + + return ImageColorWorker._(receivePort, sendPort); + } + + ImageColorWorker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object response) = message as (int, Object); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response as ImageColors); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate(ReceivePort receivePort, SendPort sendPort) { + receivePort.listen((message) async { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, Uint32List image) = message as (int, Uint32List); + try { + // final stopwatch0 = Stopwatch()..start(); + final quantizerResult = await QuantizerCelebi().quantize(image, 32); + final Map colorToCount = quantizerResult.colorToCount.map( + (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), + ); + final significantColors = Map.from(colorToCount) + ..removeWhere((key, value) => value < 10); + final meanTone = + colorToCount.entries.fold( + 0, + (double previousValue, MapEntry element) => + previousValue + Hct.fromInt(element.key).tone * element.value, + ) / + colorToCount.values.fold( + 0, + (int previousValue, int element) => previousValue + element, + ); + + final int scoredResult = + Score.score( + colorToCount, + desired: 1, + fallbackColorARGB: 0xFFFFFFFF, + filter: false, + ).first; + final Hct sourceColor = Hct.fromInt(scoredResult); + if ((meanTone - sourceColor.tone).abs() > 20) { + sourceColor.tone = meanTone; + } + final scheme = (significantColors.length <= 10 ? SchemeMonochrome.new : SchemeFidelity.new)( + sourceColorHct: sourceColor, + isDark: sourceColor.tone < 50, + contrastLevel: 0.0, + ); + // print('Quantize and scoring took: ${stopwatch0.elapsedMilliseconds}ms'); + final result = ( + primaryContainer: scheme.primaryContainer, + onPrimaryContainer: scheme.onPrimaryContainer, + ); + sendPort.send((id, result)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + } + } +} + +// Converts AABBGGRR color int to AARRGGBB format. +int _getArgbFromAbgr(int abgr) { + const int exceptRMask = 0xFF00FFFF; + const int onlyRMask = ~exceptRMask; + const int exceptBMask = 0xFFFFFF00; + const int onlyBMask = ~exceptBMask; + final int r = (abgr & onlyRMask) >> 16; + final int b = abgr & onlyBMask; + return (abgr & exceptRMask & exceptBMask) | (b << 16) | r; +} diff --git a/lib/src/utils/immersive_mode.dart b/lib/src/utils/immersive_mode.dart index e99b460606..54738fdc94 100644 --- a/lib/src/utils/immersive_mode.dart +++ b/lib/src/utils/immersive_mode.dart @@ -11,11 +11,7 @@ import 'package:wakelock_plus/wakelock_plus.dart'; /// force the device to stay awake. class ImmersiveModeWidget extends StatelessWidget { /// Create a new immersive mode widget, that enables immersive mode when focused. - const ImmersiveModeWidget({ - required this.child, - this.shouldEnableOnFocusGained, - super.key, - }); + const ImmersiveModeWidget({required this.child, this.shouldEnableOnFocusGained, super.key}); final Widget child; @@ -40,11 +36,7 @@ class ImmersiveModeWidget extends StatelessWidget { /// A widget that enables wakelock when focused. class WakelockWidget extends StatelessWidget { /// Create a new wakelock widget, that enables wakelock when focused. - const WakelockWidget({ - required this.child, - this.shouldEnableOnFocusGained, - super.key, - }); + const WakelockWidget({required this.child, this.shouldEnableOnFocusGained, super.key}); final Widget child; @@ -91,17 +83,18 @@ class ImmersiveMode { Future disable() async { final wakeFuture = WakelockPlus.disable(); - final androidInfo = defaultTargetPlatform == TargetPlatform.android - ? await DeviceInfoPlugin().androidInfo - : null; + final androidInfo = + defaultTargetPlatform == TargetPlatform.android + ? await DeviceInfoPlugin().androidInfo + : null; final setUiModeFuture = androidInfo == null || androidInfo.version.sdkInt >= 29 ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge) : SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - ); + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); return Future.wait([wakeFuture, setUiModeFuture]).then((_) {}); } diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 6066eaa7b0..cea4664d8c 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -1,6 +1,57 @@ +import 'dart:ui' show Color, Locale; + import 'package:deep_pick/deep_pick.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; +class LocaleConverter implements JsonConverter?> { + const LocaleConverter(); + + @override + Locale? fromJson(Map? json) { + if (json == null) { + return null; + } + return Locale.fromSubtags( + languageCode: json['languageCode'] as String, + countryCode: json['countryCode'] as String?, + scriptCode: json['scriptCode'] as String?, + ); + } + + @override + Map? toJson(Locale? locale) { + return locale != null + ? { + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } + : null; + } +} + +class ColorConverter implements JsonConverter?> { + const ColorConverter(); + + @override + Color? fromJson(Map? json) { + return json != null + ? Color.from( + alpha: json['a'] as double, + red: json['r'] as double, + green: json['g'] as double, + blue: json['b'] as double, + ) + : null; + } + + @override + Map? toJson(Color? color) { + return color != null ? {'a': color.a, 'r': color.r, 'g': color.g, 'b': color.b} : null; + } +} + extension UciExtension on Pick { /// Matches a UciCharPair from a string. UciCharPair asUciCharPairOrThrow() { @@ -23,11 +74,9 @@ extension TimeExtension on Pick { return value; } if (value is int) { - return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true); + return DateTime.fromMillisecondsSinceEpoch(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to DateTime", - ); + throw PickException("value $value at $debugParsingExit can't be casted to DateTime"); } /// Matches a DateTime from milliseconds since unix epoch. @@ -51,9 +100,7 @@ extension TimeExtension on Pick { } else if (value is double) { return Duration(milliseconds: (value * 1000).toInt()); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } /// Matches a Duration from seconds @@ -75,9 +122,7 @@ extension TimeExtension on Pick { if (value is int) { return Duration(milliseconds: value * 10); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } Duration? asDurationFromCentiSecondsOrNull() { @@ -98,9 +143,7 @@ extension TimeExtension on Pick { if (value is int) { return Duration(milliseconds: value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } Duration? asDurationFromMilliSecondsOrNull() { diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index c3857206af..21b4b4177c 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,57 +1,6 @@ -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'l10n.freezed.dart'; -part 'l10n.g.dart'; - -@freezed -class L10nState with _$L10nState { - const factory L10nState({ - required Locale locale, - required AppLocalizations strings, - }) = _L10nState; -} - -@Riverpod(keepAlive: true) -class L10n extends _$L10n { - @override - L10nState build() { - final observer = _LocaleObserver((locales) { - _update(); - }); - final binding = WidgetsBinding.instance; - binding.addObserver(observer); - ref.onDispose(() => binding.removeObserver(observer)); - - return _getLocale(); - } - - void _update() { - state = _getLocale(); - } - - L10nState _getLocale() { - final locale = WidgetsBinding.instance.platformDispatcher.locale; - return L10nState( - locale: locale, - strings: lookupAppLocalizations(locale), - ); - } -} - -/// observer used to notify the caller when the locale changes -class _LocaleObserver extends WidgetsBindingObserver { - _LocaleObserver(this._didChangeLocales); - final void Function(List? locales) _didChangeLocales; - @override - void didChangeLocales(List? locales) { - _didChangeLocales(locales); - } -} /// Returns a localized string with a single placeholder replaced by a widget. /// @@ -84,124 +33,157 @@ Text l10nWithWidget( children: [ if (parts[0].isNotEmpty) TextSpan(text: parts[0], style: textStyle), if (parts[0] != localizedStringWithPlaceholder) - WidgetSpan( - child: widget, - alignment: PlaceholderAlignment.middle, - style: textStyle, - ), - if (parts.length > 1 && parts[1].isNotEmpty) - TextSpan(text: parts[1], style: textStyle), + WidgetSpan(child: widget, alignment: PlaceholderAlignment.middle, style: textStyle), + if (parts.length > 1 && parts[1].isNotEmpty) TextSpan(text: parts[1], style: textStyle), ], ), ); } +final _dateFormatterWithYear = DateFormat.yMMMMd(); +final _dateFormatterWithYearShort = DateFormat.yMMMd(); + +/// Formats a date as a relative date string from now. +String relativeDate(AppLocalizations l10n, DateTime date, {bool shortDate = true}) { + final now = DateTime.now(); + final diff = date.difference(now); + + final yearFormatter = shortDate ? _dateFormatterWithYearShort : _dateFormatterWithYear; + + if (diff.isNegative) { + return diff.inDays == 0 + ? diff.inHours == 0 + ? l10n.timeagoNbMinutesAgo(diff.inMinutes.abs()) + : l10n.timeagoNbHoursAgo(diff.inHours.abs()) + : diff.inDays == 1 + ? l10n.yesterday + : diff.inDays.abs() <= 7 + ? l10n.timeagoNbDaysAgo(diff.inDays.abs()) + : diff.inDays.abs() <= 30 + ? l10n.timeagoNbWeeksAgo((diff.inDays.abs() / 7).round()) + : diff.inDays.abs() <= 365 + ? l10n.timeagoNbMonthsAgo((diff.inDays.abs() / 30).round()) + : yearFormatter.format(date); + } + return diff.inDays == 0 + ? diff.inHours == 0 + ? l10n.timeagoInNbMinutes(diff.inMinutes) + : l10n.timeagoInNbHours(diff.inHours) + : diff.inDays.abs() <= 7 + ? l10n.timeagoInNbDays(diff.inDays) + : diff.inDays.abs() <= 30 + ? l10n.timeagoInNbWeeks((diff.inDays.abs() / 7).round()) + : diff.inDays.abs() <= 365 + ? l10n.timeagoInNbMonths((diff.inDays.abs() / 30).round()) + : yearFormatter.format(date); +} + /// Returns a localized locale name. /// /// Names taken from https://github.com/lichess-org/lila/blob/master/modules/i18n/src/main/LangList.scala. /// /// Not all of these are actually supported in the app currently, but this way we won't have to check the lila code again when we add more languages. String localeToLocalizedName(Locale locale) => switch (locale) { - Locale(languageCode: 'en', countryCode: 'GB') => 'English', - Locale(languageCode: 'af', countryCode: 'ZA') => 'Afrikaans', - Locale(languageCode: 'an', countryCode: 'ES') => 'Aragonés', - Locale(languageCode: 'ar', countryCode: 'SA') => 'العربية', - Locale(languageCode: 'as', countryCode: 'IN') => 'অসমীয়া', - Locale(languageCode: 'av', countryCode: 'DA') => 'авар мацӀ', - Locale(languageCode: 'az', countryCode: 'AZ') => 'Azərbaycanca', - Locale(languageCode: 'be', countryCode: 'BY') => 'Беларуская', - Locale(languageCode: 'bg', countryCode: 'BG') => 'български език', - Locale(languageCode: 'bn', countryCode: 'BD') => 'বাংলা', - Locale(languageCode: 'br', countryCode: 'FR') => 'Brezhoneg', - Locale(languageCode: 'bs', countryCode: 'BA') => 'Bosanski', - Locale(languageCode: 'ca', countryCode: 'ES') => 'Català, valencià', - Locale(languageCode: 'ckb', countryCode: 'IR') => 'کوردی سۆرانی', - Locale(languageCode: 'co', countryCode: 'FR') => 'Corsu', - Locale(languageCode: 'cs', countryCode: 'CZ') => 'Čeština', - Locale(languageCode: 'cv', countryCode: 'CU') => 'чӑваш чӗлхи', - Locale(languageCode: 'cy', countryCode: 'GB') => 'Cymraeg', - Locale(languageCode: 'da', countryCode: 'DK') => 'Dansk', - Locale(languageCode: 'de', countryCode: 'DE') => 'Deutsch', - Locale(languageCode: 'el', countryCode: 'GR') => 'Ελληνικά', - Locale(languageCode: 'en', countryCode: 'US') => 'English (US)', - Locale(languageCode: 'eo', countryCode: 'UY') => 'Esperanto', - Locale(languageCode: 'es', countryCode: 'ES') => 'Español', - Locale(languageCode: 'et', countryCode: 'EE') => 'Eesti keel', - Locale(languageCode: 'eu', countryCode: 'ES') => 'Euskara', - Locale(languageCode: 'fa', countryCode: 'IR') => 'فارسی', - Locale(languageCode: 'fi', countryCode: 'FI') => 'Suomen kieli', - Locale(languageCode: 'fo', countryCode: 'FO') => 'Føroyskt', - Locale(languageCode: 'fr', countryCode: 'FR') => 'Français', - Locale(languageCode: 'frp', countryCode: 'IT') => 'Arpitan', - Locale(languageCode: 'fy', countryCode: 'NL') => 'Frysk', - Locale(languageCode: 'ga', countryCode: 'IE') => 'Gaeilge', - Locale(languageCode: 'gd', countryCode: 'GB') => 'Gàidhlig', - Locale(languageCode: 'gl', countryCode: 'ES') => 'Galego', - Locale(languageCode: 'gsw', countryCode: 'CH') => 'Schwizerdütsch', - Locale(languageCode: 'gu', countryCode: 'IN') => 'ગુજરાતી', - Locale(languageCode: 'he', countryCode: 'IL') => 'עִבְרִית', - Locale(languageCode: 'hi', countryCode: 'IN') => 'हिन्दी, हिंदी', - Locale(languageCode: 'hr', countryCode: 'HR') => 'Hrvatski', - Locale(languageCode: 'hu', countryCode: 'HU') => 'Magyar', - Locale(languageCode: 'hy', countryCode: 'AM') => 'Հայերեն', - Locale(languageCode: 'ia', countryCode: 'IA') => 'Interlingua', - Locale(languageCode: 'id', countryCode: 'ID') => 'Bahasa Indonesia', - Locale(languageCode: 'io', countryCode: 'EN') => 'Ido', - Locale(languageCode: 'is', countryCode: 'IS') => 'Íslenska', - Locale(languageCode: 'it', countryCode: 'IT') => 'Italiano', - Locale(languageCode: 'ja', countryCode: 'JP') => '日本語', - Locale(languageCode: 'jbo', countryCode: 'EN') => 'Lojban', - Locale(languageCode: 'jv', countryCode: 'ID') => 'Basa Jawa', - Locale(languageCode: 'ka', countryCode: 'GE') => 'ქართული', - Locale(languageCode: 'kab', countryCode: 'DZ') => 'Taqvaylit', - Locale(languageCode: 'kk', countryCode: 'KZ') => 'қазақша', - Locale(languageCode: 'kmr', countryCode: 'TR') => 'Kurdî (Kurmancî)', - Locale(languageCode: 'kn', countryCode: 'IN') => 'ಕನ್ನಡ', - Locale(languageCode: 'ko', countryCode: 'KR') => '한국어', - Locale(languageCode: 'ky', countryCode: 'KG') => 'кыргызча', - Locale(languageCode: 'la', countryCode: 'LA') => 'Lingua Latina', - Locale(languageCode: 'lb', countryCode: 'LU') => 'Lëtzebuergesch', - Locale(languageCode: 'lt', countryCode: 'LT') => 'Lietuvių kalba', - Locale(languageCode: 'lv', countryCode: 'LV') => 'Latviešu valoda', - Locale(languageCode: 'mg', countryCode: 'MG') => 'Fiteny malagasy', - Locale(languageCode: 'mk', countryCode: 'MK') => 'македонски јази', - Locale(languageCode: 'ml', countryCode: 'IN') => 'മലയാളം', - Locale(languageCode: 'mn', countryCode: 'MN') => 'монгол', - Locale(languageCode: 'mr', countryCode: 'IN') => 'मराठी', - Locale(languageCode: 'nb', countryCode: 'NO') => 'Norsk bokmål', - Locale(languageCode: 'ne', countryCode: 'NP') => 'नेपाली', - Locale(languageCode: 'nl', countryCode: 'NL') => 'Nederlands', - Locale(languageCode: 'nn', countryCode: 'NO') => 'Norsk nynorsk', - Locale(languageCode: 'pi', countryCode: 'IN') => 'पालि', - Locale(languageCode: 'pl', countryCode: 'PL') => 'Polski', - Locale(languageCode: 'ps', countryCode: 'AF') => 'پښتو', - Locale(languageCode: 'pt', countryCode: 'PT') => 'Português', - Locale(languageCode: 'pt', countryCode: 'BR') => 'Português (BR)', - Locale(languageCode: 'ro', countryCode: 'RO') => 'Română', - Locale(languageCode: 'ru', countryCode: 'RU') => 'русский язык', - Locale(languageCode: 'ry', countryCode: 'UA') => 'Русинська бисїда', - Locale(languageCode: 'sa', countryCode: 'IN') => 'संस्कृत', - Locale(languageCode: 'sk', countryCode: 'SK') => 'Slovenčina', - Locale(languageCode: 'sl', countryCode: 'SI') => 'Slovenščina', - Locale(languageCode: 'so', countryCode: 'SO') => 'Af Soomaali', - Locale(languageCode: 'sq', countryCode: 'AL') => 'Shqip', - Locale(languageCode: 'sr', countryCode: 'SP') => 'Српски језик', - Locale(languageCode: 'sv', countryCode: 'SE') => 'Svenska', - Locale(languageCode: 'sw', countryCode: 'KE') => 'Kiswahili', - Locale(languageCode: 'ta', countryCode: 'IN') => 'தமிழ்', - Locale(languageCode: 'tg', countryCode: 'TJ') => 'тоҷикӣ', - Locale(languageCode: 'th', countryCode: 'TH') => 'ไทย', - Locale(languageCode: 'tk', countryCode: 'TM') => 'Türkmençe', - Locale(languageCode: 'tl', countryCode: 'PH') => 'Tagalog', - Locale(languageCode: 'tp', countryCode: 'TP') => 'Toki pona', - Locale(languageCode: 'tr', countryCode: 'TR') => 'Türkçe', - Locale(languageCode: 'uk', countryCode: 'UA') => 'українська', - Locale(languageCode: 'ur', countryCode: 'PK') => 'اُردُو', - Locale(languageCode: 'uz', countryCode: 'UZ') => 'oʻzbekcha', - Locale(languageCode: 'vi', countryCode: 'VN') => 'Tiếng Việt', - Locale(languageCode: 'yo', countryCode: 'NG') => 'Yorùbá', - Locale(languageCode: 'zh', countryCode: 'CN') => '中文', - Locale(languageCode: 'zh', countryCode: 'TW') => '繁體中文', - Locale(languageCode: 'zu', countryCode: 'ZA') => 'isiZulu', - _ => locale.toString(), - }; + Locale(languageCode: 'en', countryCode: 'GB') => 'English', + Locale(languageCode: 'af', countryCode: 'ZA') => 'Afrikaans', + Locale(languageCode: 'an', countryCode: 'ES') => 'Aragonés', + Locale(languageCode: 'ar', countryCode: 'SA') => 'العربية', + Locale(languageCode: 'as', countryCode: 'IN') => 'অসমীয়া', + Locale(languageCode: 'av', countryCode: 'DA') => 'авар мацӀ', + Locale(languageCode: 'az', countryCode: 'AZ') => 'Azərbaycanca', + Locale(languageCode: 'be', countryCode: 'BY') => 'Беларуская', + Locale(languageCode: 'bg', countryCode: 'BG') => 'български език', + Locale(languageCode: 'bn', countryCode: 'BD') => 'বাংলা', + Locale(languageCode: 'br', countryCode: 'FR') => 'Brezhoneg', + Locale(languageCode: 'bs', countryCode: 'BA') => 'Bosanski', + Locale(languageCode: 'ca', countryCode: 'ES') => 'Català, valencià', + Locale(languageCode: 'ckb', countryCode: 'IR') => 'کوردی سۆرانی', + Locale(languageCode: 'co', countryCode: 'FR') => 'Corsu', + Locale(languageCode: 'cs', countryCode: 'CZ') => 'Čeština', + Locale(languageCode: 'cv', countryCode: 'CU') => 'чӑваш чӗлхи', + Locale(languageCode: 'cy', countryCode: 'GB') => 'Cymraeg', + Locale(languageCode: 'da', countryCode: 'DK') => 'Dansk', + Locale(languageCode: 'de', countryCode: 'DE') => 'Deutsch', + Locale(languageCode: 'el', countryCode: 'GR') => 'Ελληνικά', + Locale(languageCode: 'en', countryCode: 'US') => 'English (US)', + Locale(languageCode: 'eo', countryCode: 'UY') => 'Esperanto', + Locale(languageCode: 'es', countryCode: 'ES') => 'Español', + Locale(languageCode: 'et', countryCode: 'EE') => 'Eesti keel', + Locale(languageCode: 'eu', countryCode: 'ES') => 'Euskara', + Locale(languageCode: 'fa', countryCode: 'IR') => 'فارسی', + Locale(languageCode: 'fi', countryCode: 'FI') => 'Suomen kieli', + Locale(languageCode: 'fo', countryCode: 'FO') => 'Føroyskt', + Locale(languageCode: 'fr', countryCode: 'FR') => 'Français', + Locale(languageCode: 'frp', countryCode: 'IT') => 'Arpitan', + Locale(languageCode: 'fy', countryCode: 'NL') => 'Frysk', + Locale(languageCode: 'ga', countryCode: 'IE') => 'Gaeilge', + Locale(languageCode: 'gd', countryCode: 'GB') => 'Gàidhlig', + Locale(languageCode: 'gl', countryCode: 'ES') => 'Galego', + Locale(languageCode: 'gsw', countryCode: 'CH') => 'Schwizerdütsch', + Locale(languageCode: 'gu', countryCode: 'IN') => 'ગુજરાતી', + Locale(languageCode: 'he', countryCode: 'IL') => 'עִבְרִית', + Locale(languageCode: 'hi', countryCode: 'IN') => 'हिन्दी, हिंदी', + Locale(languageCode: 'hr', countryCode: 'HR') => 'Hrvatski', + Locale(languageCode: 'hu', countryCode: 'HU') => 'Magyar', + Locale(languageCode: 'hy', countryCode: 'AM') => 'Հայերեն', + Locale(languageCode: 'ia', countryCode: 'IA') => 'Interlingua', + Locale(languageCode: 'id', countryCode: 'ID') => 'Bahasa Indonesia', + Locale(languageCode: 'io', countryCode: 'EN') => 'Ido', + Locale(languageCode: 'is', countryCode: 'IS') => 'Íslenska', + Locale(languageCode: 'it', countryCode: 'IT') => 'Italiano', + Locale(languageCode: 'ja', countryCode: 'JP') => '日本語', + Locale(languageCode: 'jbo', countryCode: 'EN') => 'Lojban', + Locale(languageCode: 'jv', countryCode: 'ID') => 'Basa Jawa', + Locale(languageCode: 'ka', countryCode: 'GE') => 'ქართული', + Locale(languageCode: 'kab', countryCode: 'DZ') => 'Taqvaylit', + Locale(languageCode: 'kk', countryCode: 'KZ') => 'қазақша', + Locale(languageCode: 'kmr', countryCode: 'TR') => 'Kurdî (Kurmancî)', + Locale(languageCode: 'kn', countryCode: 'IN') => 'ಕನ್ನಡ', + Locale(languageCode: 'ko', countryCode: 'KR') => '한국어', + Locale(languageCode: 'ky', countryCode: 'KG') => 'кыргызча', + Locale(languageCode: 'la', countryCode: 'LA') => 'Lingua Latina', + Locale(languageCode: 'lb', countryCode: 'LU') => 'Lëtzebuergesch', + Locale(languageCode: 'lt', countryCode: 'LT') => 'Lietuvių kalba', + Locale(languageCode: 'lv', countryCode: 'LV') => 'Latviešu valoda', + Locale(languageCode: 'mg', countryCode: 'MG') => 'Fiteny malagasy', + Locale(languageCode: 'mk', countryCode: 'MK') => 'македонски јази', + Locale(languageCode: 'ml', countryCode: 'IN') => 'മലയാളം', + Locale(languageCode: 'mn', countryCode: 'MN') => 'монгол', + Locale(languageCode: 'mr', countryCode: 'IN') => 'मराठी', + Locale(languageCode: 'nb', countryCode: 'NO') => 'Norsk bokmål', + Locale(languageCode: 'ne', countryCode: 'NP') => 'नेपाली', + Locale(languageCode: 'nl', countryCode: 'NL') => 'Nederlands', + Locale(languageCode: 'nn', countryCode: 'NO') => 'Norsk nynorsk', + Locale(languageCode: 'pi', countryCode: 'IN') => 'पालि', + Locale(languageCode: 'pl', countryCode: 'PL') => 'Polski', + Locale(languageCode: 'ps', countryCode: 'AF') => 'پښتو', + Locale(languageCode: 'pt', countryCode: 'PT') => 'Português', + Locale(languageCode: 'pt', countryCode: 'BR') => 'Português (BR)', + Locale(languageCode: 'ro', countryCode: 'RO') => 'Română', + Locale(languageCode: 'ru', countryCode: 'RU') => 'русский язык', + Locale(languageCode: 'ry', countryCode: 'UA') => 'Русинська бисїда', + Locale(languageCode: 'sa', countryCode: 'IN') => 'संस्कृत', + Locale(languageCode: 'sk', countryCode: 'SK') => 'Slovenčina', + Locale(languageCode: 'sl', countryCode: 'SI') => 'Slovenščina', + Locale(languageCode: 'so', countryCode: 'SO') => 'Af Soomaali', + Locale(languageCode: 'sq', countryCode: 'AL') => 'Shqip', + Locale(languageCode: 'sr', countryCode: 'SP') => 'Српски језик', + Locale(languageCode: 'sv', countryCode: 'SE') => 'Svenska', + Locale(languageCode: 'sw', countryCode: 'KE') => 'Kiswahili', + Locale(languageCode: 'ta', countryCode: 'IN') => 'தமிழ்', + Locale(languageCode: 'tg', countryCode: 'TJ') => 'тоҷикӣ', + Locale(languageCode: 'th', countryCode: 'TH') => 'ไทย', + Locale(languageCode: 'tk', countryCode: 'TM') => 'Türkmençe', + Locale(languageCode: 'tl', countryCode: 'PH') => 'Tagalog', + Locale(languageCode: 'tp', countryCode: 'TP') => 'Toki pona', + Locale(languageCode: 'tr', countryCode: 'TR') => 'Türkçe', + Locale(languageCode: 'uk', countryCode: 'UA') => 'українська', + Locale(languageCode: 'ur', countryCode: 'PK') => 'اُردُو', + Locale(languageCode: 'uz', countryCode: 'UZ') => 'oʻzbekcha', + Locale(languageCode: 'vi', countryCode: 'VN') => 'Tiếng Việt', + Locale(languageCode: 'yo', countryCode: 'NG') => 'Yorùbá', + Locale(languageCode: 'zh', countryCode: 'CN') => '中文', + Locale(languageCode: 'zh', countryCode: 'TW') => '繁體中文', + Locale(languageCode: 'zu', countryCode: 'ZA') => 'isiZulu', + _ => locale.toString(), +}; diff --git a/lib/src/utils/lichess_assets.dart b/lib/src/utils/lichess_assets.dart index a77fde17c0..a2f43bb50c 100644 --- a/lib/src/utils/lichess_assets.dart +++ b/lib/src/utils/lichess_assets.dart @@ -7,7 +7,3 @@ String lichessFlagSrc(String country) { String lichessFlairSrc(String flair) { return '$kLichessCDNHost/assets/flair/img/$flair.webp'; } - -String lichessFideFedSrc(String name) { - return '$kLichessCDNHost/assets/images/fide-fed/$name.svg'; -} diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart index 46beeaa5d8..11dea40fe4 100644 --- a/lib/src/utils/navigation.dart +++ b/lib/src/utils/navigation.dart @@ -1,53 +1,110 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -/// Push a new route using Navigator +/// A page route that always builds the same screen widget. /// -/// Will use [MaterialPageRoute] on Android and [CupertinoPageRoute] on iOS. +/// This is useful to inspect new screens being pushed to the Navigator in tests. +abstract class ScreenRoute extends PageRoute { + /// The widget that this page route always builds. + Widget get screen; +} + +/// A [MaterialPageRoute] that always builds the same screen widget. +/// +/// This is useful to test new screens being pushed to the Navigator. +class MaterialScreenRoute extends MaterialPageRoute + implements ScreenRoute { + MaterialScreenRoute({ + required this.screen, + super.settings, + super.maintainState, + super.fullscreenDialog, + super.allowSnapshotting, + }) : super(builder: (_) => screen); + + @override + final Widget screen; +} + +/// A [CupertinoPageRoute] that always builds the same screen widget. +/// +/// This is useful to test new screens being pushed to the Navigator. +class CupertinoScreenRoute extends CupertinoPageRoute + implements ScreenRoute { + CupertinoScreenRoute({ + required this.screen, + super.settings, + super.maintainState, + super.fullscreenDialog, + super.title, + }) : super(builder: (_) => screen); + + @override + final Widget screen; +} + +/// Push a new route using Navigator. +/// +/// Either [builder] or [screen] must be provided. +/// +/// If [builder] if provided, it will return a [MaterialPageRoute] on Android and +/// a [CupertinoPageRoute] on iOS. +/// +/// If [screen] is provided, it will return a [MaterialScreenRoute] on Android and +/// a [CupertinoScreenRoute] on iOS. Future pushPlatformRoute( BuildContext context, { - required WidgetBuilder builder, + Widget? screen, + WidgetBuilder? builder, bool rootNavigator = false, bool fullscreenDialog = false, String? title, }) { - return Navigator.of( - context, - rootNavigator: rootNavigator, - ).push( + assert(screen != null || builder != null, 'Either screen or builder must be provided.'); + + return Navigator.of(context, rootNavigator: rootNavigator).push( Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) - : MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ), + ? builder != null + ? CupertinoPageRoute(builder: builder, title: title, fullscreenDialog: fullscreenDialog) + : CupertinoScreenRoute( + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : builder != null + ? MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog) + : MaterialScreenRoute(screen: screen!, fullscreenDialog: fullscreenDialog), ); } +/// Push a new route using Navigator and replace the current route. +/// +/// Either [builder] or [screen] must be provided. +/// +/// If [builder] if provided, it will return a [MaterialPageRoute] on Android and +/// a [CupertinoPageRoute] on iOS. +/// +/// If [screen] is provided, it will return a [MaterialScreenRoute] on Android and +/// a [CupertinoScreenRoute] on iOS. Future pushReplacementPlatformRoute( BuildContext context, { - required WidgetBuilder builder, + WidgetBuilder? builder, + Widget? screen, bool rootNavigator = false, bool fullscreenDialog = false, String? title, }) { - return Navigator.of( - context, - rootNavigator: rootNavigator, - ).pushReplacement( + return Navigator.of(context, rootNavigator: rootNavigator).pushReplacement( Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) - : MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ), + ? builder != null + ? CupertinoPageRoute(builder: builder, title: title, fullscreenDialog: fullscreenDialog) + : CupertinoScreenRoute( + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : builder != null + ? MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog) + : MaterialScreenRoute(screen: screen!, fullscreenDialog: fullscreenDialog), ); } diff --git a/lib/src/utils/package_info.dart b/lib/src/utils/package_info.dart deleted file mode 100644 index e9f5a8cf79..0000000000 --- a/lib/src/utils/package_info.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'package_info.g.dart'; - -@Riverpod(keepAlive: true) -PackageInfo packageInfo(PackageInfoRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.packageInfo; -} diff --git a/lib/src/utils/rate_limit.dart b/lib/src/utils/rate_limit.dart index 5033ea8076..901de4eff3 100644 --- a/lib/src/utils/rate_limit.dart +++ b/lib/src/utils/rate_limit.dart @@ -4,9 +4,7 @@ class Debouncer { final Duration delay; Timer? _timer; - Debouncer( - this.delay, - ); + Debouncer(this.delay); void call(void Function() action) { _timer?.cancel(); diff --git a/lib/src/utils/riverpod.dart b/lib/src/utils/riverpod.dart index cc09b77654..a4976c6ea0 100644 --- a/lib/src/utils/riverpod.dart +++ b/lib/src/utils/riverpod.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -extension AutoDisposeRefExtension on AutoDisposeRef { +extension RefExtension on Ref { /// Keeps the provider alive for [duration] KeepAliveLink cacheFor(Duration duration) { final link = keepAlive(); @@ -9,9 +9,7 @@ extension AutoDisposeRefExtension on AutoDisposeRef { onDispose(timer.cancel); return link; } -} -extension RefExtension on Ref { /// Delays an execution by a bit such that if a dependency changes multiple /// time rapidly, the rest of the code is only run once. Future debounce(Duration duration) { diff --git a/lib/src/utils/screen.dart b/lib/src/utils/screen.dart index eb4ba411a3..1266a6e1e5 100644 --- a/lib/src/utils/screen.dart +++ b/lib/src/utils/screen.dart @@ -7,8 +7,7 @@ double estimateRemainingHeightLeftBoard(BuildContext context) { final padding = MediaQuery.paddingOf(context); final safeViewportHeight = size.height - padding.top - padding.bottom; final boardSize = size.width; - final appBarHeight = - Theme.of(context).platform == TargetPlatform.iOS ? 44.0 : 56.0; + final appBarHeight = Theme.of(context).platform == TargetPlatform.iOS ? 44.0 : 56.0; return safeViewportHeight - boardSize - appBarHeight - kBottomBarHeight; } diff --git a/lib/src/utils/share.dart b/lib/src/utils/share.dart index 167290a4cc..1f7a2e33a9 100644 --- a/lib/src/utils/share.dart +++ b/lib/src/utils/share.dart @@ -7,6 +7,7 @@ import 'package:share_plus/share_plus.dart'; /// in order to make it work on iPads. Future launchShareDialog( BuildContext context, { + /// The uri to share. Uri? uri, @@ -26,12 +27,7 @@ Future launchShareDialog( if (uri != null) { return Share.shareUri(uri); } else if (files != null) { - return Share.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: origin, - ); + return Share.shareXFiles(files, subject: subject, text: text, sharePositionOrigin: origin); } else if (text != null) { return Share.share(text, subject: subject, sharePositionOrigin: origin); } diff --git a/lib/src/utils/system.dart b/lib/src/utils/system.dart index 70937039de..77fbb21e7a 100644 --- a/lib/src/utils/system.dart +++ b/lib/src/utils/system.dart @@ -20,6 +20,8 @@ class System { } on PlatformException catch (e) { debugPrint('Failed to get total RAM: ${e.message}'); return null; + } on MissingPluginException catch (_) { + return null; } } @@ -29,8 +31,7 @@ class System { Future clearUserData() async { if (Platform.isAndroid) { try { - final result = - await _channel.invokeMethod('clearApplicationUserData'); + final result = await _channel.invokeMethod('clearApplicationUserData'); return result ?? false; } on PlatformException catch (e) { debugPrint('Failed to clear user data: ${e.message}'); @@ -43,8 +44,7 @@ class System { } /// A provider that returns OS version of an Android device. -final androidVersionProvider = - FutureProvider((ref) async { +final androidVersionProvider = FutureProvider((ref) async { if (!Platform.isAndroid) { return null; } diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index 0c19c32b40..1cb1c0dbe6 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/user/countries.dart'; @@ -12,35 +12,60 @@ import 'package:lichess_mobile/src/widgets/adaptive_autocomplete.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:result_extensions/result_extensions.dart'; final _countries = countries.values.toList(); +final _cupertinoTextFieldDecoration = BoxDecoration( + color: CupertinoColors.tertiarySystemBackground, + border: Border.all(color: CupertinoColors.systemGrey4, width: 1), + borderRadius: BorderRadius.circular(8), +); class EditProfileScreen extends StatelessWidget { const EditProfileScreen({super.key}); - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, + Future _showBackDialog(BuildContext context) async { + return showAdaptiveDialog( + context: context, + builder: (context) { + return PlatformAlertDialog( + title: Text(context.l10n.mobileAreYouSure), + content: const Text('Your changes will be lost.'), + actions: [ + PlatformDialogAction( + child: Text(context.l10n.cancel), + onPressed: () => Navigator.of(context).pop(false), + ), + PlatformDialogAction( + child: Text(context.l10n.ok), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, ); } - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.editProfile), + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.editProfile)), + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) async { + if (didPop) { + return; + } + final NavigatorState navigator = Navigator.of(context); + final bool? shouldPop = await _showBackDialog(context); + if (shouldPop ?? false) { + navigator.pop(); + } + }, + child: _Body(), ), - body: _Body(), - ); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), ); } } @@ -52,18 +77,22 @@ class _Body extends ConsumerWidget { return account.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } - return Padding( - padding: Styles.bodyPadding, - child: ListView( - children: [ - Text(context.l10n.allInformationIsPublicAndOptional), - const SizedBox(height: 16), - _EditProfileForm(data), - ], + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Padding( + padding: Styles.bodyPadding.copyWith(top: 0, bottom: 0), + child: ListView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: [ + SizedBox(height: Styles.bodyPadding.top), + Text(context.l10n.allInformationIsPublicAndOptional), + const SizedBox(height: 16), + _EditProfileForm(data), + SizedBox(height: Styles.bodyPadding.bottom), + ], + ), ), ); }, @@ -100,36 +129,24 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { 'links': null, }; - final _cupertinoTextFieldDecoration = BoxDecoration( - color: CupertinoColors.tertiarySystemBackground, - border: Border.all( - color: CupertinoColors.systemGrey4, - width: 1, - ), - borderRadius: BorderRadius.circular(8), - ); - Future? _pendingSaveProfile; @override Widget build(BuildContext context) { - final String? initialLinks = - widget.user.profile?.links?.map((e) => e.url).join('\r\n'); - + final String? initialLinks = widget.user.profile?.links?.map((e) => e.url).join('\r\n'); return Form( key: _formKey, child: Column( children: [ - _textField( + _TextField( label: context.l10n.biography, initialValue: widget.user.profile?.bio, formKey: 'bio', - controller: TextEditingController( - text: widget.user.profile?.bio, - ), + formData: _formData, description: context.l10n.biographyDescription, maxLength: 400, maxLines: 6, + textInputAction: TextInputAction.newline, ), Padding( padding: const EdgeInsets.only(bottom: 16.0), @@ -147,17 +164,16 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { AdaptiveAutoComplete( cupertinoDecoration: _cupertinoTextFieldDecoration, textInputAction: TextInputAction.next, - initialValue: field.value != null - ? TextEditingValue(text: countries[field.value]!) - : null, + initialValue: + field.value != null + ? TextEditingValue(text: countries[field.value]!) + : null, optionsBuilder: (TextEditingValue value) { if (value.text.isEmpty) { return const Iterable.empty(); } return _countries.where((String option) { - return option - .toLowerCase() - .contains(value.text.toLowerCase()); + return option.toLowerCase().contains(value.text.toLowerCase()); }); }, onSelected: (String selection) { @@ -172,31 +188,26 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { }, ), ), - _textField( + _TextField( label: context.l10n.location, initialValue: widget.user.profile?.location, - controller: TextEditingController( - text: widget.user.profile?.location, - ), + formData: _formData, formKey: 'location', maxLength: 80, ), - _textField( + _TextField( label: context.l10n.realName, initialValue: widget.user.profile?.realName, formKey: 'realName', - controller: TextEditingController( - text: widget.user.profile?.realName, - ), + formData: _formData, maxLength: 20, ), - _numericField( + + _NumericField( label: context.l10n.xRating('FIDE'), initialValue: widget.user.profile?.fideRating, formKey: 'fideRating', - controller: TextEditingController( - text: widget.user.profile?.fideRating?.toString(), - ), + formData: _formData, validator: (value) { if (value != null && (value < 1400 || value > 3000)) { return 'Rating must be between 1400 and 3000'; @@ -204,13 +215,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _numericField( + _NumericField( label: context.l10n.xRating('USCF'), initialValue: widget.user.profile?.uscfRating, formKey: 'uscfRating', - controller: TextEditingController( - text: widget.user.profile?.uscfRating?.toString(), - ), + formData: _formData, validator: (value) { if (value != null && (value < 100 || value > 3000)) { return 'Rating must be between 100 and 3000'; @@ -218,14 +227,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _numericField( + _NumericField( label: context.l10n.xRating('ECF'), initialValue: widget.user.profile?.ecfRating, formKey: 'ecfRating', - controller: TextEditingController( - text: widget.user.profile?.ecfRating?.toString(), - ), - textInputAction: TextInputAction.done, + formData: _formData, validator: (value) { if (value != null && (value < 0 || value > 3000)) { return 'Rating must be between 0 and 3000'; @@ -233,11 +239,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _textField( + _TextField( label: context.l10n.socialMediaLinks, initialValue: initialLinks, formKey: 'links', - controller: TextEditingController(text: initialLinks), + formData: _formData, maxLength: 3000, maxLines: 4, textInputAction: TextInputAction.newline, @@ -251,55 +257,53 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { builder: (context, snapshot) { return FatButton( semanticsLabel: context.l10n.apply, - onPressed: snapshot.connectionState == ConnectionState.waiting - ? null - : () async { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - _formData.removeWhere((key, value) { - return value == null; - }); - final future = Result.capture( - ref.withClient( - (client) => - AccountRepository(client).saveProfile( - _formData.map( - (key, value) => - MapEntry(key, value.toString()), + onPressed: + snapshot.connectionState == ConnectionState.waiting + ? null + : () async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + _formData.removeWhere((key, value) { + return value == null; + }); + final future = Result.capture( + ref.withClient( + (client) => AccountRepository(client).saveProfile( + _formData.map((key, value) => MapEntry(key, value.toString())), ), ), - ), - ); + ); - setState(() { - _pendingSaveProfile = future; - }); + setState(() { + _pendingSaveProfile = future; + }); - final result = await future; + final result = await future; - result.match( - onError: (err, __) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Something went wrong', - type: SnackBarType.error, - ); - } - }, - onSuccess: (_) { - if (context.mounted) { - ref.invalidate(accountProvider); - showPlatformSnackbar( - context, - context.l10n.success, - type: SnackBarType.success, - ); - } - }, - ); - } - }, + result.match( + onError: (err, __) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Something went wrong', + type: SnackBarType.error, + ); + } + }, + onSuccess: (_) { + if (context.mounted) { + ref.invalidate(accountProvider); + showPlatformSnackbar( + context, + context.l10n.success, + type: SnackBarType.success, + ); + Navigator.of(context).pop(); + } + }, + ); + } + }, child: Text(context.l10n.apply), ); }, @@ -309,64 +313,79 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), ); } +} + +class _NumericField extends StatefulWidget { + final String label; + final int? initialValue; + final String formKey; + final String? Function(int?)? validator; + final Map formData; + const _NumericField({ + required this.label, + required this.initialValue, + required this.formKey, + required this.validator, + required this.formData, + }); + + @override + State<_NumericField> createState() => __NumericFieldState(); +} + +class __NumericFieldState extends State<_NumericField> { + final _controller = TextEditingController(); + @override + void initState() { + _controller.text = widget.initialValue?.toString() ?? ''; + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } - Widget _textField({ - required String label, - required String? initialValue, - required String formKey, - required TextEditingController controller, - String? description, - int? maxLength, - int? maxLines, - TextInputAction textInputAction = TextInputAction.next, - }) { + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: FormField( - initialValue: initialValue, + child: FormField( + initialValue: widget.initialValue, onSaved: (value) { - _formData[formKey] = value; + widget.formData[widget.formKey] = value; }, - builder: (FormFieldState field) { + validator: widget.validator, + builder: (FormFieldState field) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: Styles.formLabel), + Text(widget.label, style: Styles.formLabel), const SizedBox(height: 6.0), AdaptiveTextField( - maxLength: maxLength, - maxLines: maxLines, + controller: _controller, + keyboardType: TextInputType.number, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( - color: field.errorText == null - ? CupertinoColors.systemGrey4 - : context.lichessColors.error, + color: + field.errorText == null + ? CupertinoColors.systemGrey4 + : context.lichessColors.error, width: 1, ), ), - materialDecoration: field.errorText != null - ? InputDecoration( - errorText: field.errorText, - ) - : null, - textInputAction: textInputAction, - controller: controller, + materialDecoration: + field.errorText != null ? InputDecoration(errorText: field.errorText) : null, + onChanged: (value) { - field.didChange(value.trim()); + field.didChange(int.tryParse(value)); }, ), - if (description != null) ...[ - const SizedBox(height: 6.0), - Text(description, style: Styles.formDescription), - ], - if (Theme.of(context).platform == TargetPlatform.iOS && - field.errorText != null) + if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text( - field.errorText!, - style: Styles.formError, - ), + child: Text(field.errorText!, style: Styles.formError), ), ], ); @@ -374,58 +393,90 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), ); } +} + +class _TextField extends StatefulWidget { + final String label; + final String? initialValue; + final String formKey; + final String? description; + final int? maxLength; + final int? maxLines; + final Map formData; + final TextInputAction textInputAction; + const _TextField({ + required this.label, + required this.initialValue, + required this.formKey, + required this.formData, + this.description, + this.maxLength, + this.maxLines, + this.textInputAction = TextInputAction.next, + }); - Widget _numericField({ - required String label, - required int? initialValue, - required String formKey, - required TextEditingController controller, - required String? Function(int?)? validator, - TextInputAction textInputAction = TextInputAction.next, - }) { + @override + State<_TextField> createState() => __TextFieldState(); +} + +class __TextFieldState extends State<_TextField> { + final _controller = TextEditingController(); + @override + void initState() { + super.initState(); + _controller.text = widget.initialValue ?? ''; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: FormField( - initialValue: initialValue, + child: FormField( + initialValue: widget.initialValue, onSaved: (value) { - _formData[formKey] = value; + widget.formData[widget.formKey] = value?.trim(); }, - validator: validator, - builder: (FormFieldState field) { + + builder: (FormFieldState field) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: Styles.formLabel), + Text(widget.label, style: Styles.formLabel), const SizedBox(height: 6.0), AdaptiveTextField( - keyboardType: TextInputType.number, + maxLength: widget.maxLength, + maxLines: widget.maxLines, + controller: _controller, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( - color: field.errorText == null - ? CupertinoColors.systemGrey4 - : context.lichessColors.error, + color: + field.errorText == null + ? CupertinoColors.systemGrey4 + : context.lichessColors.error, width: 1, ), ), - materialDecoration: field.errorText != null - ? InputDecoration( - errorText: field.errorText, - ) - : null, - textInputAction: textInputAction, - controller: controller, + materialDecoration: + field.errorText != null ? InputDecoration(errorText: field.errorText) : null, + textInputAction: widget.textInputAction, onChanged: (value) { - field.didChange(int.tryParse(value)); + field.didChange(value.trim()); }, ), - if (Theme.of(context).platform == TargetPlatform.iOS && - field.errorText != null) + if (widget.description != null) ...[ + const SizedBox(height: 6.0), + Text(widget.description!, style: Styles.formDescription), + ], + if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text( - field.errorText!, - style: Styles.formError, - ), + child: Text(field.errorText!, style: Styles.formError), ), ], ); diff --git a/lib/src/view/account/profile_screen.dart b/lib/src/view/account/profile_screen.dart index b43c3a222f..805d031e7f 100644 --- a/lib/src/view/account/profile_screen.dart +++ b/lib/src/view/account/profile_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; @@ -12,7 +11,7 @@ import 'package:lichess_mobile/src/view/user/user_activity.dart'; import 'package:lichess_mobile/src/view/user/user_profile.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -21,21 +20,13 @@ class ProfileScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildAndroid(BuildContext context, WidgetRef ref) { final account = ref.watch(accountProvider); - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: account.when( - data: (user) => user == null - ? const SizedBox.shrink() - : UserFullNameWidget(user: user.lightUser), + data: + (user) => + user == null ? const SizedBox.shrink() : UserFullNameWidget(user: user.lightUser), loading: () => const SizedBox.shrink(), error: (error, _) => const SizedBox.shrink(), ), @@ -43,19 +34,14 @@ class ProfileScreen extends ConsumerWidget { AppBarIconButton( icon: const Icon(Icons.edit), semanticsLabel: context.l10n.editProfile, - onPressed: () => pushPlatformRoute( - context, - builder: (_) => const EditProfileScreen(), - ), + onPressed: () => pushPlatformRoute(context, builder: (_) => const EditProfileScreen()), ), ], ), body: account.when( data: (user) { if (user == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return ListView( children: [ @@ -68,59 +54,7 @@ class ProfileScreen extends ConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountProvider), - ); - }, - ), - ); - } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - final account = ref.watch(accountProvider); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: account.when( - data: (user) => user == null - ? const SizedBox.shrink() - : UserFullNameWidget(user: user.lightUser), - loading: () => const SizedBox.shrink(), - error: (error, _) => const SizedBox.shrink(), - ), - trailing: AppBarIconButton( - icon: const Icon(CupertinoIcons.square_pencil), - semanticsLabel: context.l10n.editProfile, - onPressed: () => pushPlatformRoute( - title: context.l10n.editProfile, - context, - builder: (_) => const EditProfileScreen(), - ), - ), - ), - child: account.when( - data: (user) { - if (user == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); - } - return SafeArea( - child: ListView( - children: [ - UserProfileWidget(user: user), - const AccountPerfCards(), - const UserActivityWidget(), - const RecentGamesWidget(), - ], - ), - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(accountProvider)); }, ), ); @@ -143,31 +77,33 @@ class AccountPerfCards extends ConsumerWidget { return const SizedBox.shrink(); } }, - loading: () => Shimmer( - child: Padding( - padding: padding ?? Styles.bodySectionPadding, - child: SizedBox( - height: 106, - child: ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 3.0), - scrollDirection: Axis.horizontal, - itemCount: 5, - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) => ShimmerLoading( - isLoading: true, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10.0), - ), + loading: + () => Shimmer( + child: Padding( + padding: padding ?? Styles.bodySectionPadding, + child: SizedBox( + height: 106, + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 3.0), + scrollDirection: Axis.horizontal, + itemCount: 5, + separatorBuilder: (context, index) => const SizedBox(width: 10), + itemBuilder: + (context, index) => ShimmerLoading( + isLoading: true, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(10.0), + ), + ), + ), ), ), ), ), - ), - ), error: (error, stack) => const SizedBox.shrink(), ); } diff --git a/lib/src/view/account/rating_pref_aware.dart b/lib/src/view/account/rating_pref_aware.dart index 46a0289348..a71faf9acc 100644 --- a/lib/src/view/account/rating_pref_aware.dart +++ b/lib/src/view/account/rating_pref_aware.dart @@ -9,11 +9,7 @@ class RatingPrefAware extends ConsumerWidget { /// in their settings. /// /// Optionally, a different [orElse] widget can be displayed if ratings are disabled. - const RatingPrefAware({ - required this.child, - this.orElse, - super.key, - }); + const RatingPrefAware({required this.child, this.orElse, super.key}); final Widget child; final Widget? orElse; diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart new file mode 100644 index 0000000000..d454dd8ecc --- /dev/null +++ b/lib/src/view/analysis/analysis_board.dart @@ -0,0 +1,129 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; + +class AnalysisBoard extends ConsumerStatefulWidget { + const AnalysisBoard( + this.options, + this.boardSize, { + this.borderRadius, + this.enableDrawingShapes = true, + this.shouldReplaceChildOnUserMove = false, + }); + + final AnalysisOptions options; + final double boardSize; + final BorderRadiusGeometry? borderRadius; + + final bool enableDrawingShapes; + final bool shouldReplaceChildOnUserMove; + + @override + ConsumerState createState() => AnalysisBoardState(); +} + +class AnalysisBoardState extends ConsumerState { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.options); + final analysisState = ref.watch(ctrlProvider).requireValue; + final boardPrefs = ref.watch(boardPreferencesProvider); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final enableComputerAnalysis = analysisPrefs.enableComputerAnalysis; + final showBestMoveArrow = enableComputerAnalysis && analysisPrefs.showBestMoveArrow; + final showAnnotationsOnBoard = enableComputerAnalysis && analysisPrefs.showAnnotations; + final evalBestMoves = + enableComputerAnalysis + ? ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)) + : null; + + final currentNode = analysisState.currentNode; + final annotation = showAnnotationsOnBoard ? makeAnnotation(currentNode.nags) : null; + + final bestMoves = enableComputerAnalysis ? evalBestMoves ?? currentNode.eval?.bestMoves : null; + + final sanMove = currentNode.sanMove; + + final ISet bestMoveShapes = + showBestMoveArrow && analysisState.isEngineAvailable && bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + return Chessboard( + size: widget.boardSize, + fen: analysisState.position.fen, + lastMove: analysisState.lastMove as NormalMove?, + orientation: analysisState.pov, + game: GameData( + playerSide: + analysisState.position.isGameOver + ? PlayerSide.none + : analysisState.position.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, + sideToMove: analysisState.position.turn, + validMoves: analysisState.validMoves, + promotionMove: analysisState.promotionMove, + onMove: + (move, {isDrop, captured}) => ref + .read(ctrlProvider.notifier) + .onUserMove(move, shouldReplace: widget.shouldReplaceChildOnUserMove), + onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), + ), + shapes: userShapes.union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) + : IMap({sanMove.move.to: annotation}) + : null, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: widget.enableDrawingShapes, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + ); + } + + void _onCompleteShape(Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } + + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); + } +} diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart new file mode 100644 index 0000000000..ee43d4a8b3 --- /dev/null +++ b/lib/src/view/analysis/analysis_layout.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// The height of the board header or footer in the analysis layout. +const kAnalysisBoardHeaderOrFooterHeight = 26.0; + +typedef BoardBuilder = + Widget Function(BuildContext context, double boardSize, BorderRadius? boardRadius); + +typedef EngineGaugeBuilder = Widget Function(BuildContext context, Orientation orientation); + +enum AnalysisTab { + opening(Icons.explore), + moves(LichessIcons.flow_cascade), + summary(Icons.area_chart); + + const AnalysisTab(this.icon); + + final IconData icon; + + String l10n(AppLocalizations l10n) { + switch (this) { + case AnalysisTab.opening: + return l10n.openingExplorer; + case AnalysisTab.moves: + return l10n.movesPlayed; + case AnalysisTab.summary: + return l10n.computerAnalysis; + } + } +} + +/// Indicator for the analysis tab, typically shown in the app bar. +class AppBarAnalysisTabIndicator extends StatefulWidget { + const AppBarAnalysisTabIndicator({required this.tabs, required this.controller, super.key}); + + final TabController controller; + + /// Typically a list of two or more [AnalysisTab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [AnalysisLayout.children] list. + final List tabs; + + @override + State createState() => _AppBarAnalysisTabIndicatorState(); +} + +class _AppBarAnalysisTabIndicatorState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.controller.animation?.addListener(_handleTabAnimationTick); + widget.controller.addListener(_handleTabChange); + } + + @override + void dispose() { + widget.controller.animation?.removeListener(_handleTabAnimationTick); + widget.controller.removeListener(_handleTabChange); + super.dispose(); + } + + void _handleTabAnimationTick() { + if (widget.controller.indexIsChanging) { + setState(() { + // Rebuild the widget when the tab index is changing. + }); + } + } + + void _handleTabChange() { + setState(() { + // Rebuild the widget when the tab changes. + }); + } + + @override + Widget build(BuildContext context) { + return AppBarIconButton( + icon: Icon(widget.tabs[widget.controller.index].icon), + semanticsLabel: widget.tabs[widget.controller.index].l10n(context.l10n), + onPressed: () { + showAdaptiveActionSheet( + context: context, + actions: + widget.tabs.map((tab) { + return BottomSheetAction( + leading: Icon(tab.icon), + makeLabel: (context) => Text(tab.l10n(context.l10n)), + onPressed: (_) { + widget.controller.animateTo(widget.tabs.indexOf(tab)); + }, + ); + }).toList(), + ); + }, + ); + } +} + +/// Layout for the analysis and similar screens (study, broadcast, etc.). +/// +/// The layout is responsive and adapts to the screen size and orientation. +/// +/// It includes a [TabBarView] with the [children] widgets. If a [TabController] +/// is not provided, then there must be a [DefaultTabController] ancestor. +/// +/// The length of the [children] list must match the [tabController]'s +/// [TabController.length] and the length of the [AppBarAnalysisTabIndicator.tabs] +class AnalysisLayout extends StatelessWidget { + const AnalysisLayout({ + this.tabController, + required this.boardBuilder, + required this.children, + this.boardHeader, + this.boardFooter, + this.engineGaugeBuilder, + this.engineLines, + this.bottomBar, + super.key, + }); + + /// The tab controller for the tab view. + final TabController? tabController; + + /// The builder for the board widget. + final BoardBuilder boardBuilder; + + /// A widget to show above the board. + /// + /// The widget will included in a parent container with a height of + /// [kAnalysisBoardHeaderOrFooterHeight]. + final Widget? boardHeader; + + /// A widget to show below the board. + /// + /// The widget will included in a parent container with a height of + /// [kAnalysisBoardHeaderOrFooterHeight]. + final Widget? boardFooter; + + /// The children of the tab view. + /// + /// The length of this list must match the [tabController]'s [TabController.length] + /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. + final List children; + + /// A builder for the engine gauge widget. + final EngineGaugeBuilder? engineGaugeBuilder; + + /// A widget to show below the engine gauge, typically the engine lines. + final Widget? engineLines; + + /// A widget to show at the bottom of the screen. + final Widget? bottomBar; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; + final isTablet = isTabletOrLarger(context); + const tabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); + + if (orientation == Orientation.landscape) { + final headerAndFooterHeight = + (boardHeader != null ? kAnalysisBoardHeaderOrFooterHeight : 0.0) + + (boardFooter != null ? kAnalysisBoardHeaderOrFooterHeight : 0.0); + final sideWidth = + constraints.biggest.longestSide - constraints.biggest.shortestSide; + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); + final boardSize = + (sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2)) - + headerAndFooterHeight; + return Padding( + padding: const EdgeInsets.all(kTabletBoardTableSidePadding), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + children: [ + if (boardHeader != null) + Container( + decoration: BoxDecoration( + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, + ), + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, + child: SizedBox( + height: kAnalysisBoardHeaderOrFooterHeight, + width: boardSize, + child: boardHeader, + ), + ), + boardBuilder( + context, + boardSize, + isTablet && boardHeader == null && boardFooter != null + ? tabletBoardRadius + : null, + ), + if (boardFooter != null) + Container( + decoration: BoxDecoration( + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, + ), + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, + width: boardSize, + child: boardFooter, + ), + ], + ), + if (engineGaugeBuilder != null) ...[ + const SizedBox(width: 4.0), + engineGaugeBuilder!(context, Orientation.landscape), + ], + const SizedBox(width: 16.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (engineLines != null) engineLines!, + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + semanticContainer: false, + child: TabBarView(controller: tabController, children: children), + ), + ), + ], + ), + ), + ], + ), + ); + } else { + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (engineGaugeBuilder != null) + engineGaugeBuilder!(context, Orientation.portrait), + if (engineLines != null) engineLines!, + Padding( + padding: + isTablet + ? const EdgeInsets.all(kTabletBoardTableSidePadding) + : EdgeInsets.zero, + child: Column( + children: [ + if (boardHeader != null) + Container( + decoration: BoxDecoration( + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, + ), + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, + child: boardHeader, + ), + boardBuilder( + context, + boardSize, + isTablet && boardHeader == null && boardFooter != null + ? tabletBoardRadius + : null, + ), + if (boardFooter != null) + Container( + decoration: BoxDecoration( + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, + ), + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, + child: boardFooter, + ), + ], + ), + ), + Expanded( + child: Padding( + padding: + isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView(controller: tabController, children: children), + ), + ), + ], + ); + } + }, + ), + ), + ), + if (bottomBar != null) bottomBar!, + ], + ); + } +} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 9c44b229bc..78c78a9c70 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,851 +1,297 @@ -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:chessground/chessground.dart' as cg; -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/game/game_common_widgets.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:popover/popover.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; import '../../utils/share.dart'; +import 'analysis_board.dart'; import 'analysis_settings.dart'; -import 'analysis_share_screen.dart'; -import 'annotations.dart'; import 'tree_view.dart'; -class AnalysisScreen extends StatelessWidget { - const AnalysisScreen({ - required this.options, - required this.pgnOrId, - this.title, - }); - - /// The analysis options. - final AnalysisOptions options; - - /// The PGN or game ID to load. - final String pgnOrId; - - final String? title; - - @override - Widget build(BuildContext context) { - return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame(GameId(pgnOrId), options, title) - : _LoadedAnalysisScreen( - options: options, - pgn: pgnOrId, - title: title, - ); - } -} +final _logger = Logger('AnalysisScreen'); -class _LoadGame extends ConsumerWidget { - const _LoadGame(this.gameId, this.options, this.title); +class AnalysisScreen extends ConsumerStatefulWidget { + const AnalysisScreen({required this.options, this.enableDrawingShapes = true}); final AnalysisOptions options; - final GameId gameId; - final String? title; - @override - Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); + final bool enableDrawingShapes; - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - ), - pgn: game.makePgn(), - title: title, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ); - } + @override + ConsumerState createState() => _AnalysisScreenState(); } -class _LoadedAnalysisScreen extends ConsumerWidget { - const _LoadedAnalysisScreen({ - required this.options, - required this.pgn, - this.title, - }); - - final AnalysisOptions options; - final String pgn; - - final String? title; +class _AnalysisScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late final List tabs; + late final TabController _tabController; @override - Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, - ); - } + void initState() { + super.initState(); - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + if (widget.options.gameId != null) AnalysisTab.summary, + ]; - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: _Title(options: options, title: title), - actions: [ - _EngineDepth(ctrlProvider), - BookmarkButton(id: options.id as GameId), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - body: _Body(pgn: pgn, options: options), - ); + _tabController = TabController(vsync: this, initialIndex: 1, length: tabs.length); } - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options, title: title), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _EngineDepth(ctrlProvider), - BookmarkButton(id: options.id as GameId), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - ), - child: _Body(pgn: pgn, options: options), - ); + @override + void dispose() { + _tabController.dispose(); + super.dispose(); } -} - -class _Title extends StatelessWidget { - const _Title({ - required this.options, - this.title, - }); - final AnalysisOptions options; - final String? title; @override Widget build(BuildContext context) { - return title != null - ? Text(title!) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (options.variant != Variant.standard) ...[ - Icon(options.variant.icon), - const SizedBox(width: 5.0), - ], - Text(context.l10n.analysis), - ], + final ctrlProvider = analysisControllerProvider(widget.options); + final asyncState = ref.watch(ctrlProvider); + final prefs = ref.watch(analysisPreferencesProvider); + + final appBarActions = [ + if (prefs.enableComputerAnalysis) + EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), + AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), + if (widget.options.gameId != null) BookmarkButton(id: widget.options.gameId!), + AppBarIconButton( + onPressed: () { + pushPlatformRoute( + context, + title: context.l10n.settingsSettings, + builder: (_) => AnalysisSettings(widget.options), ); + }, + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ]; + + switch (asyncState) { + case AsyncData(:final value): + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar(title: _Title(variant: value.variant), actions: appBarActions), + body: _Body( + options: widget.options, + controller: _tabController, + enableDrawingShapes: widget.enableDrawingShapes, + ), + ); + case AsyncError(:final error, :final stackTrace): + _logger.severe('Cannot load analysis: $error', stackTrace); + return FullScreenRetryRequest( + onRetry: () { + ref.invalidate(ctrlProvider); + }, + ); + case _: + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar( + title: const _Title(variant: Variant.standard), + actions: appBarActions, + ), + body: const Center(child: CircularProgressIndicator()), + ); + } } } -class _Body extends ConsumerWidget { - const _Body({required this.pgn, required this.options}); +class _Title extends StatelessWidget { + const _Title({required this.variant}); + final Variant variant; - final String pgn; - final AnalysisOptions options; + static const excludedIcons = [Variant.standard, Variant.fromPosition]; @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); - - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - - final showAnalysisSummary = ref.watch( - ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), - ); - - return Column( + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - return aspectRatio > 1 - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - _Board( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - _EngineLines( - ctrlProvider, - isLandscape: true, - ), - Expanded( - child: PlatformCard( - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: showAnalysisSummary - ? ServerAnalysisSummary(pgn, options) - : AnalysisTreeView( - pgn, - options, - Orientation.landscape, - ), - ), - ), - ], - ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: _Board( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - ) - else - _Board(pgn, options, boardSize, isTablet: isTablet), - if (showAnalysisSummary) - Expanded(child: ServerAnalysisSummary(pgn, options)) - else - Expanded( - child: AnalysisTreeView( - pgn, - options, - Orientation.portrait, - ), - ), - ], - ); - }, - ), + if (!excludedIcons.contains(variant)) ...[Icon(variant.icon), const SizedBox(width: 5.0)], + Flexible( + child: AutoSizeText( + context.l10n.analysis, + minFontSize: 14, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - _BottomBar(pgn: pgn, options: options), ], ); } } -class _Board extends ConsumerStatefulWidget { - const _Board( - this.pgn, - this.options, - this.boardSize, { - required this.isTablet, - }); +class _Body extends ConsumerWidget { + const _Body({required this.options, required this.controller, required this.enableDrawingShapes}); - final String pgn; + final TabController controller; final AnalysisOptions options; - final double boardSize; - final bool isTablet; - - @override - ConsumerState<_Board> createState() => _BoardState(); -} - -class _BoardState extends ConsumerState<_Board> { - ISet userShapes = ISet(); - - ISet _computeBestMoveShapes(IList moves) { - // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move - // (assume moves are ordered by their winning chances, so index==0 is the best move) - double scaleArrowAgainstBestMove(int index) { - const minScale = 0.15; - const maxScale = 1.0; - const winningDiffScaleFactor = 2.5; - - final bestMove = moves[0]; - final winningDiffComparedToBestMove = - bestMove.winningChances - moves[index].winningChances; - // Force minimum scale if the best move is significantly better than this move - if (winningDiffComparedToBestMove > 0.3) { - return minScale; - } - return clampDouble( - math.max( - minScale, - maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, - ), - 0, - 1, - ); - } - - return ISet( - moves.mapIndexed( - (i, m) { - final move = m.move; - // Same colors as in the Web UI with a slightly different opacity - // The best move has a different color than the other moves - final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); - switch (move) { - case NormalMove(from: _, to: _, promotion: final promRole): - return [ - cg.Arrow( - color: color, - orig: move.cg.from, - dest: move.cg.to, - scale: scaleArrowAgainstBestMove(i), - ), - if (promRole != null) - cg.PieceShape( - color: color, - orig: move.cg.to, - role: promRole.cg, - ), - ]; - case DropMove(role: final role, to: _): - return [ - cg.PieceShape( - color: color, - orig: move.cg.to, - role: role.cg, - ), - ]; - } - }, - ).expand((e) => e), - ); - } - - @override - Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final analysisState = ref.watch(ctrlProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); - - final currentNode = analysisState.currentNode; - final annotation = makeAnnotation(currentNode.nags); - - final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; - - final sanMove = currentNode.sanMove; - - final ISet bestMoveShapes = showBestMoveArrow && - analysisState.isEngineAvailable && - bestMoves != null - ? _computeBestMoveShapes(bestMoves) - : ISet(); - - return cg.Board( - size: widget.boardSize, - onMove: (move, {isDrop, isPremove}) => - ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), - data: cg.BoardData( - orientation: analysisState.pov.cg, - interactableSide: analysisState.position.isGameOver - ? cg.InteractableSide.none - : analysisState.position.turn == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: analysisState.position.fen, - isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, - lastMove: analysisState.lastMove?.cg, - sideToMove: analysisState.position.turn.cg, - validMoves: analysisState.validMoves, - shapes: userShapes.union(bestMoveShapes), - annotations: - showAnnotationsOnBoard && sanMove != null && annotation != null - ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.fromUci(altCastles[sanMove.move.uci]!)!.cg.to: - annotation, - }) - : IMap({sanMove.move.cg.to: annotation}) - : null, - ), - settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], - drawShape: cg.DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - ), - ), - ); - } - - void _onCompleteShape(cg.Shape shape) { - if (userShapes.any((element) => element == shape)) { - setState(() { - userShapes = userShapes.remove(shape); - }); - return; - } else { - setState(() { - userShapes = userShapes.add(shape); - }); - } - } - - void _onClearShapes() { - setState(() { - userShapes = ISet(); - }); - } -} - -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - _EngineLines(ctrlProvider, isLandscape: false), - ], - ) - : kEmptyWidget; - } -} - -class _EngineLines extends ConsumerWidget { - const _EngineLines(this.ctrlProvider, {required this.isLandscape}); - final AnalysisControllerProvider ctrlProvider; - final bool isLandscape; + final bool enableDrawingShapes; @override Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); - final engineEval = ref.watch(engineEvaluationProvider).eval; - final eval = engineEval ?? analysisState.currentNode.eval; + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; - final emptyLines = List.filled( - numEvalLines, - _Engineline.empty(ctrlProvider), - ); + final ctrlProvider = analysisControllerProvider(options); + final analysisState = ref.watch(ctrlProvider).requireValue; - final content = !analysisState.position.isGameOver - ? (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => _Engineline(ctrlProvider, eval.position, pv), - ) - .toList() - : emptyLines) - : emptyLines; - - if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - _Engineline.empty(ctrlProvider), - ); - content.addAll(padding); - } - - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), - ); - } -} - -class _Engineline extends ConsumerWidget { - const _Engineline( - this.ctrlProvider, - this.fromPosition, - this.pvData, - ); - - const _Engineline.empty(this.ctrlProvider) - : pvData = const PvData(moves: IListConst([])), - fromPosition = Chess.initial; - - final AnalysisControllerProvider ctrlProvider; - final Position fromPosition; - final PvData pvData; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (pvData.moves.isEmpty) { - return const SizedBox( - height: kEvalGaugeSize, - child: SizedBox.shrink(), - ); - } - - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - - final lineBuffer = StringBuffer(); - int ply = fromPosition.ply + 1; - pvData.sanMoves(fromPosition).forEachIndexed((i, s) { - lineBuffer.write( - ply.isOdd - ? '${(ply / 2).ceil()}. $s ' - : i == 0 - ? '${(ply / 2).ceil()}... $s ' - : '$s ', - ); - ply += 1; - }); - - final brightness = ref.watch(currentBrightnessProvider); + final isEngineAvailable = analysisState.isEngineAvailable; + final hasEval = analysisState.hasAvailableEval; + final currentNode = analysisState.currentNode; - final evalString = pvData.evalString; - return AdaptiveInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(pvData.moves[0])!), - child: SizedBox( - height: kEvalGaugeSize, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: pvData.winningSide == Side.black - ? EngineGauge.backgroundColor(context, brightness) - : EngineGauge.valueColor(context, brightness), - borderRadius: BorderRadius.circular(4.0), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), - child: Text( - evalString, - style: TextStyle( - color: pvData.winningSide == Side.black - ? Colors.white - : Colors.black, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Text( - lineBuffer.toString(), - maxLines: 1, - softWrap: false, - style: TextStyle( - fontFamily: pieceNotation == PieceNotation.symbol - ? 'ChessFont' - : null, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], + return AnalysisLayout( + tabController: controller, + boardBuilder: + (context, boardSize, borderRadius) => AnalysisBoard( + options, + boardSize, + borderRadius: borderRadius, + enableDrawingShapes: enableDrawingShapes, ), + engineGaugeBuilder: + hasEval && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } + : null, + engineLines: + isEngineAvailable && numEvalLines > 0 + ? EngineLines( + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ) + : null, + bottomBar: _BottomBar(options: options), + children: [ + OpeningExplorerView( + position: currentNode.position, + opening: + kOpeningAllowedVariants.contains(analysisState.variant) + ? analysisState.currentNode.isRoot + ? LightOpening(eco: '', name: context.l10n.startPosition) + : analysisState.currentNode.opening ?? analysisState.currentBranchOpening + : null, + onMoveSelected: (move) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, ), - ), + AnalysisTreeView(options), + if (options.gameId != null) ServerAnalysisSummary(options), + ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); + const _BottomBar({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); - final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final canShowGameSummary = - ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); + final ctrlProvider = analysisControllerProvider(options); + final analysisState = ref.watch(ctrlProvider).requireValue; - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (canShowGameSummary) - Expanded( - child: BottomBarButton( - label: displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - ref.read(ctrlProvider.notifier).toggleDisplayMode(); - }, - icon: displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ), - ], + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, ), ), - ), + RepeatButton( + onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], ); } void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { return showAdaptiveActionSheet( context: context, actions: [ BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), + makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; + final boardFen = analysisState.position.fen; + pushPlatformRoute( + context, + title: context.l10n.boardEditor, + builder: (_) => BoardEditorScreen(initialFen: boardFen), + ); }, ), BottomSheetAction( @@ -854,53 +300,41 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + builder: (_) => AnalysisShareScreen(options: options), ); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - launchShareDialog( - context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, - ); + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; + launchShareDialog(context, text: analysisState.position.fen); }, ), - if (options.gameAnyId != null) + if (options.gameId != null) BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), + makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); + final gameId = options.gameId!; + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - state.position.fen, - state.lastMove, - ); + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + analysisState.pov, + analysisState.position.fen, + analysisState.lastMove, + ); if (context.mounted) { launchShareDialog( context, files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), + subject: context.l10n.puzzleFromGameLink(lichessUri('/$gameId').toString()), ); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, @@ -909,601 +343,3 @@ class _BottomBar extends ConsumerWidget { ); } } - -class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withOpacity(0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withOpacity(0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 725619a68a..ca1f922a36 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -1,154 +1,153 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AnalysisSettings extends ConsumerWidget { - const AnalysisSettings(this.pgn, this.options); + const AnalysisSettings(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final isLocalEvaluationAllowed = - ref.watch(ctrlProvider.select((s) => s.isLocalEvaluationAllowed)); - final isEngineAvailable = ref.watch( - ctrlProvider.select((s) => s.isEngineAvailable), - ); + final ctrlProvider = analysisControllerProvider(options); final prefs = ref.watch(analysisPreferencesProvider); + final asyncState = ref.watch(ctrlProvider); final isSoundEnabled = ref.watch( generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); - return DraggableScrollableSheet( - initialChildSize: 1.0, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEvalLines.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), + switch (asyncState) { + case AsyncData(:final value): + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), + body: ListView( + children: [ + if (value.isComputerAnalysisAllowed) + ListSection( + header: SettingsSectionTitle(context.l10n.computerAnalysis), children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + SwitchSettingTile( + title: Text(context.l10n.enable), + value: prefs.enableComputerAnalysis, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); + }, + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: + value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: ListSection( + margin: EdgeInsets.zero, + cupertinoBorderRadius: BorderRadius.zero, + cupertinoClipBehavior: Clip.none, + children: [ + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), + ), + ], ), - text: prefs.numEngineCores.toString(), ), ], ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: + value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: StockfishSettingsWidget( + onToggleLocalEvaluation: () { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + onSetEngineSearchTime: (value) { + ref.read(ctrlProvider.notifier).setEngineSearchTime(value); + }, + onSetEngineCores: (value) { + ref.read(ctrlProvider.notifier).setEngineCores(value); + }, + onSetNumEvalLines: (value) { + ref.read(ctrlProvider.notifier).setNumEvalLines(value); + }, + ), ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) - : null, + ListSection( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), + ], ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); - }, - ), - ], - ), - ); + ); + case AsyncError(:final error): + debugPrint('Error loading analysis: $error'); + return const SizedBox.shrink(); + case _: + return const CenterLoadingIndicator(); + } } } diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index caf1b2ede8..24ed1b3170 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -10,58 +12,102 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +final _dateFormatter = DateFormat('yyyy.MM.dd'); class AnalysisShareScreen extends StatelessWidget { - const AnalysisShareScreen({required this.pgn, required this.options}); + const AnalysisShareScreen({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.studyShareAndExport)), + body: _EditPgnTagsForm(options), ); } +} - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.studyShareAndExport), - ), - body: _EditPgnTagsForm(pgn, options), - ); - } +const Set _ratingHeaders = {'WhiteElo', 'BlackElo', 'WhiteRatingDiff', 'BlackRatingDiff'}; - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _EditPgnTagsForm(pgn, options), - ); - } +class _EditPgnTagsForm extends ConsumerStatefulWidget { + const _EditPgnTagsForm(this.options); + + final AnalysisOptions options; + + @override + _EditPgnTagsFormState createState() => _EditPgnTagsFormState(); } -const Set _ratingHeaders = { - 'WhiteElo', - 'BlackElo', - 'WhiteRatingDiff', - 'BlackRatingDiff', -}; +class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { + final Map _controllers = {}; + final Map _focusNodes = {}; -class _EditPgnTagsForm extends ConsumerWidget { - const _EditPgnTagsForm(this.pgn, this.options); + @override + void initState() { + super.initState(); + final ctrlProvider = analysisControllerProvider(widget.options); + final pgnHeaders = ref.read(ctrlProvider).requireValue.pgnHeaders; - final String pgn; - final AnalysisOptions options; + for (final entry in pgnHeaders.entries) { + _controllers[entry.key] = TextEditingController(text: entry.value); + _focusNodes[entry.key] = FocusNode(); + _focusNodes[entry.key]!.addListener(() { + if (!_focusNodes[entry.key]!.hasFocus) { + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, _controllers[entry.key]!.text); + } + }); + } + } + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + for (final focusNode in _focusNodes.values) { + focusNode.dispose(); + } + super.dispose(); + } @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.options); + final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.requireValue.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); + void focusAndSelectNextField(int index, IMap pgnHeaders) { + if (index + 1 < pgnHeaders.entries.length) { + final nextEntry = pgnHeaders.entries.elementAt(index + 1); + if (nextEntry.key == 'Date') { + _showDatePicker( + nextEntry, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index + 1, pgnHeaders); + }, + ); + } else if (nextEntry.key == 'Result') { + _showResultChoicePicker( + nextEntry, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index + 1, pgnHeaders); + }, + ); + } else { + _focusNodes[nextEntry.key]!.requestFocus(); + _controllers[nextEntry.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[nextEntry.key]!.text.length, + ); + } + } + } + return showRatingAsync.maybeWhen( data: (showRatings) { return SafeArea( @@ -70,77 +116,44 @@ class _EditPgnTagsForm extends ConsumerWidget { child: ListView( children: [ Column( - children: pgnHeaders.entries - .where( - (e) => showRatings || !_ratingHeaders.contains(e.key), - ) - .mapIndexed((index, e) { - return Padding( - padding: Styles.horizontalBodyPadding - .add(const EdgeInsets.only(bottom: 8.0)), - child: Row( - children: [ - SizedBox( - width: 110, - child: Text( - e.key, - style: const TextStyle( - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: AdaptiveTextField( - cupertinoDecoration: BoxDecoration( - color: CupertinoColors.tertiarySystemBackground, - border: Border.all( - color: CupertinoColors.systemGrey4, - width: 1, - ), - borderRadius: BorderRadius.circular(8), - ), - controller: TextEditingController.fromValue( - TextEditingValue( - text: e.value, - selection: TextSelection( - baseOffset: 0, - extentOffset: e.value.length, - ), - ), - ), - textInputAction: TextInputAction.next, - keyboardType: - e.key == 'WhiteElo' || e.key == 'BlackElo' - ? TextInputType.number - : TextInputType.text, + children: + pgnHeaders.entries + .where((e) => showRatings || !_ratingHeaders.contains(e.key)) + .mapIndexed((index, e) { + return _EditablePgnField( + entry: e, + controller: _controllers[e.key]!, + focusNode: _focusNodes[e.key]!, onTap: () { + _controllers[e.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[e.key]!.text.length, + ); if (e.key == 'Result') { - showChoicePicker( - context, - choices: ['1-0', '0-1', '1/2-1/2', '*'], - selectedItem: e.value, - labelBuilder: (choice) => Text(choice), - onSelectedItemChanged: (choice) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(e.key, choice); + _showResultChoicePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } else if (e.key == 'Date') { + _showDatePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); }, ); } }, onSubmitted: (value) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(e.key, value); + ref.read(ctrlProvider.notifier).updatePgnHeader(e.key, value); + focusAndSelectNextField(index, pgnHeaders); }, - ), - ), - ], - ), - ); - }).toList(), + ); + }) + .toList(), ), Padding( padding: const EdgeInsets.all(24.0), @@ -151,14 +164,10 @@ class _EditPgnTagsForm extends ConsumerWidget { onPressed: () { launchShareDialog( context, - text: ref - .read( - analysisControllerProvider( - pgn, - options, - ).notifier, - ) - .makeGamePgn(), + text: + ref + .read(analysisControllerProvider(widget.options).notifier) + .makeExportPgn(), ); }, child: Text(context.l10n.mobileShareGamePGN), @@ -174,4 +183,131 @@ class _EditPgnTagsForm extends ConsumerWidget { orElse: () => const SizedBox.shrink(), ); } + + Future _showDatePicker( + MapEntry entry, { + required BuildContext context, + required void Function() onEntryChanged, + }) { + final ctrlProvider = analysisControllerProvider(widget.options); + if (Theme.of(context).platform == TargetPlatform.iOS) { + return showCupertinoModalPopup( + context: context, + builder: + (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: + entry.value.isNotEmpty ? _dateFormatter.parse(entry.value) : DateTime.now(), + onDateTimeChanged: (DateTime newDateTime) { + final newDate = _dateFormatter.format(newDateTime); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, newDate); + _controllers[entry.key]!.text = newDate; + }, + ), + ), + ), + ).then((_) { + onEntryChanged(); + }); + } else { + return showDatePicker( + context: context, + initialDate: + entry.value.isNotEmpty ? _dateFormatter.parse(entry.value) : DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ) + .then((date) { + if (date != null) { + final formatted = _dateFormatter.format(date); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, formatted); + _controllers[entry.key]!.text = formatted; + } + }) + .then((_) { + onEntryChanged(); + }); + } + } + + Future _showResultChoicePicker( + MapEntry entry, { + required BuildContext context, + required void Function() onEntryChanged, + }) { + return showChoicePicker( + context, + choices: ['1-0', '0-1', '1/2-1/2', '*'], + selectedItem: entry.value, + labelBuilder: (choice) => Text(choice), + onSelectedItemChanged: (choice) { + ref + .read(analysisControllerProvider(widget.options).notifier) + .updatePgnHeader(entry.key, choice); + _controllers[entry.key]!.text = choice; + }, + ).then((_) { + onEntryChanged(); + }); + } +} + +class _EditablePgnField extends StatelessWidget { + const _EditablePgnField({ + required this.entry, + required this.controller, + required this.focusNode, + required this.onTap, + required this.onSubmitted, + }); + + final MapEntry entry; + final TextEditingController controller; + final FocusNode focusNode; + final GestureTapCallback onTap; + final ValueChanged onSubmitted; + + @override + Widget build(BuildContext context) { + return Padding( + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 8.0)), + child: Row( + children: [ + SizedBox( + width: 110, + child: Text( + entry.key, + style: const TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis), + ), + ), + const SizedBox(width: 8), + Expanded( + child: AdaptiveTextField( + focusNode: focusNode, + cupertinoDecoration: BoxDecoration( + color: CupertinoColors.tertiarySystemBackground, + border: Border.all(color: CupertinoColors.systemGrey4, width: 1), + borderRadius: BorderRadius.circular(8), + ), + controller: controller, + textInputAction: TextInputAction.next, + keyboardType: + entry.key == 'WhiteElo' || entry.key == 'BlackElo' + ? TextInputType.number + : TextInputType.text, + onTap: onTap, + onSubmitted: onSubmitted, + ), + ), + ], + ), + ); + } } diff --git a/lib/src/view/analysis/annotations.dart b/lib/src/view/analysis/annotations.dart deleted file mode 100644 index 79c7184eee..0000000000 --- a/lib/src/view/analysis/annotations.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:chessground/chessground.dart'; -import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; - -const innacuracyColor = LichessColors.cyan; -const mistakeColor = Color(0xFFe69f00); -const blunderColor = Color(0xFFdf5353); - -Color? nagColor(int nag) { - return switch (nag) { - 1 => Colors.lightGreen, - 2 => mistakeColor, - 3 => Colors.teal, - 4 => blunderColor, - 5 => LichessColors.purple, - 6 => LichessColors.cyan, - int() => null, - }; -} - -String moveAnnotationChar(Iterable nags) { - return nags - .map( - (nag) => switch (nag) { - 1 => '!', - 2 => '?', - 3 => '!!', - 4 => '??', - 5 => '!?', - 6 => '?!', - int() => '', - }, - ) - .join(''); -} - -Annotation? makeAnnotation(Iterable? nags) { - final nag = nags?.firstOrNull; - if (nag == null) { - return null; - } - return switch (nag) { - 1 => const Annotation( - symbol: '!', - color: Colors.lightGreen, - ), - 3 => const Annotation( - symbol: '!!', - color: Colors.teal, - ), - 5 => const Annotation( - symbol: '!?', - color: Colors.purple, - ), - 6 => const Annotation( - symbol: '?!', - color: LichessColors.cyan, - ), - 2 => const Annotation( - symbol: '?', - color: mistakeColor, - ), - 4 => const Annotation( - symbol: '??', - color: blunderColor, - ), - int() => null, - }; -} diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart new file mode 100644 index 0000000000..729ee8aa8f --- /dev/null +++ b/lib/src/view/analysis/server_analysis.dart @@ -0,0 +1,427 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.options); + + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final ctrlProvider = analysisControllerProvider(options); + final playersAnalysis = ref.watch( + ctrlProvider.select((value) => value.requireValue.playersAnalysis), + ); + final canShowGameSummary = ref.watch( + ctrlProvider.select((value) => value.requireValue.canShowGameSummary), + ); + final pgnHeaders = ref.watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + if (analysisPrefs.enableComputerAnalysis == false || !canShowGameSummary) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + Text(context.l10n.computerAnalysisDisabled), + if (canShowGameSummary) + SecondaryButton( + onPressed: () { + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); + }, + semanticsLabel: context.l10n.enable, + child: Text(context.l10n.enable), + ), + const Spacer(), + ], + ), + ), + ); + } + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameId) + const Padding(padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis()), + AcplChart(options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey)), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber('${playersAnalysis.white.accuracy}%'), + Center( + heightFactor: 1.8, + child: Text(context.l10n.accuracy, softWrap: true), + ), + _SummaryNumber('${playersAnalysis.black.accuracy}%'), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n.nbInaccuracies(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.inaccuracies.toString(), + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n.nbMistakes(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.mistakes.toString(), + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n.nbBlunders(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.blunders.toString(), + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center(heightFactor: 1.2, child: Text(item.$2, softWrap: true)), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber(playersAnalysis.white.acpl.toString()), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber(playersAnalysis.black.acpl.toString()), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: context.l10n.requestAComputerAnalysis, + onPressed: + ref.watch(authSessionProvider) == null + ? () { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text(context.l10n.requestAComputerAnalysis), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset('assets/images/stockfish/icon.png', width: 30, height: 30), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center(child: Text(data, softWrap: true)); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = + side == Side.white ? pgnHeaders.get('WhiteTitle') : pgnHeaders.get('BlackTitle'); + final playerName = + side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.options); + + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context).textTheme.labelMedium?.color?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final state = ref.watch(analysisControllerProvider(options)).requireValue; + final data = state.acplChartData; + final rootPly = state.root.position.ply; + final currentNode = state.currentNode; + final isOnMainline = state.isOnMainline; + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed((i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white))) + .toList(growable: false); + + final divisionLines = []; + + if (state.division?.middlegame != null) { + if (state.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar(state.division!.middlegame! - 1, context.l10n.middlegame), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (state.division?.endgame != null) { + if (state.division!.endgame! > 0) { + divisionLines.add(phaseVerticalBar(state.division!.endgame! - 1, context.l10n.endgame)); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.endgame)); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = + context.size!.width - 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => + (a.x - touchXDataValue).abs() < (b.x - touchXDataValue).abs() ? a : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withValues(alpha: 0.7), + aboveBarData: BarAreaData(show: true, color: aboveLineColor, applyCutOffY: true), + belowBarData: BarAreaData(show: true, color: belowLineColor, applyCutOffY: true), + dotData: const FlDotData(show: false), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/analysis/stockfish_settings.dart b/lib/src/view/analysis/stockfish_settings.dart new file mode 100644 index 0000000000..7448e430a6 --- /dev/null +++ b/lib/src/view/analysis/stockfish_settings.dart @@ -0,0 +1,103 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class StockfishSettingsWidget extends ConsumerWidget { + const StockfishSettingsWidget({ + required this.onToggleLocalEvaluation, + required this.onSetEngineSearchTime, + required this.onSetNumEvalLines, + required this.onSetEngineCores, + super.key, + }); + + final VoidCallback onToggleLocalEvaluation; + final void Function(Duration) onSetEngineSearchTime; + final void Function(int) onSetNumEvalLines; + final void Function(int) onSetEngineCores; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(analysisPreferencesProvider); + + return ListSection( + header: const SettingsSectionTitle('Stockfish 16'), + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + onToggleLocalEvaluation(); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: 'Search time: ', + style: const TextStyle(fontWeight: FontWeight.normal), + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: + prefs.engineSearchTime.inSeconds == 3600 + ? '∞' + : '${prefs.engineSearchTime.inSeconds}s', + ), + ], + ), + ), + subtitle: NonLinearSlider( + labelBuilder: (value) => value == 3600 ? '∞' : '${value}s', + value: prefs.engineSearchTime.inSeconds, + values: kAvailableEngineSearchTimes.map((e) => e.inSeconds).toList(), + onChangeEnd: (value) => onSetEngineSearchTime(Duration(seconds: value.toInt())), + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.multipleLines}: ', + style: const TextStyle(fontWeight: FontWeight.normal), + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: prefs.numEvalLines.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: (value) => onSetNumEvalLines(value.toInt()), + ), + ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle(fontWeight: FontWeight.normal), + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: prefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: (value) => onSetEngineCores(value.toInt()), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 8ee1655c7b..3ed473a38a 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -1,768 +1,40 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/node.dart'; -import 'package:lichess_mobile/src/model/common/uci.dart'; -import 'package:lichess_mobile/src/utils/duration.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; -import 'annotations.dart'; - -// fast replay debounce delay, same as piece animation duration, to avoid piece -// animation jank at the end of the replay -const kFastReplayDebounceDelay = Duration(milliseconds: 150); const kOpeningHeaderHeight = 32.0; -const kInlineMoveSpacing = 3.0; -class AnalysisTreeView extends ConsumerStatefulWidget { - const AnalysisTreeView( - this.pgn, - this.options, - this.displayMode, - ); +class AnalysisTreeView extends ConsumerWidget { + const AnalysisTreeView(this.options); - final String pgn; final AnalysisOptions options; - final Orientation displayMode; - - @override - ConsumerState createState() => _InlineTreeViewState(); -} - -class _InlineTreeViewState extends ConsumerState { - final currentMoveKey = GlobalKey(); - final _debounce = Debouncer(kFastReplayDebounceDelay); - late UciPath currentPath; - - @override - void initState() { - super.initState(); - currentPath = ref.read( - analysisControllerProvider(widget.pgn, widget.options).select( - (value) => value.currentPath, - ), - ); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, - ); - } - }); - } - - @override - void dispose() { - _debounce.dispose(); - super.dispose(); - } - - // This is the most expensive part of the analysis view because of the tree - // that may be very large. - // Great care must be taken to avoid unnecessary rebuilds. - // This should actually rebuild only when the current path changes or a new node - // is added. - // Debouncing the current path change is necessary to avoid rebuilding when - // using the fast replay buttons. - @override - Widget build(BuildContext context) { - ref.listen( - analysisControllerProvider(widget.pgn, widget.options), - (prev, state) { - if (prev?.currentPath != state.currentPath) { - // debouncing the current path change to avoid rebuilding when using - // the fast replay buttons - _debounce(() { - setState(() { - currentPath = state.currentPath; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, - ); - } - }); - }); - } - }, - ); - - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final root = ref.watch(ctrlProvider.select((value) => value.root)); - final rootComments = ref.watch( - ctrlProvider.select((value) => value.pgnRootComments), - ); - - final shouldShowComments = ref.watch( - analysisPreferencesProvider.select((value) => value.showPgnComments), - ); - - final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final List moveWidgets = _buildTreeWidget( - widget.pgn, - widget.options, - parent: root, - nodes: root.children, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: true, - startMainline: true, - startSideline: false, - initialPath: UciPath.empty, - ); - - // trick to make auto-scroll work when returning to the root position - moveWidgets.insert( - 0, - currentPath.isEmpty - ? SizedBox.shrink(key: currentMoveKey) - : const SizedBox.shrink(), - ); - - if (shouldShowComments && - rootComments?.any((c) => c.text?.isNotEmpty == true) == true) { - moveWidgets.insert( - 0, - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: _Comments(rootComments!), - ), - ); - } - - return CustomScrollView( - slivers: [ - if (kOpeningAllowedVariants.contains(widget.options.variant)) - SliverPersistentHeader( - delegate: _OpeningHeaderDelegate( - ctrlProvider, - displayMode: widget.displayMode, - ), - ), - SliverFillRemaining( - hasScrollBody: false, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: Wrap( - spacing: kInlineMoveSpacing, - children: moveWidgets, - ), - ), - ), - ], - ); - } - - List _buildTreeWidget( - String pgn, - AnalysisOptions options, { - required ViewNode parent, - required IList nodes, - required bool inMainline, - required bool startMainline, - required bool startSideline, - required bool shouldShowAnnotations, - required bool shouldShowComments, - required UciPath initialPath, - }) { - if (nodes.isEmpty) return []; - final List widgets = []; - - final firstChild = nodes.first; - final newPath = initialPath + firstChild.id; - final currentMove = newPath == currentPath; - - // add the first child - widgets.add( - InlineMove( - pgn, - options, - path: newPath, - parent: parent, - branch: firstChild, - isCurrentMove: currentMove, - key: currentMove ? currentMoveKey : null, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - isSideline: !inMainline, - startMainline: startMainline, - startSideline: startSideline, - endSideline: !inMainline && firstChild.children.isEmpty, - ), - ); - - // add the sidelines if present - for (var i = 1; i < nodes.length; i++) { - final node = nodes[i]; - if (node.isHidden) continue; - // start new sideline from mainline on a new line - if (inMainline) { - widgets.add( - SizedBox( - width: double.infinity, - child: Wrap( - spacing: kInlineMoveSpacing, - children: _buildTreeWidget( - pgn, - options, - parent: parent, - nodes: [nodes[i]].lockUnsafe, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: false, - startMainline: false, - startSideline: true, - initialPath: initialPath, - ), - ), - ), - ); - } else { - widgets.addAll( - _buildTreeWidget( - pgn, - options, - parent: parent, - nodes: [nodes[i]].lockUnsafe, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: false, - startMainline: false, - startSideline: true, - initialPath: initialPath, - ), - ); - } - } - - // add the children of the first child - widgets.addAll( - _buildTreeWidget( - pgn, - options, - parent: firstChild, - nodes: firstChild.children, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: inMainline, - startMainline: false, - startSideline: false, - initialPath: newPath, - ), - ); - - return widgets; - } -} - -Color? _textColor( - BuildContext context, - double opacity, { - bool isLichessGameAnalysis = true, - int? nag, -}) { - final defaultColor = Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(opacity) - : CupertinoTheme.of(context) - .textTheme - .textStyle - .color - ?.withOpacity(opacity); - - return nag != null && nag > 0 ? nagColor(nag) : defaultColor; -} - -class InlineMove extends ConsumerWidget { - const InlineMove( - this.pgn, - this.options, { - required this.path, - required this.parent, - required this.branch, - required this.shouldShowAnnotations, - required this.shouldShowComments, - required this.isCurrentMove, - required this.isSideline, - super.key, - this.startMainline = false, - this.startSideline = false, - this.endSideline = false, - }); - - final String pgn; - final AnalysisOptions options; - final UciPath path; - final ViewNode parent; - final ViewBranch branch; - final bool shouldShowAnnotations; - final bool shouldShowComments; - final bool isCurrentMove; - final bool isSideline; - final bool startMainline; - final bool startSideline; - final bool endSideline; - - static const borderRadius = BorderRadius.all(Radius.circular(4.0)); - static const baseTextStyle = TextStyle( - fontSize: 16.0, - height: 1.5, - ); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final move = branch.sanMove; - final ply = branch.position.ply; - - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - final fontFamily = - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; - - final textStyle = isSideline - ? TextStyle( - fontFamily: fontFamily, - color: _textColor(context, 0.6), - ) - : baseTextStyle.copyWith( - fontFamily: fontFamily, - color: _textColor(context, 0.9), - fontWeight: FontWeight.w600, - ); - - final indexTextStyle = baseTextStyle.copyWith( - color: _textColor(context, 0.6), - ); - - final indexWidget = ply.isOdd - ? Text( - '${(ply / 2).ceil()}.', - style: indexTextStyle, - ) - : ((startMainline || startSideline) - ? Text( - '${(ply / 2).ceil()}...', - style: indexTextStyle, - ) - : null); - - final moveWithNag = move.san + - (branch.nags != null && shouldShowAnnotations - ? moveAnnotationChar(branch.nags!) - : ''); - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (startSideline) - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Text('(', style: textStyle), - ), - if (shouldShowComments && branch.hasStartingTextComment) - Flexible( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: - _Comments(branch.startingComments!, isSideline: isSideline), - ), - ), - if (indexWidget != null) indexWidget, - if (indexWidget != null) const SizedBox(width: 1), - AdaptiveInkWell( - borderRadius: borderRadius, - onTap: () => ref.read(ctrlProvider.notifier).userJump(path), - onLongPress: () { - showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (context) => _MoveContextMenu( - pgn, - options, - title: ply.isOdd - ? '${(ply / 2).ceil()}. $moveWithNag' - : '${(ply / 2).ceil()}... $moveWithNag', - path: path, - parent: parent, - branch: branch, - isSideline: isSideline, - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), - decoration: isCurrentMove - ? BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey3.resolveFrom(context) - : Theme.of(context).focusColor, - shape: BoxShape.rectangle, - borderRadius: borderRadius, - ) - : null, - child: Text( - moveWithNag, - style: isCurrentMove - ? textStyle.copyWith( - fontWeight: FontWeight.bold, - color: _textColor( - context, - 1, - isLichessGameAnalysis: options.isLichessGameAnalysis, - nag: shouldShowAnnotations - ? branch.nags?.firstOrNull - : null, - ), - ) - : textStyle.copyWith( - color: _textColor( - context, - 0.9, - isLichessGameAnalysis: options.isLichessGameAnalysis, - nag: shouldShowAnnotations - ? branch.nags?.firstOrNull - : null, - ), - ), - ), - ), - ), - if (shouldShowComments && branch.hasTextComment) - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: _Comments(branch.comments!, isSideline: isSideline), - ), - ), - if (endSideline) Text(')', style: textStyle), - ], - ); - } -} - -class _MoveContextMenu extends ConsumerWidget { - const _MoveContextMenu( - this.pgn, - this.options, { - required this.title, - required this.path, - required this.parent, - required this.branch, - required this.isSideline, - }); - - final String title; - final String pgn; - final AnalysisOptions options; - final UciPath path; - final ViewNode parent; - final ViewBranch branch; - final bool isSideline; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - - return DraggableScrollableSheet( - initialChildSize: .3, - expand: false, - snap: true, - snapSizes: const [.3, .7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - if (branch.clock != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.punch_clock, - ), - const SizedBox(width: 4.0), - Text( - branch.clock!.toHoursMinutesSeconds( - showTenths: branch.clock! < - const Duration(minutes: 1), - ), - ), - ], - ), - if (branch.elapsedMoveTime != null) ...[ - const SizedBox(height: 4.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.hourglass_bottom, - ), - const SizedBox(width: 4.0), - Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), - ), - ], - ), - ], - ], - ), - ], - ), - ), - if (branch.hasLichessAnalysisTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.lichessAnalysisComments! - .map((c) => c.text ?? '') - .join(' '), - ), - ), - if (branch.hasTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.comments!.map((c) => c.text ?? '').join(' '), - ), - ), - const PlatformDivider(indent: 0), - if (parent.children.any((c) => c.isHidden)) - BottomSheetContextMenuAction( - icon: Icons.subtitles, - child: Text(context.l10n.mobileShowVariations), - onPressed: () { - ref.read(ctrlProvider.notifier).showAllVariations(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.subtitles_off, - child: Text(context.l10n.mobileHideVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).hideVariation(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.expand_less, - child: Text(context.l10n.promoteVariation), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .promoteVariation(path, false); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.check, - child: Text(context.l10n.makeMainLine), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .promoteVariation(path, true); - }, - ), - BottomSheetContextMenuAction( - icon: Icons.delete, - child: Text(context.l10n.deleteFromHere), - onPressed: () { - ref.read(ctrlProvider.notifier).deleteFromHere(path); - }, - ), - ], - ), - ), + final ctrlProvider = analysisControllerProvider(options); + + final root = ref.watch(ctrlProvider.select((value) => value.requireValue.root)); + final currentPath = ref.watch(ctrlProvider.select((value) => value.requireValue.currentPath)); + final pgnRootComments = ref.watch( + ctrlProvider.select((value) => value.requireValue.pgnRootComments), + ); + final prefs = ref.watch(analysisPreferencesProvider); + // enable computer analysis takes effect here only if it's a lichess game + final enableComputerAnalysis = !options.isLichessGameAnalysis || prefs.enableComputerAnalysis; + + return SingleChildScrollView( + padding: EdgeInsets.zero, + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + pgnRootComments: pgnRootComments, + notifier: ref.read(ctrlProvider.notifier), + shouldShowComputerVariations: enableComputerAnalysis, + shouldShowComments: enableComputerAnalysis && prefs.showPgnComments, + shouldShowAnnotations: enableComputerAnalysis && prefs.showAnnotations, ), ); } } - -class _Comments extends StatelessWidget { - _Comments(this.comments, {this.isSideline = false}) - : assert(comments.any((c) => c.text?.isNotEmpty == true)); - - final Iterable comments; - final bool isSideline; - - @override - Widget build(BuildContext context) { - return AdaptiveInkWell( - onTap: () { - showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.3, - expand: false, - snap: true, - snapSizes: const [.3, .7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: comments.map( - (comment) { - if (comment.text == null || comment.text!.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text(comment.text!.replaceAll('\n', ' ')), - ); - }, - ).toList(), - ), - ), - ), - ), - ); - }, - child: Text( - comments.map((c) => c.text ?? '').join(' ').replaceAll('\n', ' '), - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontStyle: FontStyle.italic, - color: - isSideline ? _textColor(context, 0.6) : _textColor(context, 0.7), - ), - ), - ); - } -} - -class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { - const _OpeningHeaderDelegate( - this.ctrlProvider, { - required this.displayMode, - }); - - final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return _Opening(ctrlProvider, displayMode); - } - - @override - double get minExtent => kOpeningHeaderHeight; - - @override - double get maxExtent => kOpeningHeaderHeight; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; -} - -class _Opening extends ConsumerWidget { - const _Opening(this.ctrlProvider, this.displayMode); - - final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRootNode = ref.watch( - ctrlProvider.select((s) => s.currentNode.isRoot), - ); - final nodeOpening = - ref.watch(ctrlProvider.select((s) => s.currentNode.opening)); - final branchOpening = - ref.watch(ctrlProvider.select((s) => s.currentBranchOpening)); - final contextOpening = - ref.watch(ctrlProvider.select((s) => s.contextOpening)); - final opening = isRootNode - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : nodeOpening ?? branchOpening ?? contextOpening; - - return opening != null - ? Container( - height: kOpeningHeaderHeight, - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: displayMode == Orientation.landscape - ? const BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ) - : null, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Center( - child: Text( - opening.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart new file mode 100644 index 0000000000..a8e60ec4cc --- /dev/null +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -0,0 +1,95 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; + +class BoardEditorMenu extends ConsumerWidget { + const BoardEditorMenu({required this.initialFen, super.key}); + + final String? initialFen; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorController = boardEditorControllerProvider(initialFen); + final editorState = ref.watch(editorController); + + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: + Side.values.map((side) { + return ChoiceChip( + label: Text( + side == Side.white ? context.l10n.whitePlays : context.l10n.blackPlays, + ), + selected: editorState.sideToPlay == side, + onSelected: (selected) { + if (selected) { + ref.read(editorController.notifier).setSideToPlay(side); + } + }, + ); + }).toList(), + ), + ), + Padding( + padding: Styles.bodySectionPadding, + child: Text(context.l10n.castling, style: Styles.title), + ), + ...Side.values.map((side) { + return Padding( + padding: Styles.horizontalBodyPadding, + child: Row( + spacing: 8.0, + children: [ + SizedBox( + width: 100.0, + child: Text( + side == Side.white ? context.l10n.white : context.l10n.black, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ...[CastlingSide.king, CastlingSide.queen].map((castlingSide) { + return ChoiceChip( + label: Text(castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O'), + selected: editorState.isCastlingAllowed(side, castlingSide), + onSelected: (selected) { + ref.read(editorController.notifier).setCastling(side, castlingSide, selected); + }, + ); + }), + ], + ), + ); + }), + if (editorState.enPassantOptions.isNotEmpty) ...[ + const Padding( + padding: Styles.bodySectionPadding, + child: Text('En passant', style: Styles.subtitle), + ), + Wrap( + spacing: 8.0, + children: + editorState.enPassantOptions.squares.map((square) { + return ChoiceChip( + label: Text(square.name), + selected: editorState.enPassantSquare == square, + onSelected: (selected) { + ref.read(editorController.notifier).toggleEnPassantSquare(square); + }, + ); + }).toList(), + ), + ], + ], + ); + } +} diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart new file mode 100644 index 0000000000..2a06aae43c --- /dev/null +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -0,0 +1,339 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_menu.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class BoardEditorScreen extends StatelessWidget { + const BoardEditorScreen({super.key, this.initialFen}); + + final String? initialFen; + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.boardEditor)), + body: _Body(initialFen), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body(this.initialFen); + + final String? initialFen; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final boardEditorState = ref.watch(boardEditorControllerProvider(initialFen)); + + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final direction = aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + + return Flex( + direction: direction, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _PieceMenu( + boardSize, + initialFen: initialFen, + direction: flipAxis(direction), + side: boardEditorState.orientation.opposite, + isTablet: isTablet, + ), + _BoardEditor( + boardSize, + initialFen: initialFen, + orientation: boardEditorState.orientation, + isTablet: isTablet, + // unlockView is safe because chessground will never modify the pieces + pieces: boardEditorState.pieces.unlockView, + ), + _PieceMenu( + boardSize, + initialFen: initialFen, + direction: flipAxis(direction), + side: boardEditorState.orientation, + isTablet: isTablet, + ), + ], + ); + }, + ), + ), + ), + _BottomBar(initialFen), + ], + ); + } +} + +class _BoardEditor extends ConsumerWidget { + const _BoardEditor( + this.boardSize, { + required this.initialFen, + required this.isTablet, + required this.orientation, + required this.pieces, + }); + + final String? initialFen; + final double boardSize; + final bool isTablet; + final Side orientation; + final Pieces pieces; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(boardEditorControllerProvider(initialFen)); + final boardPrefs = ref.watch(boardPreferencesProvider); + + return ChessboardEditor( + size: boardSize, + pieces: pieces, + orientation: orientation, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + ), + pointerMode: editorState.editorPointerMode, + onDiscardedPiece: + (Square square) => + ref.read(boardEditorControllerProvider(initialFen).notifier).discardPiece(square), + onDroppedPiece: + (Square? origin, Square dest, Piece piece) => ref + .read(boardEditorControllerProvider(initialFen).notifier) + .movePiece(origin, dest, piece), + onEditedSquare: + (Square square) => + ref.read(boardEditorControllerProvider(initialFen).notifier).editSquare(square), + ); + } +} + +class _PieceMenu extends ConsumerStatefulWidget { + const _PieceMenu( + this.boardSize, { + required this.initialFen, + required this.direction, + required this.side, + required this.isTablet, + }); + + final String? initialFen; + + final double boardSize; + + final Axis direction; + + final Side side; + + final bool isTablet; + + @override + ConsumerState<_PieceMenu> createState() => _PieceMenuState(); +} + +class _PieceMenuState extends ConsumerState<_PieceMenu> { + @override + Widget build(BuildContext context) { + final boardPrefs = ref.watch(boardPreferencesProvider); + final editorController = boardEditorControllerProvider(widget.initialFen); + final editorState = ref.watch(editorController); + + final squareSize = widget.boardSize / 8; + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: + widget.isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + ), + child: ColoredBox( + color: Theme.of(context).disabledColor, + child: Flex( + direction: widget.direction, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: squareSize, + height: squareSize, + child: ColoredBox( + key: Key('drag-button-${widget.side.name}'), + color: + editorState.editorPointerMode == EditorPointerMode.drag + ? context.lichessColors.good + : Colors.transparent, + child: GestureDetector( + onTap: + () => ref.read(editorController.notifier).updateMode(EditorPointerMode.drag), + child: Icon(CupertinoIcons.hand_draw, size: 0.9 * squareSize), + ), + ), + ), + ...Role.values.map((role) { + final piece = Piece(role: role, color: widget.side); + final pieceWidget = PieceWidget( + piece: piece, + size: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, + ); + + return ColoredBox( + key: Key('piece-button-${piece.color.name}-${piece.role.name}'), + color: + ref.read(boardEditorControllerProvider(widget.initialFen)).activePieceOnEdit == + piece + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + child: GestureDetector( + child: Draggable( + data: Piece(role: role, color: widget.side), + feedback: PieceDragFeedback( + piece: piece, + squareSize: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, + ), + child: pieceWidget, + onDragEnd: + (_) => + ref.read(editorController.notifier).updateMode(EditorPointerMode.drag), + ), + onTap: + () => ref + .read(editorController.notifier) + .updateMode(EditorPointerMode.edit, piece), + ), + ); + }), + SizedBox( + key: Key('delete-button-${widget.side.name}'), + width: squareSize, + height: squareSize, + child: ColoredBox( + color: + editorState.deletePiecesActive + ? context.lichessColors.error + : Colors.transparent, + child: GestureDetector( + onTap: + () => { + ref + .read(editorController.notifier) + .updateMode(EditorPointerMode.edit, null), + }, + child: Icon(CupertinoIcons.delete, size: 0.8 * squareSize), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar(this.initialFen); + + final String? initialFen; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(boardEditorControllerProvider(initialFen)); + final pieceCount = editorState.pieces.length; + + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: + () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => BoardEditorMenu(initialFen: initialFen), + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + ), + icon: Icons.tune, + ), + BottomBarButton( + key: const Key('flip-button'), + label: context.l10n.flipBoard, + onTap: ref.read(boardEditorControllerProvider(initialFen).notifier).flipBoard, + icon: CupertinoIcons.arrow_2_squarepath, + ), + BottomBarButton( + label: context.l10n.analysis, + key: const Key('analysis-board-button'), + onTap: + editorState.pgn != null && + // 1 condition (of many) where stockfish segfaults + pieceCount > 0 && + pieceCount <= 32 + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: editorState.orientation, + standalone: ( + pgn: editorState.pgn!, + isComputerAnalysisAllowed: true, + variant: Variant.fromPosition, + ), + ), + ), + ); + } + : null, + icon: Icons.biotech, + ), + BottomBarButton( + label: context.l10n.mobileSharePositionAsFEN, + onTap: () => launchShareDialog(context, text: editorState.fen), + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart new file mode 100644 index 0000000000..cb55aa1e23 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -0,0 +1,277 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +// height of 1.0 is important because we need to determine the height of the text +// to calculate the height of the header and footer of the board +const _kPlayerWidgetTextStyle = TextStyle(fontSize: 13, height: 1.0); + +const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); + +/// A tab that displays the live games of a broadcast round. +class BroadcastBoardsTab extends ConsumerWidget { + const BroadcastBoardsTab({ + required this.tournamentId, + required this.roundId, + required this.tournamentSlug, + }); + + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final String tournamentSlug; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = + MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; + final round = ref.watch(broadcastRoundControllerProvider(roundId)); + + return SliverPadding( + padding: edgeInsets, + sliver: switch (round) { + AsyncData(:final value) => + value.games.isEmpty + ? SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverToBoxAdapter( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.info, size: 30), + Text(context.l10n.broadcastNoBoardsYet), + ], + ), + ), + ) + : BroadcastPreview( + games: value.games.values.toIList(), + tournamentId: tournamentId, + roundId: roundId, + title: value.round.name, + tournamentSlug: tournamentSlug, + roundSlug: value.round.slug, + ), + AsyncError(:final error) => SliverFillRemaining( + child: Center(child: Text('Could not load broadcast: $error')), + ), + _ => const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), + }, + ); + } +} + +class BroadcastPreview extends StatelessWidget { + const BroadcastPreview({ + required this.tournamentId, + required this.roundId, + required this.games, + required this.title, + required this.tournamentSlug, + required this.roundSlug, + }); + + const BroadcastPreview.loading() + : tournamentId = const BroadcastTournamentId(''), + roundId = const BroadcastRoundId(''), + games = null, + title = '', + tournamentSlug = '', + roundSlug = ''; + + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final IList? games; + final String title; + final String tournamentSlug; + final String roundSlug; + + @override + Widget build(BuildContext context) { + const numberLoadingBoards = 12; + const boardSpacing = 10.0; + // height of the text based on the font size + // since the TextStyle is defined with an height at 1.0, this is the real height + // see: https://api.flutter.dev/flutter/painting/TextStyle/height.html + final textHeight = _kPlayerWidgetTextStyle.fontSize!; + final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; + final numberOfBoardsByRow = isTabletOrLarger(context) ? 3 : 2; + final screenWidth = MediaQuery.sizeOf(context).width; + final boardWidth = + (screenWidth - + Styles.horizontalBodyPadding.horizontal - + (numberOfBoardsByRow - 1) * boardSpacing) / + numberOfBoardsByRow; + + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: numberOfBoardsByRow, + crossAxisSpacing: boardSpacing, + mainAxisSpacing: boardSpacing, + mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, + ), + delegate: SliverChildBuilderDelegate( + childCount: games == null ? numberLoadingBoards : games!.length, + (context, index) { + if (games == null) { + return ShimmerLoading( + isLoading: true, + child: BoardThumbnail.loading( + size: boardWidth, + header: _PlayerWidgetLoading(width: boardWidth), + footer: _PlayerWidgetLoading(width: boardWidth), + ), + ); + } + + final game = games![index]; + final playingSide = Setup.parseFen(game.fen).turn; + + return BoardThumbnail( + animationDuration: const Duration(milliseconds: 150), + onTap: () { + pushPlatformRoute( + context, + title: title, + builder: + (context) => BroadcastGameScreen( + tournamentId: tournamentId, + roundId: roundId, + gameId: game.id, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, + title: title, + ), + ); + }, + orientation: Side.white, + fen: game.fen, + lastMove: game.lastMove, + size: boardWidth, + header: _PlayerWidget( + width: boardWidth, + game: game, + side: Side.black, + playingSide: playingSide, + ), + footer: _PlayerWidget( + width: boardWidth, + game: game, + side: Side.white, + playingSide: playingSide, + ), + ); + }, + ), + ); + } +} + +class _PlayerWidgetLoading extends StatelessWidget { + const _PlayerWidgetLoading({required this.width}); + + final double width; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Padding( + padding: _kPlayerWidgetPadding, + child: Container( + height: _kPlayerWidgetTextStyle.fontSize, + decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(5)), + ), + ), + ); + } +} + +class _PlayerWidget extends StatelessWidget { + const _PlayerWidget({ + required this.width, + required this.game, + required this.side, + required this.playingSide, + }); + + final BroadcastGame game; + final Side side; + final Side playingSide; + final double width; + + @override + Widget build(BuildContext context) { + final player = game.players[side]!; + final gameStatus = game.status; + + return SizedBox( + width: width, + child: Padding( + padding: _kPlayerWidgetPadding, + child: DefaultTextStyle.merge( + style: _kPlayerWidgetTextStyle, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + ), + ), + const SizedBox(width: 5), + if (game.isOver) + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ) + else if (player.clock != null) + CountdownClockBuilder( + timeLeft: player.clock!, + active: side == playingSide, + builder: + (context, timeLeft) => Text( + timeLeft.toHoursMinutesSeconds(), + style: TextStyle( + color: (side == playingSide) ? Colors.orange[900] : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart new file mode 100644 index 0000000000..4283bfcc54 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -0,0 +1,137 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +class BroadcastGameBottomBar extends ConsumerWidget { + const BroadcastGameBottomBar({ + required this.roundId, + required this.gameId, + this.tournamentSlug, + this.roundSlug, + }); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String? tournamentSlug; + final String? roundSlug; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + final broadcastGameState = ref.watch(ctrlProvider).requireValue; + + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + showAdaptiveActionSheet( + context: context, + actions: [ + if (tournamentSlug != null && roundSlug != null) + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGameURL), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri('/broadcast/$tournamentSlug/$roundSlug/$roundId/$gameId'), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (context) async { + try { + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), + ); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => const Text('GIF'), + onPressed: (_) async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .chapterGif(roundId, gameId); + if (context.mounted) { + launchShareDialog(context, files: [gif]); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + }, + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, + ), + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () { + ref.read(ctrlProvider.notifier).toggleBoard(); + }, + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: broadcastGameState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: broadcastGameState.canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).userPrevious(); +} diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart new file mode 100644 index 0000000000..52cdf4d0db --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -0,0 +1,478 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen_providers.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class BroadcastGameScreen extends ConsumerStatefulWidget { + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String? tournamentSlug; + final String? roundSlug; + final String? title; + + const BroadcastGameScreen({ + required this.tournamentId, + required this.roundId, + required this.gameId, + this.tournamentSlug, + this.roundSlug, + this.title, + }); + + @override + ConsumerState createState() => _BroadcastGameScreenState(); +} + +class _BroadcastGameScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late final List tabs; + late final TabController _tabController; + + @override + void initState() { + super.initState(); + + tabs = [AnalysisTab.opening, AnalysisTab.moves]; + + _tabController = TabController(vsync: this, initialIndex: 1, length: tabs.length); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final broadcastRoundGameState = ref.watch( + broadcastRoundGameProvider(widget.roundId, widget.gameId), + ); + final broadcastGameState = ref.watch( + broadcastGameControllerProvider(widget.roundId, widget.gameId), + ); + final title = + (widget.title != null) + ? Text(widget.title!, overflow: TextOverflow.ellipsis, maxLines: 1) + : switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { + AsyncData(value: final title) => Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + _ => const SizedBox.shrink(), + }; + + return PlatformScaffold( + appBar: PlatformAppBar( + title: title, + actions: [ + AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), + AppBarIconButton( + onPressed: + (broadcastGameState.hasValue) + ? () { + pushPlatformRoute( + context, + screen: BroadcastGameSettings(widget.roundId, widget.gameId), + ); + } + : null, + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: switch ((broadcastRoundGameState, broadcastGameState)) { + (AsyncData(), AsyncData()) => _Body( + widget.tournamentId, + widget.roundId, + widget.gameId, + widget.tournamentSlug, + widget.roundSlug, + tabController: _tabController, + ), + (AsyncError(:final error), _) => Center(child: Text('Cannot load broadcast game: $error')), + (_, AsyncError(:final error)) => Center(child: Text('Cannot load broadcast game: $error')), + _ => const Center(child: CircularProgressIndicator.adaptive()), + }, + ); + } +} + +class _Body extends ConsumerWidget { + const _Body( + this.tournamentId, + this.roundId, + this.gameId, + this.tournamentSlug, + this.roundSlug, { + required this.tabController, + }); + + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String? tournamentSlug; + final String? roundSlug; + final TabController tabController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broadcastState = ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; + + final engineGaugeParams = broadcastState.engineGaugeParams; + final isLocalEvaluationEnabled = broadcastState.isLocalEvaluationEnabled; + final currentNode = broadcastState.currentNode; + + return AnalysisLayout( + tabController: tabController, + boardBuilder: + (context, boardSize, borderRadius) => + _BroadcastBoard(roundId, gameId, boardSize, borderRadius), + boardHeader: _PlayerWidget( + tournamentId: tournamentId, + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.top, + ), + boardFooter: _PlayerWidget( + tournamentId: tournamentId, + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.bottom, + ), + engineGaugeBuilder: + isLocalEvaluationEnabled && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: + isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).onUserMove, + ) + : null, + bottomBar: BroadcastGameBottomBar( + roundId: roundId, + gameId: gameId, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, + ), + children: [_OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId)], + ); + } +} + +class _OpeningExplorerTab extends ConsumerWidget { + const _OpeningExplorerTab(this.roundId, this.gameId); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + final state = ref.watch(ctrlProvider).requireValue; + + return OpeningExplorerView( + position: state.currentNode.position, + onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, + ); + } +} + +class _BroadcastBoard extends ConsumerStatefulWidget { + const _BroadcastBoard(this.roundId, this.gameId, this.boardSize, this.borderRadius); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final double boardSize; + final BorderRadiusGeometry? borderRadius; + + @override + ConsumerState<_BroadcastBoard> createState() => _BroadcastBoardState(); +} + +class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + final ctrlProvider = broadcastGameControllerProvider(widget.roundId, widget.gameId); + final broadcastAnalysisState = ref.watch(ctrlProvider).requireValue; + final boardPrefs = ref.watch(boardPreferencesProvider); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + + final evalBestMoves = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)); + + final currentNode = broadcastAnalysisState.currentNode; + final annotation = makeAnnotation(currentNode.nags); + + final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + + final sanMove = currentNode.sanMove; + + final ISet bestMoveShapes = + analysisPrefs.showBestMoveArrow && + broadcastAnalysisState.isLocalEvaluationEnabled && + bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + return Chessboard( + size: widget.boardSize, + fen: broadcastAnalysisState.position.fen, + lastMove: broadcastAnalysisState.lastMove as NormalMove?, + orientation: broadcastAnalysisState.pov, + game: GameData( + playerSide: + broadcastAnalysisState.position.isGameOver + ? PlayerSide.none + : broadcastAnalysisState.position.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPrefs.boardHighlights && broadcastAnalysisState.position.isCheck, + sideToMove: broadcastAnalysisState.position.turn, + validMoves: broadcastAnalysisState.validMoves, + promotionMove: broadcastAnalysisState.promotionMove, + onMove: (move, {isDrop, captured}) => ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), + ), + shapes: userShapes.union(bestMoveShapes), + annotations: + analysisPrefs.showAnnotations && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) + : IMap({sanMove.move.to: annotation}) + : null, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + ); + } + + void _onCompleteShape(Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } + + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); + } +} + +enum _PlayerWidgetPosition { bottom, top } + +class _PlayerWidget extends ConsumerWidget { + const _PlayerWidget({ + required this.tournamentId, + required this.roundId, + required this.gameId, + required this.widgetPosition, + }); + + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final _PlayerWidgetPosition widgetPosition; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broadcastGameState = + ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; + final game = ref.watch(broadcastRoundGameProvider(roundId, gameId)).requireValue; + + final isCursorOnLiveMove = + broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; + final sideToMove = broadcastGameState.position.turn; + final side = switch (widgetPosition) { + _PlayerWidgetPosition.bottom => broadcastGameState.pov, + _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, + }; + + final player = game.players[side]!; + final liveClock = isCursorOnLiveMove ? player.clock : null; + final gameStatus = game.status; + + final pastClocks = broadcastGameState.clocks; + final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastPlayerResultsScreen( + tournamentId, + (player.fideId != null) ? player.fideId!.toString() : player.name, + player.title, + player.name, + ), + ); + }, + child: Container( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + if (game.isOver) ...[ + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 16.0), + ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + ), + if (liveClock != null || pastClock != null) + Container( + height: kAnalysisBoardHeaderOrFooterHeight, + color: + (side == sideToMove) + ? isCursorOnLiveMove + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Center( + child: + liveClock != null + ? CountdownClockBuilder( + timeLeft: liveClock, + active: side == sideToMove, + builder: + (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ) + : _Clock( + timeLeft: pastClock!, + isSideToMove: side == sideToMove, + isLive: false, + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _Clock extends StatelessWidget { + const _Clock({required this.timeLeft, required this.isSideToMove, required this.isLive}); + + final Duration timeLeft; + final bool isSideToMove; + final bool isLive; + + @override + Widget build(BuildContext context) { + return Text( + timeLeft.toHoursMinutesSeconds(), + style: TextStyle( + color: + isSideToMove + ? isLive + ? Theme.of(context).colorScheme.onTertiaryContainer + : Theme.of(context).colorScheme.onSecondaryContainer + : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_game_screen_providers.dart b/lib/src/view/broadcast/broadcast_game_screen_providers.dart new file mode 100644 index 0000000000..288733f47f --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_screen_providers.g.dart'; + +@riverpod +Future broadcastRoundGame( + Ref ref, + BroadcastRoundId roundId, + BroadcastGameId gameId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId).selectAsync((round) => round.games[gameId]!), + ); +} + +@riverpod +Future broadcastGameScreenTitle(Ref ref, BroadcastRoundId roundId) { + return ref.watch( + broadcastRoundControllerProvider(roundId).selectAsync((round) => round.round.name), + ); +} diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart new file mode 100644 index 0000000000..10d27db672 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -0,0 +1,90 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class BroadcastGameSettings extends ConsumerWidget { + const BroadcastGameSettings(this.roundId, this.gameId); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broacdcastGameAnalysisController = broadcastGameControllerProvider(roundId, gameId); + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); + + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), + body: ListView( + children: [ + StockfishSettingsWidget( + onToggleLocalEvaluation: + () => ref.read(broacdcastGameAnalysisController.notifier).toggleLocalEvaluation(), + onSetEngineSearchTime: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setEngineSearchTime(value), + onSetNumEvalLines: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setNumEvalLines(value), + onSetEngineCores: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setEngineCores(value), + ), + ListSection( + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: analysisPrefs.showAnnotations, + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: analysisPrefs.showPgnComments, + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).togglePgnComments(), + ), + ], + ), + ListSection( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart new file mode 100644 index 0000000000..81c062ab17 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; + +const kOpeningHeaderHeight = 32.0; + +class BroadcastGameTreeView extends ConsumerWidget { + const BroadcastGameTreeView(this.roundId, this.gameId); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + final broadcastGameState = ref.watch(ctrlProvider).requireValue; + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + + return SingleChildScrollView( + child: DebouncedPgnTreeView( + root: broadcastGameState.root, + currentPath: broadcastGameState.currentPath, + broadcastLivePath: broadcastGameState.broadcastLivePath, + pgnRootComments: broadcastGameState.pgnRootComments, + shouldShowAnnotations: analysisPrefs.showAnnotations, + notifier: ref.read(ctrlProvider.notifier), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart new file mode 100644 index 0000000000..af322f4220 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -0,0 +1,658 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); +const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 12.0); + +/// A screen that displays a paginated list of broadcasts. +class BroadcastListScreen extends StatelessWidget { + const BroadcastListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final title = AutoSizeText( + context.l10n.broadcastBroadcasts, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ); + return PlatformWidget( + androidBuilder: (_) => Scaffold(body: const _Body(), appBar: AppBar(title: title)), + iosBuilder: + (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: title, + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), + border: null, + ), + child: const _Body(), + ), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + final ScrollController _scrollController = ScrollController(); + ImageColorWorker? _worker; + + final GlobalKey _refreshIndicatorKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + _initWorker(); + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _worker?.close(); + super.dispose(); + } + + Future _initWorker() async { + final worker = await ref.read(broadcastImageWorkerFactoryProvider).spawn(); + if (mounted) { + setState(() { + _worker = worker; + }); + } + } + + void _scrollListener() { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { + final broadcastList = ref.read(broadcastsPaginatorProvider); + + if (!broadcastList.isLoading) { + ref.read(broadcastsPaginatorProvider.notifier).next(); + } + } + } + + @override + Widget build(BuildContext context) { + final broadcasts = ref.watch(broadcastsPaginatorProvider); + + if (_worker == null || (!broadcasts.hasValue && broadcasts.isLoading)) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + if (!broadcasts.hasValue && broadcasts.isLoading) { + debugPrint('SEVERE: [BroadcastsListScreen] could not load broadcast tournaments'); + return const Center(child: Text('Could not load broadcast tournaments')); + } + + final screenWidth = MediaQuery.sizeOf(context).width; + final itemsByRow = + screenWidth >= 1200 + ? 3 + : screenWidth >= 700 + ? 2 + : 1; + const loadingItems = 12; + final pastItemsCount = + broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); + + final highTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow, + crossAxisSpacing: 12.0, + mainAxisSpacing: 16.0, + childAspectRatio: 1.45, + ); + + final lowTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow + 1, + crossAxisSpacing: 12.0, + mainAxisSpacing: 16.0, + childAspectRatio: + screenWidth >= 1200 + ? 1.4 + : screenWidth >= 700 + ? 1.3 + : 1.0, + ); + + final sections = [ + ('ongoing', context.l10n.broadcastOngoing, broadcasts.value!.active), + ('past', context.l10n.broadcastCompleted, broadcasts.value!.past), + ]; + + final activeHighTier = + broadcasts.value!.active + .where((broadcast) => broadcast.tour.tier != null && broadcast.tour.tier! >= 4) + .toList(); + + final activeLowTier = + broadcasts.value!.active + .where((broadcast) => broadcast.tour.tier == null || broadcast.tour.tier! < 4) + .toList(); + + return RefreshIndicator.adaptive( + edgeOffset: + Theme.of(context).platform == TargetPlatform.iOS + ? MediaQuery.paddingOf(context).top + 16.0 + : 0, + key: _refreshIndicatorKey, + onRefresh: () async => ref.refresh(broadcastsPaginatorProvider), + child: Shimmer( + child: CustomScrollView( + controller: _scrollController, + slivers: [ + for (final section in sections) + SliverMainAxisGroup( + key: ValueKey(section), + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + CupertinoSliverNavigationBar( + automaticallyImplyLeading: false, + leading: null, + largeTitle: AutoSizeText( + section.$2, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + transitionBetweenRoutes: false, + ) + else + SliverAppBar( + automaticallyImplyLeading: false, + title: AutoSizeText( + section.$2, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + pinned: true, + ), + if (section.$1 == 'ongoing') ...[ + if (activeHighTier.isNotEmpty) + SliverPadding( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: highTierGridDelegate, + itemBuilder: + (context, index) => BroadcastCard( + worker: _worker!, + broadcast: activeHighTier[index], + aspectRatio: highTierGridDelegate.childAspectRatio, + ), + itemCount: activeHighTier.length, + ), + ), + if (activeLowTier.isNotEmpty) + SliverPadding( + padding: Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: lowTierGridDelegate, + itemBuilder: + (context, index) => BroadcastCard( + worker: _worker!, + broadcast: activeLowTier[index], + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), + itemCount: activeLowTier.length, + ), + ), + ] else + SliverPadding( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: lowTierGridDelegate, + itemBuilder: + (context, index) => + (broadcasts.isLoading && index >= pastItemsCount - loadingItems) + ? ShimmerLoading( + isLoading: true, + child: BroadcastCard.loading( + worker: _worker!, + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), + ) + : BroadcastCard( + worker: _worker!, + broadcast: section.$3[index], + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), + itemCount: section.$3.length, + ), + ), + ], + ), + const SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter(child: SizedBox(height: 16.0)), + ), + ], + ), + ), + ); + } +} + +class BroadcastCard extends StatefulWidget { + const BroadcastCard({ + required this.broadcast, + required this.worker, + required this.aspectRatio, + super.key, + }); + + final Broadcast broadcast; + final ImageColorWorker worker; + final double aspectRatio; + + const BroadcastCard.loading({required this.worker, required this.aspectRatio}) + : broadcast = const Broadcast( + tour: BroadcastTournamentData( + id: BroadcastTournamentId(''), + name: '', + slug: '', + imageUrl: null, + description: '', + information: ( + format: null, + timeControl: null, + players: null, + website: null, + location: null, + dates: null, + ), + ), + round: BroadcastRound( + id: BroadcastRoundId(''), + name: '', + slug: '', + status: RoundStatus.finished, + startsAt: null, + finishedAt: null, + startsAfterPrevious: false, + ), + group: null, + roundToLinkId: BroadcastRoundId(''), + ); + + @override + State createState() => _BroadcastCartState(); +} + +typedef _CardColors = ({Color primaryContainer, Color onPrimaryContainer}); +final Map _colorsCache = {}; + +final _dateFormat = DateFormat.MMMd().add_jm(); + +class _BroadcastCartState extends State { + _CardColors? _cardColors; + bool _tapDown = false; + + String? get imageUrl => widget.broadcast.tour.imageUrl; + + ImageProvider get imageProvider => + imageUrl != null ? NetworkImage(imageUrl!) : kDefaultBroadcastImage; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final cachedColors = _colorsCache[imageProvider]; + if (_colorsCache.containsKey(imageProvider)) { + _cardColors = cachedColors; + } else if (imageUrl != null) { + _getImageColors(NetworkImage(imageUrl!)); + } + } + + Future _getImageColors(NetworkImage provider) async { + if (!mounted) return; + + if (Scrollable.recommendDeferredLoadingForContext(context)) { + SchedulerBinding.instance.scheduleFrameCallback((_) { + scheduleMicrotask(() => _getImageColors(provider)); + }); + } else if (widget.worker.closed == false) { + await precacheImage(provider, context); + final ui.Image scaledImage = await _imageProviderToScaled(provider); + final imageBytes = await scaledImage.toByteData(); + final response = await _computeImageColors(widget.worker, provider.url, imageBytes!); + if (response != null) { + if (mounted) { + setState(() { + _cardColors = response; + }); + } + } + } + } + + void _onTapDown() { + setState(() => _tapDown = true); + } + + void _onTapCancel() { + setState(() => _tapDown = false); + } + + @override + Widget build(BuildContext context) { + final defaultBackgroundColor = + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer; + final backgroundColor = _cardColors?.primaryContainer ?? defaultBackgroundColor; + final titleColor = _cardColors?.onPrimaryContainer; + final subTitleColor = + _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? textShade(context, 0.8); + final bgHsl = HSLColor.fromColor(backgroundColor); + final liveHsl = HSLColor.fromColor(LichessColors.red); + final liveColor = (bgHsl.lightness <= 0.6 ? liveHsl.withLightness(0.9) : liveHsl).toColor(); + + String? eventDate; + if (widget.broadcast.round.startsAt != null) { + final diff = widget.broadcast.round.startsAt!.difference(DateTime.now()); + if (!diff.isNegative && diff.inDays >= 1) { + eventDate = _dateFormat.format(widget.broadcast.round.startsAt!); + } else { + eventDate = relativeDate(context.l10n, widget.broadcast.round.startsAt!); + } + } + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + title: widget.broadcast.title, + rootNavigator: true, + builder: (context) => BroadcastRoundScreen(broadcast: widget.broadcast), + ); + }, + onTapDown: (_) => _onTapDown(), + onTapCancel: _onTapCancel, + onTapUp: (_) => _onTapCancel(), + child: AnimatedOpacity( + opacity: _tapDown ? 1.0 : 0.85, + duration: const Duration(milliseconds: 100), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kCardBorderRadius, + color: backgroundColor, + boxShadow: + Theme.of(context).platform == TargetPlatform.iOS ? null : kElevationToShadow[1], + ), + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: const Alignment(0.0, 0.5), + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.0), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.10], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: AspectRatio( + aspectRatio: 2.0, + child: Image( + image: imageProvider, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: + (context, error, stackTrace) => const Image(image: kDefaultBroadcastImage), + ), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 8.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + mainAxisAlignment: + widget.broadcast.isLive + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + if (!widget.broadcast.isFinished) ...[ + Flexible( + flex: widget.broadcast.isLive ? 1 : 0, + child: Text( + widget.broadcast.round.name, + style: TextStyle(color: subTitleColor, letterSpacing: -0.2), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + const SizedBox(width: 5.0), + ], + if (widget.broadcast.isLive) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.circle, + size: 16, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + const SizedBox(width: 4.0), + Text( + 'LIVE', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ] else if (eventDate != null) + Flexible( + child: Text( + eventDate, + style: TextStyle(fontSize: 12, color: subTitleColor), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + Padding( + padding: kBroadcastGridItemContentPadding.add( + const EdgeInsets.symmetric(vertical: 3.0), + ), + child: Text( + widget.broadcast.title, + maxLines: widget.aspectRatio == 1.0 ? 3 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.bold, + height: 1.0, + ), + ), + ), + if (widget.broadcast.tour.information.players != null) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Text( + widget.broadcast.tour.information.players!, + style: TextStyle(fontSize: 12, color: subTitleColor, letterSpacing: -0.2), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +Future<_CardColors?> _computeImageColors( + ImageColorWorker worker, + String imageUrl, + ByteData imageBytes, +) async { + final response = await worker.getImageColors(imageBytes.buffer.asUint32List()); + if (response != null) { + final (:primaryContainer, :onPrimaryContainer) = response; + final cardColors = ( + primaryContainer: Color(primaryContainer), + onPrimaryContainer: Color(onPrimaryContainer), + ); + _colorsCache[NetworkImage(imageUrl)] = cardColors; + return cardColors; + } + return null; +} + +/// Pre-cache images and extract colors for broadcasts. +Future preCacheBroadcastImages( + BuildContext context, { + required Iterable broadcasts, + required ImageColorWorker worker, +}) async { + for (final broadcast in broadcasts) { + final imageUrl = broadcast.tour.imageUrl; + if (imageUrl != null) { + final provider = NetworkImage(imageUrl); + await precacheImage(provider, context); + final ui.Image scaledImage = await _imageProviderToScaled(provider); + final imageBytes = await scaledImage.toByteData(); + await _computeImageColors(worker, imageUrl, imageBytes!); + } + } +} + +// Scale image size down to reduce computation time of color extraction. +Future _imageProviderToScaled(ImageProvider imageProvider) async { + const double maxDimension = 112.0; + final ImageStream stream = imageProvider.resolve( + const ImageConfiguration(size: Size(maxDimension, maxDimension)), + ); + final Completer imageCompleter = Completer(); + late ImageStreamListener listener; + late ui.Image scaledImage; + Timer? loadFailureTimeout; + + listener = ImageStreamListener( + (ImageInfo info, bool sync) async { + loadFailureTimeout?.cancel(); + stream.removeListener(listener); + final ui.Image image = info.image; + final int width = image.width; + final int height = image.height; + double paintWidth = width.toDouble(); + double paintHeight = height.toDouble(); + assert(width > 0 && height > 0); + + final bool rescale = width > maxDimension || height > maxDimension; + if (rescale) { + paintWidth = (width > height) ? maxDimension : (maxDimension / height) * width; + paintHeight = (height > width) ? maxDimension : (maxDimension / width) * height; + } + final ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(pictureRecorder); + paintImage( + canvas: canvas, + rect: Rect.fromLTRB(0, 0, paintWidth, paintHeight), + image: image, + filterQuality: FilterQuality.none, + ); + + final ui.Picture picture = pictureRecorder.endRecording(); + scaledImage = await picture.toImage(paintWidth.toInt(), paintHeight.toInt()); + imageCompleter.complete(info.image); + }, + onError: (Object exception, StackTrace? stackTrace) { + stream.removeListener(listener); + throw Exception('Failed to render image: $exception'); + }, + ); + + loadFailureTimeout = Timer(const Duration(seconds: 5), () { + stream.removeListener(listener); + imageCompleter.completeError(TimeoutException('Timeout occurred trying to load image')); + }); + + stream.addListener(listener); + await imageCompleter.future; + return scaledImage; +} diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart new file mode 100644 index 0000000000..36b68cd26e --- /dev/null +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -0,0 +1,142 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:url_launcher/url_launcher.dart'; + +final _dateFormatter = DateFormat.MMMd(); + +/// A tab that displays the overview of a broadcast. +class BroadcastOverviewTab extends ConsumerWidget { + const BroadcastOverviewTab({required this.broadcast, required this.tournamentId, super.key}); + + final Broadcast broadcast; + final BroadcastTournamentId tournamentId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = + MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; + final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); + + switch (tournament) { + case AsyncData(value: final tournament): + final information = tournament.data.information; + final description = tournament.data.description; + return SliverPadding( + padding: edgeInsets, + sliver: SliverList( + delegate: SliverChildListDelegate([ + if (tournament.data.imageUrl != null) ...[ + Image.network(tournament.data.imageUrl!), + const SizedBox(height: 16.0), + ], + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + _BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + _BroadcastOverviewCard(Icons.emoji_events, '${information.format}'), + if (information.timeControl != null) + _BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.location != null) + _BroadcastOverviewCard(Icons.public, '${information.location}'), + if (information.players != null) + _BroadcastOverviewCard(Icons.person, '${information.players}'), + if (information.website != null) + _BroadcastOverviewCard( + Icons.link, + context.l10n.broadcastOfficialWebsite, + information.website, + ), + ], + ), + if (description != null) ...[ + const SizedBox(height: 16), + MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ], + ]), + ), + ); + case AsyncError(:final error): + return SliverPadding( + padding: edgeInsets, + sliver: SliverFillRemaining( + child: Center(child: Text('Cannot load broadcast data: $error')), + ), + ); + case _: + return SliverPadding( + padding: edgeInsets, + sliver: const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ), + ); + } + } +} + +class _BroadcastOverviewCard extends StatelessWidget { + const _BroadcastOverviewCard(this.iconData, this.text, [this.website]); + + final IconData iconData; + final String text; + final Uri? website; + + @override + Widget build(BuildContext context) { + return PlatformCard( + borderRadius: const BorderRadius.all(Radius.circular(10)), + margin: const EdgeInsets.all(4.0), + child: AdaptiveInkWell( + borderRadius: const BorderRadius.all(Radius.circular(10)), + onTap: website != null ? () => launchUrl(website!) : null, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(iconData, color: website != null ? Theme.of(context).colorScheme.primary : null), + const SizedBox(width: 10), + Flexible( + child: Text( + text, + style: TextStyle( + color: website != null ? Theme.of(context).colorScheme.primary : null, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart new file mode 100644 index 0000000000..2b1d14527b --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -0,0 +1,308 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_federation.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; +import 'package:lichess_mobile/src/widgets/stat_card.dart'; + +class BroadcastPlayerResultsScreen extends StatelessWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + final String? playerTitle; + final String playerName; + + const BroadcastPlayerResultsScreen( + this.tournamentId, + this.playerId, + this.playerTitle, + this.playerName, + ); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: BroadcastPlayerWidget(title: playerTitle, name: playerName)), + body: _Body(tournamentId, playerId), + ); + } +} + +const _kTableRowPadding = EdgeInsets.symmetric(vertical: 12.0); + +class _Body extends ConsumerWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + + const _Body(this.tournamentId, this.playerId); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final playersResults = ref.watch(broadcastPlayerResultProvider(tournamentId, playerId)); + + switch (playersResults) { + case AsyncData(value: final playerResults): + final player = playerResults.player; + final fideData = playerResults.fideData; + final showRatingDiff = playerResults.games.any((result) => result.ratingDiff != null); + final statWidth = + (MediaQuery.sizeOf(context).width - Styles.bodyPadding.horizontal - 10 * 2) / 3; + const cardSpacing = 10.0; + final indexWidth = max(8.0 + playerResults.games.length.toString().length * 10.0, 28.0); + + return ListView.builder( + itemCount: playerResults.games.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: Styles.bodyPadding, + child: Column( + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null && + fideData.ratings.rapid != null && + fideData.ratings.blitz != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.classical, + value: fideData.ratings.standard.toString(), + ), + ), + if (fideData.ratings.rapid != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.rapid, + value: fideData.ratings.rapid.toString(), + ), + ), + if (fideData.ratings.blitz != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.blitz, + value: fideData.ratings.blitz.toString(), + ), + ), + ], + ), + if (fideData.birthYear != null && + player.federation != null && + player.fideId != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.birthYear != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastAgeThisYear, + value: (DateTime.now().year - fideData.birthYear!).toString(), + ), + ), + if (player.federation != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastFederation, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/fide-fed/${player.federation}.png', + height: 12, + ), + const SizedBox(width: 5), + Flexible( + child: Text( + federationIdToName[player.federation!]!, + style: const TextStyle(fontSize: 18.0), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + if (player.fideId != null) + SizedBox( + width: statWidth, + child: StatCard('FIDE ID', value: player.fideId!.toString()), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (player.score != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastScore, + value: + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ), + if (player.performance != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.performance, + value: player.performance.toString(), + ), + ), + if (player.ratingDiff != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastRatingDiff, + child: ProgressionWidget(player.ratingDiff!, fontSize: 18.0), + ), + ), + ], + ), + ], + ), + ); + } + + final playerResult = playerResults.games[index - 1]; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastGameScreen( + tournamentId: tournamentId, + roundId: playerResult.roundId, + gameId: playerResult.gameId, + ), + ); + }, + child: ColoredBox( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : CupertinoColors.tertiarySystemBackground.resolveFrom(context) + : index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + SizedBox( + width: indexWidth, + child: Center( + child: Text( + index.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + Expanded( + flex: 5, + child: BroadcastPlayerWidget( + federation: playerResult.opponent.federation, + title: playerResult.opponent.title, + name: playerResult.opponent.name, + ), + ), + Expanded( + flex: 3, + child: + (playerResult.opponent.rating != null) + ? Center(child: Text(playerResult.opponent.rating.toString())) + : const SizedBox.shrink(), + ), + SizedBox( + width: 30, + child: Center( + child: Container( + width: 15, + height: 15, + decoration: BoxDecoration( + border: + (Theme.of(context).brightness == Brightness.light && + playerResult.color == Side.white || + Theme.of(context).brightness == Brightness.dark && + playerResult.color == Side.black) + ? Border.all( + width: 2.0, + color: Theme.of(context).colorScheme.outline, + ) + : null, + shape: BoxShape.circle, + color: switch (playerResult.color) { + Side.white => Colors.white.withValues(alpha: 0.9), + Side.black => Colors.black.withValues(alpha: 0.9), + }, + ), + ), + ), + ), + SizedBox( + width: 30, + child: Center( + child: Text( + switch (playerResult.points) { + BroadcastPoints.one => '1', + BroadcastPoints.half => '½', + BroadcastPoints.zero => '0', + _ => '*', + }, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: switch (playerResult.points) { + BroadcastPoints.one => context.lichessColors.good, + BroadcastPoints.zero => context.lichessColors.error, + _ => null, + }, + ), + ), + ), + ), + if (showRatingDiff) + SizedBox( + width: 38, + child: + (playerResult.ratingDiff != null) + ? ProgressionWidget(playerResult.ratingDiff!, fontSize: 14) + : null, + ), + ], + ), + ), + ), + ); + }, + ); + case AsyncError(:final error): + return Center(child: Text('Cannot load player data: $error')); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); + } + } +} diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart new file mode 100644 index 0000000000..e46774cbab --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; + +class BroadcastPlayerWidget extends ConsumerWidget { + const BroadcastPlayerWidget({ + this.federation, + required this.title, + required this.name, + this.rating, + this.textStyle, + }); + + final String? federation; + final String? title; + final int? rating; + final String name; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Row( + children: [ + if (federation != null) ...[ + Image.asset('assets/images/fide-fed/$federation.png', height: 12), + const SizedBox(width: 5), + ], + if (title != null) ...[ + Text( + title!, + style: TextStyle( + color: (title == 'BOT') ? context.lichessColors.fancy : context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + Flexible(child: Text(name, style: textStyle, overflow: TextOverflow.ellipsis)), + if (rating != null) ...[ + const SizedBox(width: 5), + Text(rating.toString(), overflow: TextOverflow.ellipsis), + ], + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart new file mode 100644 index 0000000000..6c1345b31e --- /dev/null +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -0,0 +1,294 @@ +import 'dart:math'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; + +/// A tab that displays the players participating in a broadcast tournament. +class BroadcastPlayersTab extends ConsumerWidget { + const BroadcastPlayersTab({required this.tournamentId}); + + final BroadcastTournamentId tournamentId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = + MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; + final players = ref.watch(broadcastPlayersProvider(tournamentId)); + + return switch (players) { + AsyncData(value: final players) => PlayersList(players, tournamentId), + AsyncError(:final error) => SliverPadding( + padding: edgeInsets, + sliver: SliverFillRemaining(child: Center(child: Text('Cannot load players data: $error'))), + ), + _ => const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), + }; + } +} + +enum _SortingTypes { player, elo, score } + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); +const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); + +class PlayersList extends ConsumerStatefulWidget { + const PlayersList(this.players, this.tournamentId); + + final IList players; + final BroadcastTournamentId tournamentId; + + @override + ConsumerState createState() => _PlayersListState(); +} + +class _PlayersListState extends ConsumerState { + late IList players; + late _SortingTypes currentSort; + bool reverse = false; + + @override + void initState() { + super.initState(); + players = widget.players; + currentSort = players.firstOrNull?.score != null ? _SortingTypes.score : _SortingTypes.elo; + sort(currentSort); + } + + @override + void didUpdateWidget(PlayersList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.players != widget.players) { + players = widget.players; + currentSort = players.firstOrNull?.score != null ? _SortingTypes.score : _SortingTypes.elo; + sort(currentSort); + } + } + + void sort(_SortingTypes newSort, {bool toggleReverse = false}) { + final compare = switch (newSort) { + _SortingTypes.player => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => a.name.compareTo(b.name), + _SortingTypes.elo => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.rating == null) return 1; + if (b.rating == null) return -1; + return b.rating!.compareTo(a.rating!); + }, + _SortingTypes.score => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.score == null) return 1; + if (b.score == null) return -1; + + final value = b.score!.compareTo(a.score!); + if (value == 0) { + return a.played.compareTo(b.played); + } else { + return value; + } + }, + }; + + setState(() { + if (currentSort != newSort) { + reverse = false; + } else { + reverse = toggleReverse ? !reverse : reverse; + } + currentSort = newSort; + players = reverse ? players.sortReversed(compare) : players.sort(compare); + }); + } + + @override + Widget build(BuildContext context) { + final double eloWidth = max(MediaQuery.sizeOf(context).width * 0.2, 100); + final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 90); + + final firstPlayer = players.firstOrNull; + + return SliverList.builder( + itemCount: players.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: _TableTitleCell( + title: Text(context.l10n.player, style: _kHeaderTextStyle), + onTap: + () => sort( + _SortingTypes.player, + toggleReverse: currentSort == _SortingTypes.player, + ), + sortIcon: + (currentSort == _SortingTypes.player) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, + ), + ), + SizedBox( + width: eloWidth, + child: _TableTitleCell( + title: const Text('Elo', style: _kHeaderTextStyle), + onTap: + () => + sort(_SortingTypes.elo, toggleReverse: currentSort == _SortingTypes.elo), + sortIcon: + (currentSort == _SortingTypes.elo) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, + ), + ), + SizedBox( + width: scoreWidth, + child: _TableTitleCell( + title: Text( + firstPlayer?.score != null ? context.l10n.broadcastScore : context.l10n.games, + style: _kHeaderTextStyle, + ), + onTap: + () => sort( + _SortingTypes.score, + toggleReverse: currentSort == _SortingTypes.score, + ), + sortIcon: + (currentSort == _SortingTypes.score) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, + ), + ), + ], + ); + } else { + final player = players[index - 1]; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastPlayerResultsScreen( + widget.tournamentId, + player.fideId != null ? player.fideId.toString() : player.name, + player.title, + player.name, + ), + ); + }, + child: ColoredBox( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : CupertinoColors.tertiarySystemBackground.resolveFrom(context) + : index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + ), + ), + ), + SizedBox( + width: eloWidth, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + if (player.rating != null) ...[ + Text(player.rating.toString()), + const SizedBox(width: 5), + if (player.ratingDiff != null) + ProgressionWidget(player.ratingDiff!, fontSize: 14), + ], + ], + ), + ), + ), + SizedBox( + width: scoreWidth, + child: Padding( + padding: _kTableRowPadding, + child: + (player.score != null) + ? Align( + alignment: Alignment.centerRight, + child: Text( + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ) + : Align( + alignment: Alignment.centerRight, + child: Text(player.played.toString()), + ), + ), + ), + ], + ), + ), + ); + } + }, + ); + } +} + +class _TableTitleCell extends StatelessWidget { + const _TableTitleCell({required this.title, required this.onTap, this.sortIcon}); + + final Widget title; + final void Function() onTap; + final IconData? sortIcon; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44, + child: GestureDetector( + onTap: onTap, + child: Padding( + padding: _kTableRowPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: title), + if (sortIcon != null) + Text( + String.fromCharCode(sortIcon!.codePoint), + style: _kHeaderTextStyle.copyWith(fontSize: 16, fontFamily: sortIcon!.fontFamily), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 8d1839a8d8..cafba27587 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -1,346 +1,485 @@ -import 'dart:async'; - -import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; -import 'package:lichess_mobile/src/utils/duration.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_players_tab.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -// height of 1.0 is important because we need to determine the height of the text -// to calculate the height of the header and footer of the board -const _kPlayerWidgetTextStyle = TextStyle(fontSize: 13, height: 1.0); +class BroadcastRoundScreen extends ConsumerStatefulWidget { + final Broadcast broadcast; -const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); + const BroadcastRoundScreen({required this.broadcast}); -/// A screen that displays the live games of a broadcast round. -class BroadcastRoundScreen extends StatelessWidget { - final String broadCastTitle; - final BroadcastRoundId roundId; + @override + _BroadcastRoundScreenState createState() => _BroadcastRoundScreenState(); +} - const BroadcastRoundScreen({ - super.key, - required this.broadCastTitle, - required this.roundId, - }); +enum _CupertinoView { overview, boards, players } + +class _BroadcastRoundScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + _CupertinoView selectedTab = _CupertinoView.overview; + late final TabController _tabController; + late BroadcastTournamentId _selectedTournamentId; + BroadcastRoundId? _selectedRoundId; + + bool roundLoaded = false; @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + void initState() { + super.initState(); + _tabController = TabController(initialIndex: 0, length: 3, vsync: this); + _selectedTournamentId = widget.broadcast.tour.id; + _selectedRoundId = widget.broadcast.roundToLinkId; } - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: Text(broadCastTitle), - ), - body: _Body(roundId), - ); + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void setCupertinoTab(_CupertinoView mode) { + setState(() { + selectedTab = mode; + }); + } + + void setTournamentId(BroadcastTournamentId tournamentId) { + setState(() { + _selectedTournamentId = tournamentId; + _selectedRoundId = null; + }); + } + + void setRoundId(BroadcastRoundId roundId) { + setState(() { + roundLoaded = false; + _selectedRoundId = roundId; + }); } Widget _iosBuilder( BuildContext context, + AsyncValue asyncTournament, + AsyncValue asyncRound, ) { + final tabSwitcher = CupertinoSlidingSegmentedControl<_CupertinoView>( + groupValue: selectedTab, + children: { + _CupertinoView.overview: Text(context.l10n.broadcastOverview), + _CupertinoView.boards: Text(context.l10n.broadcastBoards), + _CupertinoView.players: Text(context.l10n.players), + }, + onValueChanged: (_CupertinoView? view) { + if (view != null) { + setCupertinoTab(view); + } + }, + ); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Text(broadCastTitle), + middle: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + child: Column( + children: [ + Expanded( + child: switch (asyncRound) { + AsyncData(value: final _) => switch (selectedTab) { + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, + roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, + ), + _ => const SliverFillRemaining(child: SizedBox.shrink()), + }, + ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab(tournamentId: _selectedTournamentId), + ), + }, + _ => const Center(child: CircularProgressIndicator.adaptive()), + }, + ), + switch (asyncTournament) { + AsyncData(:final value) => _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + _ => const BottomBar.empty(), + }, + ], ), - child: _Body(roundId), ); } -} - -class _Body extends ConsumerWidget { - final BroadcastRoundId roundId; - const _Body(this.roundId); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final games = ref.watch(broadcastRoundControllerProvider(roundId)); - - return games.when( - data: (games) => (games.isEmpty) - ? const Text('No games to show for now') - : BroadcastPreview(games: games.values.toIList()), - loading: () => const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastPreview(), + Widget _androidBuilder( + BuildContext context, + AsyncValue asyncTournament, + AsyncValue asyncRound, + ) { + return Scaffold( + appBar: AppBar( + title: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.broadcastOverview), + Tab(text: context.l10n.broadcastBoards), + Tab(text: context.l10n.players), + ], ), ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), + body: switch (asyncRound) { + AsyncData(value: final _) => TabBarView( + controller: _tabController, + children: [ + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _TabView( + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, + roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, + ), + _ => const SliverFillRemaining(child: SizedBox.shrink()), + }, + ), + _TabView(sliver: BroadcastPlayersTab(tournamentId: _selectedTournamentId)), + ], + ), + _ => const Center(child: CircularProgressIndicator()), + }, + bottomNavigationBar: switch (asyncTournament) { + AsyncData(:final value) => _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + _ => const BottomBar.empty(), + }, ); } + + @override + Widget build(BuildContext context) { + final asyncTour = ref.watch(broadcastTournamentProvider(_selectedTournamentId)); + + const loadingRound = AsyncValue.loading(); + + switch (asyncTour) { + case AsyncData(value: final tournament): + // Eagerly initalize the round controller so it stays alive when switching tabs + // and to know if the round has games to show + final round = ref.watch( + broadcastRoundControllerProvider(_selectedRoundId ?? tournament.defaultRoundId), + ); + + ref.listen( + broadcastRoundControllerProvider(_selectedRoundId ?? tournament.defaultRoundId), + (_, round) { + if (round.hasValue && !roundLoaded) { + roundLoaded = true; + if (round.value!.games.isNotEmpty) { + _tabController.index = 1; + + if (Theme.of(context).platform == TargetPlatform.iOS) { + setCupertinoTab(_CupertinoView.boards); + } + } + } + }, + ); + + return PlatformWidget( + androidBuilder: (context) => _androidBuilder(context, asyncTour, round), + iosBuilder: (context) => _iosBuilder(context, asyncTour, round), + ); + + case _: + return PlatformWidget( + androidBuilder: (context) => _androidBuilder(context, asyncTour, loadingRound), + iosBuilder: (context) => _iosBuilder(context, asyncTour, loadingRound), + ); + } + } } -class BroadcastPreview extends StatelessWidget { - final IList? games; +class _TabView extends StatelessWidget { + const _TabView({required this.sliver, this.cupertinoTabSwitcher}); - const BroadcastPreview({super.key, this.games}); + final Widget sliver; + final Widget? cupertinoTabSwitcher; @override Widget build(BuildContext context) { - const numberLoadingBoards = 12; - const boardSpacing = 10.0; - // height of the text based on the font size - // since the TextStyle is defined with an height at 1.0, this is the real height - // see: https://api.flutter.dev/flutter/painting/TextStyle/height.html - final textHeight = _kPlayerWidgetTextStyle.fontSize!; - final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; - final numberOfBoardsByRow = isTabletOrLarger(context) ? 4 : 2; - final screenWidth = MediaQuery.sizeOf(context).width; - final boardWidth = (screenWidth - - Styles.horizontalBodyPadding.horizontal - - (numberOfBoardsByRow - 1) * boardSpacing) / - numberOfBoardsByRow; - - return SafeArea( - child: Padding( - padding: Styles.horizontalBodyPadding, - child: GridView.builder( - itemCount: games == null ? numberLoadingBoards : games!.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: numberOfBoardsByRow, - crossAxisSpacing: boardSpacing, - mainAxisSpacing: boardSpacing, - mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, - ), - itemBuilder: (context, index) { - if (games == null) { - return BoardThumbnail.loading( - size: boardWidth, - header: _PlayerWidget.loading(width: boardWidth), - footer: _PlayerWidget.loading(width: boardWidth), - ); - } - - final game = games![index]; - final playingSide = dartchess.Setup.parseFen(game.fen).turn.cg; - - return BoardThumbnail( - orientation: Side.white, - fen: game.fen, - lastMove: game.lastMove?.cg, - size: boardWidth, - header: _PlayerWidget( - width: boardWidth, - player: game.players[dartchess.Side.black]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - side: Side.black, - playingSide: playingSide, - ), - footer: _PlayerWidget( - width: boardWidth, - player: game.players[dartchess.Side.white]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - side: Side.white, - playingSide: playingSide, - ), - ); - }, - ), + return Shimmer( + child: CustomScrollView( + slivers: [ + if (cupertinoTabSwitcher != null) + SliverPadding( + padding: Styles.bodyPadding + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), + ), + sliver, + ], ), ); } } -class _PlayerWidget extends StatelessWidget { - const _PlayerWidget({ - required this.width, - required this.player, - required this.gameStatus, - required this.thinkTime, - required this.side, - required this.playingSide, - }) : _displayShimmerPlaceholder = false; - - const _PlayerWidget.loading({ - required this.width, - }) : player = const BroadcastPlayer( - name: '', - title: null, - rating: null, - clock: null, - federation: null, - ), - gameStatus = '*', - thinkTime = null, - side = Side.white, - playingSide = Side.white, - _displayShimmerPlaceholder = true; - - final BroadcastPlayer player; - final String gameStatus; - final Duration? thinkTime; - final Side side; - final Side playingSide; - final double width; +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.tournament, + required this.roundId, + required this.setTournamentId, + required this.setRoundId, + }); - final bool _displayShimmerPlaceholder; + final BroadcastTournament tournament; + final BroadcastRoundId roundId; + final void Function(BroadcastTournamentId) setTournamentId; + final void Function(BroadcastRoundId) setRoundId; @override - Widget build(BuildContext context) { - if (_displayShimmerPlaceholder) { - return SizedBox( - width: width, - child: Padding( - padding: _kPlayerWidgetPadding, - child: Container( - height: _kPlayerWidgetTextStyle.fontSize, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), + Widget build(BuildContext context, WidgetRef ref) { + return BottomBar( + children: [ + if (tournament.group != null) + AdaptiveTextButton( + onPressed: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.4, + maxChildSize: 0.4, + minChildSize: 0.1, + snap: true, + expand: false, + builder: (context, scrollController) { + return _TournamentSelectorMenu( + tournament: tournament, + group: tournament.group!, + scrollController: scrollController, + setTournamentId: setTournamentId, + ); + }, + ), + ), + child: Text( + tournament.group!.firstWhere((g) => g.id == tournament.data.id).name, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - ), - ); - } - - return SizedBox( - width: width, - child: Padding( - padding: _kPlayerWidgetPadding, - child: DefaultTextStyle.merge( - style: _kPlayerWidgetTextStyle, + AdaptiveTextButton( + onPressed: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.6, + snap: true, + expand: false, + builder: (context, scrollController) { + return _RoundSelectorMenu( + selectedRoundId: roundId, + rounds: tournament.rounds, + scrollController: scrollController, + setRoundId: setRoundId, + ); + }, + ), + ), child: Row( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: widgetRef.read(defaultClientProvider), - ); - }, - ), - ], - const SizedBox(width: 5), - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Flexible( - child: Text( - player.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + child: Text( + tournament.rounds.firstWhere((round) => round.id == roundId).name, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - const SizedBox(width: 5), - if (gameStatus != '*') - Text( - (gameStatus == '½-½') - ? '½' - : (gameStatus == '1-0') - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: - const TextStyle().copyWith(fontWeight: FontWeight.bold), - ) - else if (player.clock != null) - if (side == playingSide) - _Clock( - clock: player.clock! - (thinkTime ?? Duration.zero), - ) - else - Text(player.clock!.toHoursMinutesSeconds()), + const SizedBox(width: 5.0), + switch (tournament.rounds.firstWhere((round) => round.id == roundId).status) { + RoundStatus.finished => Icon(Icons.check, color: context.lichessColors.good), + RoundStatus.live => Icon(Icons.circle, color: context.lichessColors.error), + RoundStatus.upcoming => const Icon(Icons.calendar_month, color: Colors.grey), + }, ], ), ), - ), + ], ); } } -class _Clock extends StatefulWidget { - const _Clock({required this.clock}); +class _RoundSelectorMenu extends ConsumerStatefulWidget { + const _RoundSelectorMenu({ + required this.selectedRoundId, + required this.rounds, + required this.scrollController, + required this.setRoundId, + }); - final Duration clock; + final BroadcastRoundId selectedRoundId; + final IList rounds; + final ScrollController scrollController; + final void Function(BroadcastRoundId) setRoundId; @override - _ClockState createState() => _ClockState(); + ConsumerState<_RoundSelectorMenu> createState() => _RoundSelectorState(); } -class _ClockState extends State<_Clock> { - Timer? _timer; - late Duration _clock; +final _dateFormatMonth = DateFormat.MMMd().add_jm(); +final _dateFormatYearMonth = DateFormat.yMMMd().add_jm(); + +class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { + final currentRoundKey = GlobalKey(); @override - void initState() { - super.initState(); - _clock = widget.clock; - if (_clock.inSeconds <= 0) { - _clock = Duration.zero; - return; - } - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - _clock = _clock - const Duration(seconds: 1); - }); - if (_clock.inSeconds == 0) { - timer.cancel(); - return; + Widget build(BuildContext context) { + // Scroll to the current round + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentRoundKey.currentContext != null) { + Scrollable.ensureVisible(currentRoundKey.currentContext!, alignment: 0.5); } }); + + return BottomSheetScrollableContainer( + scrollController: widget.scrollController, + children: [ + for (final (index, round) in widget.rounds.indexed) + PlatformListTile( + key: round.id == widget.selectedRoundId ? currentRoundKey : null, + selected: round.id == widget.selectedRoundId, + title: Text(round.name), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (round.startsAt != null || round.startsAfterPrevious) ...[ + Text( + round.startsAt != null + ? round.startsAt!.difference(DateTime.now()).inDays.abs() < 30 + ? _dateFormatMonth.format(round.startsAt!) + : _dateFormatYearMonth.format(round.startsAt!) + : context.l10n.broadcastStartsAfter(widget.rounds[index - 1].name), + ), + const SizedBox(width: 5.0), + ], + switch (round.status) { + RoundStatus.finished => Icon(Icons.check, color: context.lichessColors.good), + RoundStatus.live => Icon(Icons.circle, color: context.lichessColors.error), + RoundStatus.upcoming => const Icon(Icons.calendar_month, color: Colors.grey), + }, + ], + ), + onTap: () { + widget.setRoundId(round.id); + Navigator.of(context).pop(); + }, + ), + ], + ); } +} + +class _TournamentSelectorMenu extends ConsumerStatefulWidget { + const _TournamentSelectorMenu({ + required this.tournament, + required this.group, + required this.scrollController, + required this.setTournamentId, + }); + + final BroadcastTournament tournament; + final IList group; + final ScrollController scrollController; + final void Function(BroadcastTournamentId) setTournamentId; @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } + ConsumerState<_TournamentSelectorMenu> createState() => _TournamentSelectorState(); +} + +class _TournamentSelectorState extends ConsumerState<_TournamentSelectorMenu> { + final currentTournamentKey = GlobalKey(); @override Widget build(BuildContext context) { - return Text( - _clock.toHoursMinutesSeconds(), - style: TextStyle( - color: Colors.orange[900], - fontFeatures: const [FontFeature.tabularFigures()], - ), + // Scroll to the current tournament + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentTournamentKey.currentContext != null) { + Scrollable.ensureVisible(currentTournamentKey.currentContext!, alignment: 0.5); + } + }); + + return BottomSheetScrollableContainer( + scrollController: widget.scrollController, + children: [ + for (final tournament in widget.group) + PlatformListTile( + key: tournament.id == widget.tournament.data.id ? currentTournamentKey : null, + selected: tournament.id == widget.tournament.data.id, + title: Text(tournament.name), + onTap: () { + widget.setTournamentId(tournament.id); + Navigator.of(context).pop(); + }, + ), + ], ); } } diff --git a/lib/src/view/broadcast/broadcast_tile.dart b/lib/src/view/broadcast/broadcast_tile.dart deleted file mode 100644 index 2097846ca9..0000000000 --- a/lib/src/view/broadcast/broadcast_tile.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -import 'package:lichess_mobile/src/styles/transparent_image.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; - -class BroadcastTile extends StatelessWidget { - const BroadcastTile({ - required this.broadcast, - }); - - final Broadcast broadcast; - - @override - Widget build(BuildContext context) { - return PlatformListTile( - leading: (broadcast.tour.imageUrl != null) - ? FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: broadcast.tour.imageUrl!, - width: 60, - height: 30, - ) - : const DefaultBroadcastImage(width: 60), - onTap: () { - pushPlatformRoute( - context, - builder: (context) => BroadcastRoundScreen( - broadCastTitle: broadcast.tour.name, - roundId: broadcast.roundToLinkId, - ), - ); - }, - title: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: Row( - children: [ - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - trailing: (broadcast.isLive) - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.circle, color: Colors.red, size: 20), - SizedBox(height: 5), - Text( - 'LIVE', - style: - TextStyle(fontWeight: FontWeight.bold, color: Colors.red), - ), - ], - ) - : null, - ); - } -} diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart deleted file mode 100644 index ccdf262370..0000000000 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ /dev/null @@ -1,313 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/styles/transparent_image.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; - -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); - -/// A screen that displays a paginated list of broadcasts. -class BroadcastsListScreen extends StatelessWidget { - const BroadcastsListScreen({super.key}); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.broadcastLiveBroadcasts), - ), - body: const _Body(), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.broadcastLiveBroadcasts), - ), - child: const _Body(), - ); - } -} - -class _Body extends ConsumerStatefulWidget { - const _Body(); - - @override - ConsumerState createState() => _BodyState(); -} - -class _BodyState extends ConsumerState<_Body> { - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - _scrollController.addListener(_scrollListener); - } - - @override - void dispose() { - _scrollController.removeListener(_scrollListener); - _scrollController.dispose(); - super.dispose(); - } - - void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { - final broadcastList = ref.read(broadcastsPaginatorProvider); - - if (!broadcastList.isLoading) { - ref.read(broadcastsPaginatorProvider.notifier).next(); - } - } - } - - @override - Widget build(BuildContext context) { - final broadcasts = ref.watch(broadcastsPaginatorProvider); - - if (!broadcasts.hasValue && broadcasts.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (!broadcasts.hasValue && broadcasts.isLoading) { - debugPrint( - 'SEVERE: [BroadcastsListScreen] could not load broadcast tournaments', - ); - return const Center(child: Text('Could not load broadcast tournaments')); - } - - final itemsCount = - broadcasts.requireValue.past.length + (broadcasts.isLoading ? 10 : 0); - - return SafeArea( - child: CustomScrollView( - controller: _scrollController, - slivers: [ - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemBuilder: (context, index) => - BroadcastGridItem(broadcast: broadcasts.value!.active[index]), - itemCount: broadcasts.value!.active.length, - ), - ), - SliverPadding( - padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - sliver: SliverToBoxAdapter( - child: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: const Text('Upcoming broadcasts'), - ), - ), - ), - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemBuilder: (context, index) => BroadcastGridItem( - broadcast: broadcasts.value!.upcoming[index], - ), - itemCount: broadcasts.value!.upcoming.length, - ), - ), - SliverPadding( - padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - sliver: SliverToBoxAdapter( - child: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: const Text('Past broadcasts'), - ), - ), - ), - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemBuilder: (context, index) => (broadcasts.isLoading && - index >= itemsCount - 10) - ? Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastGridItem.loading(), - ), - ) - : BroadcastGridItem(broadcast: broadcasts.value!.past[index]), - itemCount: itemsCount, - ), - ), - const SliverToBoxAdapter( - child: SizedBox( - height: 10, - ), - ), - ], - ), - ); - } -} - -class BroadcastGridItem extends StatelessWidget { - final Broadcast broadcast; - - const BroadcastGridItem({required this.broadcast}); - - BroadcastGridItem.loading() - : broadcast = Broadcast( - tour: const (name: '', imageUrl: null), - round: BroadcastRound( - id: const BroadcastRoundId(''), - name: '', - status: RoundStatus.finished, - startsAt: DateTime.now(), - ), - group: null, - roundToLinkId: const BroadcastRoundId(''), - ); - - @override - Widget build(BuildContext context) { - return AdaptiveInkWell( - borderRadius: BorderRadius.circular(20), - onTap: () { - pushPlatformRoute( - context, - builder: (context) => BroadcastRoundScreen( - broadCastTitle: broadcast.tour.name, - roundId: broadcast.roundToLinkId, - ), - ); - }, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: LichessColors.grey.withOpacity(0.5), - blurRadius: 5, - spreadRadius: 1, - ), - ], - ), - foregroundDecoration: BoxDecoration( - border: (broadcast.isLive) - ? Border.all(color: LichessColors.red, width: 2) - : Border.all(color: LichessColors.grey), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - if (broadcast.tour.imageUrl != null) - AspectRatio( - aspectRatio: 2.0, - child: FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: broadcast.tour.imageUrl!, - ), - ) - else - const DefaultBroadcastImage(aspectRatio: 2.0), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (!broadcast.isFinished) ...[ - Text( - broadcast.round.name, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: textShade(context, 0.5), - ), - ), - const SizedBox(width: 4.0), - ], - if (broadcast.isLive) - const Text( - 'LIVE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ) - else - Text( - _dateFormatter.format(broadcast.round.startsAt), - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith( - color: textShade(context, 0.5), - ), - ), - ], - ), - const SizedBox(height: 4.0), - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/view/broadcast/default_broadcast_image.dart b/lib/src/view/broadcast/default_broadcast_image.dart deleted file mode 100644 index 223de8bcf3..0000000000 --- a/lib/src/view/broadcast/default_broadcast_image.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; - -class DefaultBroadcastImage extends StatelessWidget { - final double? width; - final double aspectRatio; - - const DefaultBroadcastImage({ - super.key, - this.width, - this.aspectRatio = 2.0, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: width, - child: AspectRatio( - aspectRatio: aspectRatio, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - LichessColors.primary.withOpacity(0.7), - LichessColors.brag.withOpacity(0.7), - ], - ), - ), - child: LayoutBuilder( - builder: (context, constraints) => Icon( - LichessIcons.radio_tower_lichess, - color: Theme.of(context).colorScheme.onSurfaceVariant, - size: constraints.maxWidth / 4, - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart deleted file mode 100644 index 2254c53c8a..0000000000 --- a/lib/src/view/clock/clock_screen.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; -import 'package:lichess_mobile/src/utils/immersive_mode.dart'; -import 'package:lichess_mobile/src/view/clock/clock_settings.dart'; -import 'package:lichess_mobile/src/view/clock/clock_tile.dart'; - -class ClockScreen extends StatefulWidget { - const ClockScreen({super.key}); - - @override - State createState() => _ClockScreenState(); -} - -class _ClockScreenState extends State { - @override - Widget build(BuildContext context) { - return const ImmersiveModeWidget( - child: Scaffold(body: _Body()), - ); - } -} - -class _Body extends ConsumerWidget { - const _Body(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(clockControllerProvider); - - return SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: ClockTile( - playerType: ClockPlayerType.top, - clockState: state, - ), - ), - const ClockSettings(), - Expanded( - child: ClockTile( - playerType: ClockPlayerType.bottom, - clockState: state, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 022f6bdcd5..3b3e44723f 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -1,77 +1,92 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/play/time_control_modal.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -const _iconSize = 45.0; +const _iconSize = 38.0; class ClockSettings extends ConsumerWidget { - const ClockSettings({super.key}); + const ClockSettings({required this.orientation, super.key}); + + final Orientation orientation; @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.read(clockControllerProvider.notifier); - final buttonsEnabled = ref.watch( - clockControllerProvider - .select((value) => value.paused || value.currentPlayer == null), + final state = ref.watch(clockToolControllerProvider); + final buttonsEnabled = !state.started || state.paused; + + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((prefs) => prefs.isSoundEnabled), ); return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( + padding: + orientation == Orientation.portrait + ? const EdgeInsets.symmetric(vertical: 10.0) + : const EdgeInsets.symmetric(horizontal: 10.0), + child: (orientation == Orientation.portrait ? Row.new : Column.new)( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const _PlayResumeButton(), - PlatformIconButton( - semanticsLabel: context.l10n.reset, + const _PlayResumeButton(_iconSize), + IconButton( + tooltip: context.l10n.reset, iconSize: _iconSize, - onTap: buttonsEnabled ? () => controller.reset() : null, - icon: Icons.cached, + onPressed: + buttonsEnabled + ? () { + ref.read(clockToolControllerProvider.notifier).reset(); + } + : null, + icon: const Icon(Icons.refresh), ), - PlatformIconButton( - semanticsLabel: context.l10n.settingsSettings, + IconButton( + tooltip: context.l10n.settingsSettings, iconSize: _iconSize, - onTap: buttonsEnabled - ? () { - final double screenHeight = - MediaQuery.sizeOf(context).height; - showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - constraints: BoxConstraints( - maxHeight: screenHeight - (screenHeight / 10), - ), - builder: (BuildContext context) { - final options = ref.watch( - clockControllerProvider - .select((value) => value.options), - ); - return TimeControlModal( - excludeUltraBullet: true, - value: TimeIncrement( - options.time.inSeconds, - options.increment.inSeconds, - ), - onSelected: (choice) { - controller.updateOptions(choice); - }, - ); - }, - ); - } - : null, - icon: Icons.settings, + onPressed: + buttonsEnabled + ? () { + final double screenHeight = MediaQuery.sizeOf(context).height; + showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), + builder: (BuildContext context) { + final options = ref.watch( + clockToolControllerProvider.select((value) => value.options), + ); + return TimeControlModal( + excludeUltraBullet: true, + value: TimeIncrement( + options.whiteTime.inSeconds, + options.whiteIncrement.inSeconds, + ), + onSelected: (choice) { + ref.read(clockToolControllerProvider.notifier).updateOptions(choice); + }, + ); + }, + ); + } + : null, + icon: const Icon(Icons.settings), ), - PlatformIconButton( - semanticsLabel: context.l10n.close, + IconButton( iconSize: _iconSize, - onTap: buttonsEnabled ? () => Navigator.of(context).pop() : null, - icon: Icons.home, + // TODO: translate + tooltip: 'Toggle sound', + onPressed: () => ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), + icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), + ), + IconButton( + tooltip: context.l10n.close, + iconSize: _iconSize, + onPressed: buttonsEnabled ? () => Navigator.of(context).pop() : null, + icon: const Icon(Icons.home), ), ], ), @@ -80,25 +95,38 @@ class ClockSettings extends ConsumerWidget { } class _PlayResumeButton extends ConsumerWidget { - const _PlayResumeButton(); + const _PlayResumeButton(this.iconSize); + + final double iconSize; @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.read(clockControllerProvider.notifier); - final state = ref.watch(clockControllerProvider); + final controller = ref.read(clockToolControllerProvider.notifier); + final state = ref.watch(clockToolControllerProvider); + + if (!state.started) { + return IconButton( + tooltip: context.l10n.play, + iconSize: iconSize, + onPressed: () => controller.start(), + icon: const Icon(Icons.play_arrow), + ); + } + if (state.paused) { - return PlatformIconButton( - semanticsLabel: context.l10n.resume, - iconSize: 35, - onTap: () => controller.resume(), - icon: Icons.play_arrow, + return IconButton( + tooltip: context.l10n.resume, + iconSize: iconSize, + onPressed: () => controller.resume(), + icon: const Icon(Icons.play_arrow), ); } - return PlatformIconButton( - semanticsLabel: context.l10n.pause, - iconSize: 35, - onTap: state.currentPlayer != null ? () => controller.pause() : null, - icon: Icons.pause, + + return IconButton( + tooltip: context.l10n.pause, + iconSize: iconSize, + onPressed: () => controller.pause(), + icon: const Icon(Icons.pause), ); } } diff --git a/lib/src/view/clock/clock_tile.dart b/lib/src/view/clock/clock_tile.dart deleted file mode 100644 index 6f8d582823..0000000000 --- a/lib/src/view/clock/clock_tile.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; - -const _darkClockStyle = ClockStyle( - textColor: Colors.black87, - activeTextColor: Colors.white, - emergencyTextColor: Colors.white, - backgroundColor: Colors.transparent, - activeBackgroundColor: Colors.transparent, - emergencyBackgroundColor: Color(0xFF673431), -); - -const _lightClockStyle = ClockStyle( - textColor: Colors.black87, - activeTextColor: Colors.white, - emergencyTextColor: Colors.black, - backgroundColor: Colors.transparent, - activeBackgroundColor: Colors.transparent, - emergencyBackgroundColor: Color(0xFFF2CCCC), -); - -class ClockTile extends ConsumerWidget { - final ClockPlayerType playerType; - final ClockState clockState; - - const ClockTile({ - required this.playerType, - required this.clockState, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - Color getBackgroundColor() { - if (clockState.isLoser(playerType)) { - return LichessColors.red; - } else if (clockState.isPlayersTurn(playerType) && - clockState.currentPlayer != null) { - return LichessColors.brag; - } else { - return Colors.grey; - } - } - - return PopScope( - canPop: false, - child: RotatedBox( - quarterTurns: playerType == ClockPlayerType.top ? 2 : 0, - child: Stack( - fit: StackFit.expand, - children: [ - Opacity( - opacity: clockState.paused ? 0.7 : 1, - child: Material( - color: getBackgroundColor(), - child: InkWell( - splashFactory: NoSplash.splashFactory, - onTap: clockState.isPlayersMoveAllowed(playerType) - ? () { - ref - .read(clockControllerProvider.notifier) - .onTap(playerType); - } - : null, - child: Padding( - padding: const EdgeInsets.all(40), - child: FittedBox( - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - firstChild: CountdownClock( - key: Key('${clockState.id}-$playerType'), - lightColorStyle: _lightClockStyle, - darkColorStyle: _darkClockStyle, - duration: clockState.getDuration(playerType), - active: clockState.isActivePlayer(playerType), - onFlag: () { - ref - .read(clockControllerProvider.notifier) - .setLoser(playerType); - }, - onStop: (remaining) { - ref - .read(clockControllerProvider.notifier) - .updateDuration(playerType, remaining); - }, - ), - secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isLoser(playerType) - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - ), - ), - ), - ), - ), - ), - Positioned( - bottom: 24, - right: 24, - child: Text( - '${context.l10n.stormMoves}: ${clockState.getMovesCount(playerType)}', - style: const TextStyle(fontSize: 13, color: Colors.black), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/view/clock/clock_tool_screen.dart b/lib/src/view/clock/clock_tool_screen.dart new file mode 100644 index 0000000000..be5b873708 --- /dev/null +++ b/lib/src/view/clock/clock_tool_screen.dart @@ -0,0 +1,219 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/immersive_mode.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/clock/clock_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; + +import 'custom_clock_settings.dart'; + +class ClockToolScreen extends StatelessWidget { + const ClockToolScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const ImmersiveModeWidget( + child: PopScope(canPop: false, child: Scaffold(body: _Body())), + ); + } +} + +enum TilePosition { bottom, top } + +class _Body extends ConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(clockToolControllerProvider); + + return OrientationBuilder( + builder: (context, orientation) { + return (orientation == Orientation.portrait ? Column.new : Row.new)( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ClockTile( + position: TilePosition.top, + orientation: orientation, + playerType: state.bottomPlayer.opposite, + clockState: state, + ), + ), + ClockSettings(orientation: orientation), + Expanded( + child: ClockTile( + position: TilePosition.bottom, + orientation: orientation, + playerType: state.bottomPlayer, + clockState: state, + ), + ), + ], + ); + }, + ); + } +} + +class ClockTile extends ConsumerWidget { + const ClockTile({ + required this.position, + required this.playerType, + required this.clockState, + required this.orientation, + super.key, + }); + + final TilePosition position; + final Side playerType; + final ClockState clockState; + final Orientation orientation; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final backgroundColor = + clockState.isFlagged(playerType) + ? context.lichessColors.error + : !clockState.paused && clockState.isPlayersTurn(playerType) + ? colorScheme.primary + : clockState.activeSide == playerType + ? colorScheme.secondaryContainer + : colorScheme.surfaceContainer; + + final clockStyle = ClockStyle( + textColor: + clockState.activeSide == playerType + ? colorScheme.onSecondaryContainer + : colorScheme.onSurface, + activeTextColor: colorScheme.onPrimary, + emergencyTextColor: Colors.white, + backgroundColor: Colors.transparent, + activeBackgroundColor: Colors.transparent, + emergencyBackgroundColor: const Color(0xFF673431), + ); + + return RotatedBox( + quarterTurns: orientation == Orientation.portrait && position == TilePosition.top ? 2 : 0, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + Material( + color: backgroundColor, + child: InkWell( + splashFactory: NoSplash.splashFactory, + onTap: + !clockState.started + ? () { + ref + .read(clockToolControllerProvider.notifier) + .setBottomPlayer( + position == TilePosition.bottom ? Side.white : Side.black, + ); + } + : null, + onTapDown: + clockState.started && clockState.isPlayersMoveAllowed(playerType) + ? (_) { + ref.read(clockToolControllerProvider.notifier).onTap(playerType); + } + : null, + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FittedBox( + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + firstChild: ValueListenableBuilder( + valueListenable: clockState.getDuration(playerType), + builder: (context, value, _) { + return Clock( + padLeft: true, + clockStyle: clockStyle, + timeLeft: value, + active: clockState.isActivePlayer(playerType), + ); + }, + ), + secondChild: const Icon(Icons.flag), + crossFadeState: + clockState.isFlagged(playerType) + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + ), + ), + ], + ), + ), + ), + ), + Positioned( + top: 24, + right: 24, + child: Text( + '${context.l10n.stormMoves}: ${clockState.getMovesCount(playerType)}', + style: TextStyle( + fontSize: 13, + color: + !clockState.paused && clockState.isPlayersTurn(playerType) + ? clockStyle.activeTextColor + : clockStyle.textColor, + ), + ), + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 48.0, + child: AnimatedOpacity( + opacity: clockState.started ? 0 : 1.0, + duration: const Duration(milliseconds: 300), + child: PlatformIconButton( + semanticsLabel: context.l10n.settingsSettings, + iconSize: 32, + icon: Icons.tune, + color: clockStyle.textColor, + onTap: + clockState.started + ? null + : () => showAdaptiveBottomSheet( + context: context, + builder: + (BuildContext context) => CustomClockSettings( + player: playerType, + clock: + playerType == Side.white + ? TimeIncrement.fromDurations( + clockState.options.whiteTime, + clockState.options.whiteIncrement, + ) + : TimeIncrement.fromDurations( + clockState.options.blackTime, + clockState.options.blackIncrement, + ), + onSubmit: (Side player, TimeIncrement clock) { + Navigator.of(context).pop(); + ref + .read(clockToolControllerProvider.notifier) + .updateOptionsCustom(clock, player); + }, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart new file mode 100644 index 0000000000..b1051d5732 --- /dev/null +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -0,0 +1,128 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; + +class CustomClockSettings extends StatefulWidget { + const CustomClockSettings({required this.onSubmit, required this.player, required this.clock}); + + final Side player; + final TimeIncrement clock; + final void Function(Side player, TimeIncrement clock) onSubmit; + + @override + State createState() => _CustomClockSettingsState(); +} + +class _CustomClockSettingsState extends State { + late int time; + late int increment; + + @override + void initState() { + time = widget.clock.time; + increment = widget.clock.increment; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + _PlayerTimeSlider( + clock: TimeIncrement(time, increment), + updateTime: (int t) => setState(() => time = t), + updateIncrement: (int inc) => setState(() => increment = inc), + ), + Padding( + padding: Styles.horizontalBodyPadding, + child: FatButton( + semanticsLabel: context.l10n.apply, + child: Text(context.l10n.apply), + onPressed: () => widget.onSubmit(widget.player, TimeIncrement(time, increment)), + ), + ), + ], + ); + } +} + +class _PlayerTimeSlider extends StatelessWidget { + const _PlayerTimeSlider({ + required this.clock, + required this.updateTime, + required this.updateIncrement, + }); + + final TimeIncrement clock; + final void Function(int time) updateTime; + final void Function(int time) updateIncrement; + + @override + Widget build(BuildContext context) { + return ListSection( + children: [ + PlatformListTile( + title: Text( + '${context.l10n.time}: ${clock.time < 60 ? context.l10n.nbSeconds(clock.time) : context.l10n.nbMinutes(_secToMin(clock.time))}', + ), + subtitle: NonLinearSlider( + value: clock.time, + values: kAvailableTimesInSeconds, + labelBuilder: _clockTimeLabel, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateTime(value.toInt()); + } + : null, + onChangeEnd: (num value) { + updateTime(value.toInt()); + }, + ), + ), + PlatformListTile( + title: Text('${context.l10n.increment}: ${context.l10n.nbSeconds(clock.increment)}'), + subtitle: NonLinearSlider( + value: clock.increment, + values: kAvailableIncrementsInSeconds, + labelBuilder: (num sec) => sec.toString(), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateIncrement(value.toInt()); + } + : null, + onChangeEnd: (num value) { + updateIncrement(value.toInt()); + }, + ), + ), + ], + ); + } +} + +int _secToMin(num sec) => sec ~/ 60; + +String _clockTimeLabel(num seconds) { + switch (seconds) { + case 0: + return '0'; + case 45: + return '¾'; + case 30: + return '½'; + case 15: + return '¼'; + default: + return _secToMin(seconds).toString(); + } +} diff --git a/lib/src/view/coordinate_training/coordinate_display.dart b/lib/src/view/coordinate_training/coordinate_display.dart new file mode 100644 index 0000000000..b9b9ede150 --- /dev/null +++ b/lib/src/view/coordinate_training/coordinate_display.dart @@ -0,0 +1,113 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; + +const Offset _kNextCoordFractionalTranslation = Offset(0.8, 0.3); +const double _kNextCoordScale = 0.4; + +const double _kCurrCoordOpacity = 0.9; +const double _kNextCoordOpacity = 0.7; + +class CoordinateDisplay extends ConsumerStatefulWidget { + const CoordinateDisplay({required this.currentCoord, required this.nextCoord}); + + final Square currentCoord; + + final Square nextCoord; + + @override + ConsumerState createState() => CoordinateDisplayState(); +} + +class CoordinateDisplayState extends ConsumerState + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + )..value = 1.0; + + late final Animation _scaleAnimation = Tween( + begin: _kNextCoordScale, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); + + late final Animation _currCoordSlideInAnimation = Tween( + begin: _kNextCoordFractionalTranslation, + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); + + late final Animation _nextCoordSlideInAnimation = Tween( + begin: const Offset(0.5, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); + + late final Animation _currCoordOpacityAnimation = Tween( + begin: _kNextCoordOpacity, + end: _kCurrCoordOpacity, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); + + late final Animation _nextCoordFadeInAnimation = Tween( + begin: 0.0, + end: _kNextCoordOpacity, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + @override + Widget build(BuildContext context) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + final textStyle = DefaultTextStyle.of(context).style.copyWith( + fontSize: 110.0, + fontFamily: 'monospace', + color: Colors.white.withValues(alpha: 0.9), + fontWeight: FontWeight.bold, + fontFeatures: [const FontFeature.tabularFigures()], + shadows: const [Shadow(color: Colors.black, offset: Offset(0, 5), blurRadius: 40.0)], + ); + + return IgnorePointer( + child: Stack( + children: [ + FadeTransition( + opacity: _currCoordOpacityAnimation, + child: SlideTransition( + position: _currCoordSlideInAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Text(trainingState.currentCoord?.name ?? '', style: textStyle), + ), + ), + ), + FadeTransition( + opacity: _nextCoordFadeInAnimation, + child: SlideTransition( + position: _nextCoordSlideInAnimation, + child: FractionalTranslation( + translation: _kNextCoordFractionalTranslation, + child: Transform.scale( + scale: _kNextCoordScale, + child: Text(trainingState.nextCoord?.name ?? '', style: textStyle), + ), + ), + ), + ), + ], + ), + ); + } + + @override + void didUpdateWidget(covariant CoordinateDisplay oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.nextCoord != widget.nextCoord) { + _controller.forward(from: 0.0); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart new file mode 100644 index 0000000000..f4d8e65b44 --- /dev/null +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -0,0 +1,523 @@ +import 'dart:async'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_display.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/filter.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class CoordinateTrainingScreen extends StatelessWidget { + const CoordinateTrainingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: const Text('Coordinate Training'), // TODO l10n once script works + actions: [ + AppBarIconButton( + icon: const Icon(Icons.settings), + semanticsLabel: context.l10n.settingsSettings, + onPressed: + () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => const _CoordinateTrainingMenu(), + ), + ), + ], + ), + body: const _Body(), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + Square? highlightLastGuess; + + Timer? highlightTimer; + + @override + void dispose() { + super.dispose(); + highlightTimer?.cancel(); + } + + @override + Widget build(BuildContext context) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + final IMap squareHighlights = + { + if (trainingState.trainingActive) + if (trainingPrefs.mode == TrainingMode.findSquare) ...{ + if (highlightLastGuess != null) ...{ + highlightLastGuess!: SquareHighlight( + details: HighlightDetails( + solidColor: (trainingState.lastGuess == Guess.correct + ? context.lichessColors.good + : context.lichessColors.error) + .withValues(alpha: 0.5), + ), + ), + }, + } else ...{ + trainingState.currentCoord!: SquareHighlight( + details: HighlightDetails( + solidColor: context.lichessColors.good.withValues(alpha: 0.5), + ), + ), + }, + }.lock; + + return SafeArea( + bottom: false, + child: Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final direction = aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + + return Flex( + direction: direction, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + _TimeBar( + maxWidth: boardSize, + timeFractionElapsed: trainingState.timeFractionElapsed, + color: + trainingState.lastGuess == Guess.incorrect + ? context.lichessColors.error + : context.lichessColors.good, + ), + _TrainingBoard( + boardSize: boardSize, + isTablet: isTablet, + orientation: trainingState.orientation, + squareHighlights: squareHighlights, + onGuess: _onGuess, + ), + ], + ), + if (trainingState.trainingActive) + _ScoreAndTrainingButton( + scoreSize: boardSize / 8, + score: trainingState.score, + onPressed: + ref.read(coordinateTrainingControllerProvider.notifier).abortTraining, + label: 'Abort Training', + ) + else if (trainingState.lastScore != null) + _ScoreAndTrainingButton( + scoreSize: boardSize / 8, + score: trainingState.lastScore!, + onPressed: () { + ref + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration); + }, + label: 'New Training', + ) + else + Expanded( + child: Center( + child: _Button( + onPressed: () { + ref + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration); + }, + label: 'Start Training', + ), + ), + ), + ], + ); + }, + ), + ), + if (!trainingState.trainingActive) + BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () => _coordinateTrainingSettingsBuilder(context), + icon: Icons.tune, + ), + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Coordinate Training'), + onTap: () => _coordinateTrainingInfoDialogBuilder(context), + ), + ], + ), + ], + ), + ); + } + + void _onGuess(Square square) { + ref.read(coordinateTrainingControllerProvider.notifier).guessCoordinate(square); + + setState(() { + highlightLastGuess = square; + + highlightTimer?.cancel(); + highlightTimer = Timer(const Duration(milliseconds: 200), () { + setState(() { + highlightLastGuess = null; + }); + }); + }); + } +} + +class _TimeBar extends StatelessWidget { + const _TimeBar({required this.maxWidth, required this.timeFractionElapsed, required this.color}); + + final double maxWidth; + final double? timeFractionElapsed; + final Color color; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: maxWidth * (timeFractionElapsed ?? 0.0), + height: 15.0, + child: ColoredBox(color: color), + ), + ); + } +} + +class _CoordinateTrainingMenu extends ConsumerWidget { + const _CoordinateTrainingMenu(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(context.l10n.preferencesDisplay, style: Styles.sectionTitle), + ), + SwitchSettingTile( + title: const Text('Show Coordinates'), + value: trainingPrefs.showCoordinates, + onChanged: + ref.read(coordinateTrainingPreferencesProvider.notifier).setShowCoordinates, + ), + SwitchSettingTile( + title: const Text('Show Pieces'), + value: trainingPrefs.showPieces, + onChanged: ref.read(coordinateTrainingPreferencesProvider.notifier).setShowPieces, + ), + ], + ), + ], + ); + } +} + +class _ScoreAndTrainingButton extends ConsumerWidget { + const _ScoreAndTrainingButton({ + required this.scoreSize, + required this.score, + required this.onPressed, + required this.label, + }); + + final double scoreSize; + final int score; + final VoidCallback onPressed; + final String label; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _Score( + score: score, + size: scoreSize, + color: + trainingState.lastGuess == Guess.incorrect + ? context.lichessColors.error + : context.lichessColors.good, + ), + _Button(label: label, onPressed: onPressed), + ], + ), + ); + } +} + +class _Score extends StatelessWidget { + const _Score({required this.size, required this.color, required this.score}); + + final int score; + + final double size; + + final Color color; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + color: color, + ), + width: size, + height: size, + child: Center( + child: Text( + score.toString(), + style: Styles.bold.copyWith(color: Colors.white, fontSize: 24.0), + ), + ), + ), + ); + } +} + +class _Button extends StatelessWidget { + const _Button({required this.onPressed, required this.label}); + + final VoidCallback onPressed; + final String label; + + @override + Widget build(BuildContext context) { + return FatButton( + semanticsLabel: label, + onPressed: onPressed, + child: Text(label, style: Styles.bold), + ); + } +} + +class SettingsBottomSheet extends ConsumerWidget { + const SettingsBottomSheet(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: SideChoice.values, + showCheckmark: false, + choiceSelected: (choice) => trainingPrefs.sideChoice == choice, + choiceLabel: (choice) => Text(choice.label(context.l10n)), + onSelected: (choice, selected) { + if (selected) { + ref.read(coordinateTrainingPreferencesProvider.notifier).setSideChoice(choice); + } + }, + ), + const SizedBox(height: 12.0), + const PlatformDivider(thickness: 1, indent: 0), + const SizedBox(height: 12.0), + Filter( + filterName: context.l10n.time, + filterType: FilterType.singleChoice, + choices: TimeChoice.values, + showCheckmark: false, + choiceSelected: (choice) => trainingPrefs.timeChoice == choice, + choiceLabel: (choice) => choice.label(context.l10n), + onSelected: (choice, selected) { + if (selected) { + ref.read(coordinateTrainingPreferencesProvider.notifier).setTimeChoice(choice); + } + }, + ), + ], + ); + } +} + +class _TrainingBoard extends ConsumerStatefulWidget { + const _TrainingBoard({ + required this.boardSize, + required this.isTablet, + required this.orientation, + required this.onGuess, + required this.squareHighlights, + }); + + final double boardSize; + + final bool isTablet; + + final Side orientation; + + final void Function(Square) onGuess; + + final IMap squareHighlights; + + @override + ConsumerState<_TrainingBoard> createState() => _TrainingBoardState(); +} + +class _TrainingBoardState extends ConsumerState<_TrainingBoard> { + @override + Widget build(BuildContext context) { + final boardPrefs = ref.watch(boardPreferencesProvider); + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + ChessboardEditor( + size: widget.boardSize, + pieces: readFen(trainingPrefs.showPieces ? kInitialFEN : kEmptyFEN), + squareHighlights: widget.squareHighlights, + orientation: widget.orientation, + settings: boardPrefs.toBoardSettings().copyWith( + enableCoordinates: trainingPrefs.showCoordinates, + borderRadius: + widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + ), + pointerMode: EditorPointerMode.edit, + onEditedSquare: (square) { + if (trainingState.trainingActive && trainingPrefs.mode == TrainingMode.findSquare) { + widget.onGuess(square); + } + }, + ), + if (trainingState.trainingActive && trainingPrefs.mode == TrainingMode.findSquare) + CoordinateDisplay( + currentCoord: trainingState.currentCoord!, + nextCoord: trainingState.nextCoord!, + ), + ], + ), + ], + ); + } +} + +Future _coordinateTrainingSettingsBuilder(BuildContext context) { + return showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => const SettingsBottomSheet(), + ); +} + +Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { + return showAdaptiveDialog( + context: context, + builder: (context) { + final content = SingleChildScrollView( + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + // TODO translate + children: const [ + TextSpan( + text: + 'Knowing the chessboard coordinates is a very important skill for several reasons:\n', + ), + TextSpan( + text: + ' • Most chess courses and exercises use the algebraic notation extensively.\n', + ), + TextSpan( + text: + " • It makes it easier to talk to your chess friends, since you both understand the 'language of chess'.\n", + ), + TextSpan( + text: + ' • You can analyse a game more effectively if you can quickly recognise coordinates.\n', + ), + TextSpan(text: '\n'), + TextSpan(text: 'Find Square\n', style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: + 'A coordinate appears on the board and you must click on the corresponding square.\n', + ), + TextSpan(text: 'You have 30 seconds to correctly map as many squares as possible!\n'), + ], + ), + ), + ); + + return PlatformAlertDialog( + title: Text(context.l10n.aboutX('Coordinate Training')), + content: content, + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ); + }, + ); +} diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 85da1930ff..472a6ed00b 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -1,10 +1,9 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; @@ -15,7 +14,6 @@ import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -23,25 +21,21 @@ import 'package:lichess_mobile/src/view/game/correspondence_clock_widget.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class OfflineCorrespondenceGameScreen extends StatefulWidget { - const OfflineCorrespondenceGameScreen({ - required this.initialGame, - super.key, - }); + const OfflineCorrespondenceGameScreen({required this.initialGame, super.key}); final (DateTime, OfflineCorrespondenceGame) initialGame; @override - State createState() => - _OfflineCorrespondenceGameScreenState(); + State createState() => _OfflineCorrespondenceGameScreenState(); } -class _OfflineCorrespondenceGameScreenState - extends State { +class _OfflineCorrespondenceGameScreenState extends State { late (DateTime, OfflineCorrespondenceGame) currentGame; @override @@ -58,35 +52,10 @@ class _OfflineCorrespondenceGameScreenState @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { final (lastModified, game) = currentGame; - return Scaffold( - appBar: AppBar(title: _Title(game)), - body: _Body( - game: game, - lastModified: lastModified, - onGameChanged: goToNextGame, - ), - ); - } - - Widget _iosBuilder(BuildContext context) { - final (lastModified, game) = currentGame; - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: _Title(game), - ), - child: _Body( - game: game, - lastModified: lastModified, - onGameChanged: goToNextGame, - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: _Title(game)), + body: _Body(game: game, lastModified: lastModified, onGameChanged: goToNextGame), ); } } @@ -97,20 +66,14 @@ class _Title extends StatelessWidget { @override Widget build(BuildContext context) { - final mode = - game.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + final mode = game.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - game.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(game.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (game.daysPerTurn != null) - Text( - '${context.l10n.nbDays(game.daysPerTurn!)}$mode', - ) + Text('${context.l10n.nbDays(game.daysPerTurn!)}$mode') else Text('∞$mode'), ], @@ -119,11 +82,7 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.game, - required this.lastModified, - required this.onGameChanged, - }); + const _Body({required this.game, required this.lastModified, required this.onGameChanged}); final OfflineCorrespondenceGame game; final DateTime lastModified; @@ -138,6 +97,7 @@ class _BodyState extends ConsumerState<_Body> { int stepCursor = 0; (String, Move)? moveToConfirm; bool isBoardTurned = false; + NormalMove? promotionMove; bool get isReplaying => stepCursor < game.steps.length - 1; bool get canGoForward => stepCursor < game.steps.length - 1; @@ -163,14 +123,11 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { - final shouldShowMaterialDiff = ref.watch( - boardPreferencesProvider.select( - (prefs) => prefs.showMaterialDifference, - ), + final materialDifference = ref.watch( + boardPreferencesProvider.select((prefs) => prefs.materialDifferenceFormat), ); - final offlineOngoingGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineOngoingGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); final position = game.positionAt(stepCursor); final sideToMove = position.turn; @@ -178,46 +135,39 @@ class _BodyState extends ConsumerState<_Body> { final black = GamePlayer( player: game.black, - materialDiff: shouldShowMaterialDiff - ? game.materialDiffAt(stepCursor, Side.black) - : null, + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.black, - confirmMoveCallbacks: youAre == Side.black && moveToConfirm != null - ? ( - confirm: confirmMove, - cancel: cancelMove, - ) - : null, - clock: youAre == Side.black && - game.estimatedTimeLeft(Side.black, widget.lastModified) != null - ? CorrespondenceClock( - duration: - game.estimatedTimeLeft(Side.black, widget.lastModified)!, - active: activeClockSide == Side.black, - ) - : null, + confirmMoveCallbacks: + youAre == Side.black && moveToConfirm != null + ? (confirm: confirmMove, cancel: cancelMove) + : null, + clock: + youAre == Side.black && game.estimatedTimeLeft(Side.black, widget.lastModified) != null + ? CorrespondenceClock( + duration: game.estimatedTimeLeft(Side.black, widget.lastModified)!, + active: activeClockSide == Side.black, + ) + : null, ); final white = GamePlayer( player: game.white, - materialDiff: shouldShowMaterialDiff - ? game.materialDiffAt(stepCursor, Side.white) - : null, + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.white, - confirmMoveCallbacks: youAre == Side.white && moveToConfirm != null - ? ( - confirm: confirmMove, - cancel: cancelMove, - ) - : null, - clock: game.estimatedTimeLeft(Side.white, widget.lastModified) != null - ? CorrespondenceClock( - duration: - game.estimatedTimeLeft(Side.white, widget.lastModified)!, - active: activeClockSide == Side.white, - ) - : null, + confirmMoveCallbacks: + youAre == Side.white && moveToConfirm != null + ? (confirm: confirmMove, cancel: cancelMove) + : null, + clock: + game.estimatedTimeLeft(Side.white, widget.lastModified) != null + ? CorrespondenceClock( + duration: game.estimatedTimeLeft(Side.white, widget.lastModified)!, + active: activeClockSide == Side.white, + ) + : null, ); final topPlayer = youAre == Side.white ? black : white; @@ -229,28 +179,28 @@ class _BodyState extends ConsumerState<_Body> { child: SafeArea( bottom: false, child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - onUserMove(Move.fromUci(move.uci)!); - }, - boardData: cg.BoardData( - interactableSide: game.playable && !isReplaying - ? youAre == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black - : cg.InteractableSide.none, - orientation: isBoardTurned ? youAre.opposite.cg : youAre.cg, - fen: position.fen, - lastMove: game.moveAt(stepCursor)?.cg, + orientation: isBoardTurned ? youAre.opposite : youAre, + fen: position.fen, + lastMove: game.moveAt(stepCursor) as NormalMove?, + gameData: GameData( + playerSide: + game.playable && !isReplaying + ? youAre == Side.white + ? PlayerSide.white + : PlayerSide.black + : PlayerSide.none, isCheck: position.isCheck, - sideToMove: sideToMove.cg, - validMoves: algebraicLegalMoves(position), + sideToMove: sideToMove, + validMoves: makeLegalMoves(position, isChess960: game.variant == Variant.chess960), + promotionMove: promotionMove, + onMove: (move, {isDrop, captured}) { + onUserMove(move); + }, + onPromotionSelection: onPromotionSelection, ), topTable: topPlayer, bottomTable: bottomPlayer, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: stepCursor, onSelectMove: (moveIndex) { // ref.read(ctrlProvider.notifier).cursorAt(moveIndex); @@ -258,115 +208,91 @@ class _BodyState extends ConsumerState<_Body> { ), ), ), - Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.flipBoard, - onTap: () { - setState(() { - isBoardTurned = !isBoardTurned; - }); - }, - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.analysis, - onTap: () { - pushPlatformRoute( - context, - builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: game.variant, - initialMoveCursor: stepCursor, - orientation: game.youAre, - id: game.id, - division: game.meta.division, - ), - title: context.l10n.analysis, + BottomBar( + children: [ + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () { + setState(() { + isBoardTurned = !isBoardTurned; + }); + }, + icon: CupertinoIcons.arrow_2_squarepath, + ), + BottomBarButton( + label: context.l10n.analysis, + onTap: () { + pushPlatformRoute( + context, + builder: + (_) => AnalysisScreen( + options: AnalysisOptions( + orientation: game.youAre, + standalone: ( + pgn: game.makePgn(), + isComputerAnalysisAllowed: false, + variant: game.variant, ), - ); - }, - icon: Icons.biotech, - ), - ), - Expanded( - child: BottomBarButton( - label: 'Go to the next game', - icon: Icons.skip_next, - onTap: offlineOngoingGames.maybeWhen( - data: (games) { - final nextTurn = games - .whereNot((g) => g.$2.id == game.id) - .firstWhereOrNull((g) => g.$2.isPlayerTurn); - return nextTurn != null - ? () { - widget.onGameChanged(nextTurn); - } - : null; - }, - orElse: () => null, + initialMoveCursor: stepCursor, + ), ), - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.mobileCorrespondenceClearSavedMove, - onTap: game.registeredMoveAtPgn != null - ? () { - showConfirmDialog( - context, - title: Text( - context - .l10n.mobileCorrespondenceClearSavedMove, - ), - isDestructiveAction: true, - onConfirm: (_) => deleteRegisteredMove(), - ); - } - : null, - icon: Icons.save, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBackward ? () => moveBackward() : null, - child: BottomBarButton( - onTap: canGoBackward ? () => moveBackward() : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoForward ? () => moveForward() : null, - child: BottomBarButton( - onTap: canGoForward ? () => moveForward() : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, - ), - ), - ), - ], + ); + }, + icon: Icons.biotech, + ), + BottomBarButton( + label: 'Go to the next game', + icon: Icons.skip_next, + onTap: offlineOngoingGames.maybeWhen( + data: (games) { + final nextTurn = games + .whereNot((g) => g.$2.id == game.id) + .firstWhereOrNull((g) => g.$2.isMyTurn); + return nextTurn != null + ? () { + widget.onGameChanged(nextTurn); + } + : null; + }, + orElse: () => null, ), ), - ), + BottomBarButton( + label: context.l10n.mobileCorrespondenceClearSavedMove, + onTap: + game.registeredMoveAtPgn != null + ? () { + showConfirmDialog( + context, + title: Text(context.l10n.mobileCorrespondenceClearSavedMove), + isDestructiveAction: true, + onConfirm: (_) => deleteRegisteredMove(), + ); + } + : null, + icon: Icons.save, + ), + RepeatButton( + onLongPress: canGoBackward ? () => moveBackward() : null, + child: BottomBarButton( + onTap: canGoBackward ? () => moveBackward() : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoForward ? () => moveForward() : null, + child: BottomBarButton( + onTap: canGoForward ? () => moveForward() : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, + ), + ), + ), + ], ), ], ); @@ -390,7 +316,14 @@ class _BodyState extends ConsumerState<_Body> { } } - void onUserMove(Move move) { + void onUserMove(NormalMove move) { + if (isPromotionPawnMove(game.lastPosition, move)) { + setState(() { + promotionMove = move; + }); + return; + } + final (newPos, newSan) = game.lastPosition.makeSan(move); final sanMove = SanMove(newSan, move); final newStep = GameStep( @@ -401,46 +334,53 @@ class _BodyState extends ConsumerState<_Body> { setState(() { moveToConfirm = (game.sanMoves, move); - game = game.copyWith( - steps: game.steps.add(newStep), - ); + game = game.copyWith(steps: game.steps.add(newStep)); + promotionMove = null; stepCursor = stepCursor + 1; }); _moveFeedback(sanMove); } - void confirmMove() { + void onPromotionSelection(Role? role) { + if (role == null) { + setState(() { + promotionMove = null; + }); + return; + } + if (promotionMove != null) { + final move = promotionMove!.withPromotion(role); + onUserMove(move); + } + } + + Future confirmMove() async { setState(() { - game = game.copyWith( - registeredMoveAtPgn: (moveToConfirm!.$1, moveToConfirm!.$2), - ); + game = game.copyWith(registeredMoveAtPgn: (moveToConfirm!.$1, moveToConfirm!.$2)); moveToConfirm = null; }); - ref.read(correspondenceGameStorageProvider).save(game); + final storage = await ref.read(correspondenceGameStorageProvider.future); + storage.save(game); } void cancelMove() { setState(() { moveToConfirm = null; stepCursor = stepCursor - 1; - game = game.copyWith( - steps: game.steps.removeLast(), - ); + game = game.copyWith(steps: game.steps.removeLast()); }); } - void deleteRegisteredMove() { + Future deleteRegisteredMove() async { setState(() { stepCursor = stepCursor - 1; - game = game.copyWith( - steps: game.steps.removeLast(), - registeredMoveAtPgn: null, - ); + game = game.copyWith(steps: game.steps.removeLast(), registeredMoveAtPgn: null); }); - ref.read(correspondenceGameStorageProvider).save(game); + final storage = await ref.read(correspondenceGameStorageProvider.future); + storage.save(game); } Side? get activeClockSide { diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart new file mode 100644 index 0000000000..e9a03a036d --- /dev/null +++ b/lib/src/view/engine/engine_depth.dart @@ -0,0 +1,106 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:popover/popover.dart'; + +class EngineDepth extends ConsumerWidget { + const EngineDepth({this.defaultEval}); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final depth = + ref.watch(engineEvaluationProvider.select((value) => value.eval?.depth)) ?? + defaultEval?.depth; + + return depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(defaultEval); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? DialogTheme.of(context).backgroundColor ?? + Theme.of(context).colorScheme.surfaceContainerHigh + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.defaultEval); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = ref.watch( + engineEvaluationProvider, + ); + + final currentEval = eval ?? defaultEval; + + final knps = engineState == EngineState.computing ? ', ${eval?.knps.round()}kn/s' : ''; + final depth = currentEval?.depth ?? 0; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset('assets/images/stockfish/icon.png', width: 44, height: 44), + title: Text(engineName), + subtitle: Text(context.l10n.depthX('$depth$knps')), + ), + ], + ); + } +} diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index 22f9050545..438cd880d2 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -2,47 +2,36 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -part 'engine_gauge.freezed.dart'; - -const double kEvalGaugeSize = 26.0; +const double kEvalGaugeSize = 24.0; const double kEvalGaugeFontSize = 11.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); -enum EngineGaugeDisplayMode { - vertical, - horizontal, -} +enum EngineGaugeDisplayMode { vertical, horizontal } -@freezed -class EngineGaugeParams with _$EngineGaugeParams { - const factory EngineGaugeParams({ - required bool isLocalEngineAvailable, +typedef EngineGaugeParams = + ({ + bool isLocalEngineAvailable, - /// Only used for vertical display mode. - required Side orientation, + /// Only used for vertical display mode. + Side orientation, - /// Position to evaluate. - required Position position, + /// Position to evaluate. + Position position, - /// Saved evaluation to display when the current evaluation is not available. - Eval? savedEval, - }) = _EngineGaugeParams; -} + /// Saved evaluation to display when the current evaluation is not available. + Eval? savedEval, + }); class EngineGauge extends ConsumerWidget { - const EngineGauge({ - required this.displayMode, - required this.params, - }); + const EngineGauge({required this.displayMode, required this.params}); final EngineGaugeDisplayMode displayMode; @@ -52,8 +41,8 @@ class EngineGauge extends ConsumerWidget { Theme.of(context).platform == TargetPlatform.iOS ? _kEvalGaugeBackgroundColor : brightness == Brightness.dark - ? lighten(Theme.of(context).colorScheme.surface, .07) - : lighten(Theme.of(context).colorScheme.onSurface, .17); + ? lighten(Theme.of(context).colorScheme.surface, .07) + : lighten(Theme.of(context).colorScheme.onSurface, .17); static Color valueColor(BuildContext context, Brightness brightness) => Theme.of(context).platform == TargetPlatform.iOS @@ -61,34 +50,33 @@ class EngineGauge extends ConsumerWidget { ? _kEvalGaugeValueColorDarkBg : _kEvalGaugeValueColorLightBg : brightness == Brightness.dark - ? darken(Theme.of(context).colorScheme.onSurface, .03) - : darken(Theme.of(context).colorScheme.surface, .01); + ? darken(Theme.of(context).colorScheme.onSurface, .03) + : darken(Theme.of(context).colorScheme.surface, .01); @override Widget build(BuildContext context, WidgetRef ref) { - final localEval = params.isLocalEngineAvailable - ? ref.watch(engineEvaluationProvider).eval - : null; + final localEval = + params.isLocalEngineAvailable ? ref.watch(engineEvaluationProvider).eval : null; return localEval != null ? _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - eval: localEval, - ) + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + eval: localEval, + ) : params.savedEval != null - ? _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - eval: params.savedEval, - ) - : _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - ); + ? _EvalGauge( + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + eval: params.savedEval, + ) + : _EvalGauge( + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + ); } } @@ -127,23 +115,25 @@ class _EvalGaugeState extends ConsumerState<_EvalGauge> { final brightness = ref.watch(currentBrightnessProvider); final TextDirection textDirection = Directionality.of(context); - final evalDisplay = widget.position.outcome != null - ? widget.position.outcome!.winner == null - ? widget.position.isStalemate - ? context.l10n.stalemate - : context.l10n.insufficientMaterial - : widget.position.isCheckmate + final evalDisplay = + widget.position.outcome != null + ? widget.position.outcome!.winner == null + ? widget.position.isStalemate + ? context.l10n.stalemate + : context.l10n.insufficientMaterial + : widget.position.isCheckmate ? context.l10n.checkmate : context.l10n.variantEnding - : widget.eval?.evalString; + : widget.eval?.evalString; - final toValue = widget.position.outcome != null - ? widget.position.outcome!.winner == null - ? 0.5 - : widget.position.outcome!.winner == Side.white + final toValue = + widget.position.outcome != null + ? widget.position.outcome!.winner == null + ? 0.5 + : widget.position.outcome!.winner == Side.white ? 1.0 : 0.0 - : widget.animationValue; + : widget.animationValue; return TweenAnimationBuilder( tween: Tween(begin: fromValue, end: toValue), @@ -155,56 +145,44 @@ class _EvalGaugeState extends ConsumerState<_EvalGauge> { value: evalDisplay ?? context.l10n.loadingEngine, child: RepaintBoundary( child: Container( - constraints: widget.displayMode == EngineGaugeDisplayMode.vertical - ? const BoxConstraints( - minWidth: kEvalGaugeSize, - minHeight: double.infinity, - ) - : const BoxConstraints( - minWidth: double.infinity, - minHeight: kEvalGaugeSize, - ), - width: widget.displayMode == EngineGaugeDisplayMode.vertical - ? kEvalGaugeSize - : null, - height: widget.displayMode == EngineGaugeDisplayMode.vertical - ? null - : kEvalGaugeSize, + constraints: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? const BoxConstraints(minWidth: kEvalGaugeSize, minHeight: double.infinity) + : const BoxConstraints(minWidth: double.infinity, minHeight: kEvalGaugeSize), + width: widget.displayMode == EngineGaugeDisplayMode.vertical ? kEvalGaugeSize : null, + height: widget.displayMode == EngineGaugeDisplayMode.vertical ? null : kEvalGaugeSize, child: CustomPaint( - painter: widget.displayMode == EngineGaugeDisplayMode.vertical - ? _EvalGaugeVerticalPainter( - orientation: widget.orientation, - backgroundColor: - EngineGauge.backgroundColor(context, brightness), - valueColor: EngineGauge.valueColor(context, brightness), - value: value, - ) - : _EvalGaugeHorizontalPainter( - backgroundColor: - EngineGauge.backgroundColor(context, brightness), - valueColor: EngineGauge.valueColor(context, brightness), - value: value, - textDirection: textDirection, - ), - child: widget.displayMode == EngineGaugeDisplayMode.vertical - ? const SizedBox.shrink() - : Align( - alignment: toValue >= 0.5 - ? Alignment.centerLeft - : Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - evalDisplay ?? '', - style: TextStyle( - color: - toValue >= 0.5 ? Colors.black : Colors.white, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.bold, + painter: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? _EvalGaugeVerticalPainter( + orientation: widget.orientation, + backgroundColor: EngineGauge.backgroundColor(context, brightness), + valueColor: EngineGauge.valueColor(context, brightness), + value: value, + ) + : _EvalGaugeHorizontalPainter( + backgroundColor: EngineGauge.backgroundColor(context, brightness), + valueColor: EngineGauge.valueColor(context, brightness), + value: value, + textDirection: textDirection, + ), + child: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? const SizedBox.shrink() + : Align( + alignment: toValue >= 0.5 ? Alignment.centerLeft : Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + evalDisplay ?? '', + style: TextStyle( + color: toValue >= 0.5 ? Colors.black : Colors.white, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.bold, + ), ), ), ), - ), ), ), ), @@ -229,9 +207,10 @@ class _EvalGaugeHorizontalPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill; + final Paint paint = + Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; canvas.drawRect(Offset.zero & size, paint); paint.color = valueColor; @@ -278,9 +257,10 @@ class _EvalGaugeVerticalPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill; + final Paint paint = + Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; canvas.drawRect(Offset.zero & size, paint); paint.color = valueColor; diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart new file mode 100644 index 0000000000..1e31077c34 --- /dev/null +++ b/lib/src/view/engine/engine_lines.dart @@ -0,0 +1,136 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +class EngineLines extends ConsumerWidget { + const EngineLines({required this.onTapMove, required this.clientEval, required this.isGameOver}); + final void Function(NormalMove move) onTapMove; + final ClientEval? clientEval; + final bool isGameOver; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final numEvalLines = ref.watch(analysisPreferencesProvider.select((p) => p.numEvalLines)); + final engineEval = ref.watch(engineEvaluationProvider).eval; + final eval = engineEval ?? clientEval; + + final emptyLines = List.filled(numEvalLines, const Engineline.empty()); + + final content = + isGameOver + ? emptyLines + : (eval != null + ? eval.pvs + .take(numEvalLines) + .map((pv) => Engineline(onTapMove, eval.position, pv)) + .toList() + : emptyLines); + + if (content.length < numEvalLines) { + final padding = List.filled(numEvalLines - content.length, const Engineline.empty()); + content.addAll(padding); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, + ); + } +} + +class Engineline extends ConsumerWidget { + const Engineline(this.onTapMove, this.fromPosition, this.pvData); + + const Engineline.empty() + : onTapMove = null, + pvData = const PvData(moves: IListConst([])), + fromPosition = Chess.initial; + + final void Function(NormalMove move)? onTapMove; + final Position fromPosition; + final PvData pvData; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (pvData.moves.isEmpty) { + return const SizedBox(height: kEvalGaugeSize, child: SizedBox.shrink()); + } + + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); + + final lineBuffer = StringBuffer(); + int ply = fromPosition.ply + 1; + pvData.sanMoves(fromPosition).forEachIndexed((i, s) { + lineBuffer.write( + ply.isOdd + ? '${(ply / 2).ceil()}. $s ' + : i == 0 + ? '${(ply / 2).ceil()}... $s ' + : '$s ', + ); + ply += 1; + }); + + final brightness = Theme.of(context).brightness; + + final evalString = pvData.evalString; + return AdaptiveInkWell( + onTap: () => onTapMove?.call(NormalMove.fromUci(pvData.moves[0])), + child: SizedBox( + height: kEvalGaugeSize, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: + pvData.winningSide == Side.black + ? EngineGauge.backgroundColor(context, brightness) + : EngineGauge.valueColor(context, brightness), + borderRadius: BorderRadius.circular(4.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), + child: Text( + evalString, + style: TextStyle( + color: pvData.winningSide == Side.black ? Colors.white : Colors.black, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + lineBuffer.toString(), + maxLines: 1, + softWrap: false, + style: TextStyle( + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 2e268741c7..a2b3acbb7c 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -1,13 +1,14 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -17,85 +18,110 @@ import 'package:lichess_mobile/src/view/game/game_result_dialog.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'archived_game_screen_providers.dart'; /// Screen for viewing an archived game. class ArchivedGameScreen extends ConsumerWidget { const ArchivedGameScreen({ - required this.gameData, + this.gameId, + this.gameData, required this.orientation, this.initialCursor, super.key, - }); + }) : assert(gameId != null || gameData != null); + + final LightArchivedGame? gameData; + final GameId? gameId; - final LightArchivedGame gameData; final Side orientation; final int? initialCursor; @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + if (gameData != null) { + return _Body(gameData: gameData, orientation: orientation, initialCursor: initialCursor); + } else { + return _LoadGame(gameId: gameId!, orientation: orientation, initialCursor: initialCursor); + } } +} - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: _GameTitle(gameData: gameData), - actions: [ - BookmarkButton( - id: gameData.id, - ), - ToggleSoundButton(), - ], - ), - body: _BoardBody( - gameData: gameData, - orientation: orientation, - initialCursor: initialCursor, - ), - bottomNavigationBar: - _BottomBar(gameData: gameData, orientation: orientation), +class _LoadGame extends ConsumerWidget { + const _LoadGame({required this.gameId, required this.orientation, required this.initialCursor}); + + final GameId gameId; + final Side orientation; + final int? initialCursor; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final game = ref.watch(archivedGameProvider(id: gameId)); + return game.when( + data: (game) { + return _Body(gameData: game.data, orientation: orientation, initialCursor: initialCursor); + }, + loading: () => _Body(gameData: null, orientation: orientation, initialCursor: initialCursor), + error: (error, stackTrace) { + debugPrint('SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace'); + switch (error) { + case ServerException _ when error.statusCode == 404: + return _Body( + gameData: null, + orientation: orientation, + initialCursor: initialCursor, + error: 'Game not found.', + ); + default: + return _Body( + gameData: null, + orientation: orientation, + initialCursor: initialCursor, + error: error, + ); + } + }, ); } +} - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - middle: _GameTitle(gameData: gameData), - padding: const EdgeInsetsDirectional.only(end: 16.0), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - BookmarkButton( - id: gameData.id, - ), - ToggleSoundButton(), - ], - ), +class _Body extends StatelessWidget { + const _Body({required this.gameData, required this.orientation, this.initialCursor, this.error}); + + final LightArchivedGame? gameData; + final Object? error; + final Side orientation; + final int? initialCursor; + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: gameData != null ? _GameTitle(gameData: gameData!) : const SizedBox.shrink(), + actions: [ + if (gameData == null && error == null) const PlatformAppBarLoadingIndicator(), + if (gameData != null) BookmarkButton(id: gameData!.id), + const ToggleSoundButton(), + ], ), - child: SafeArea( + body: SafeArea( bottom: false, child: Column( children: [ Expanded( child: _BoardBody( - gameData: gameData, + archivedGameData: gameData, orientation: orientation, initialCursor: initialCursor, + error: error, ), ), - _BottomBar(gameData: gameData, orientation: orientation), + _BottomBar(archivedGameData: gameData, orientation: orientation), ], ), ), @@ -104,25 +130,26 @@ class ArchivedGameScreen extends ConsumerWidget { } class _GameTitle extends StatelessWidget { - const _GameTitle({ - required this.gameData, - }); + const _GameTitle({required this.gameData}); final LightArchivedGame gameData; + static final _dateFormat = DateFormat.yMMMd(); + @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - gameData.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + if (gameData.source == GameSource.import) + Icon(Icons.cloud_upload, color: DefaultTextStyle.of(context).style.color) + else + Icon(gameData.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), - Text( - '${gameData.clockDisplay} • ${gameData.rated ? context.l10n.rated : context.l10n.casual}', - ), + if (gameData.source == GameSource.import) + Text('Import • ${_dateFormat.format(gameData.createdAt)}') + else + Text('${gameData.clockDisplay} • ${_dateFormat.format(gameData.lastMoveAt)}'), ], ); } @@ -130,45 +157,42 @@ class _GameTitle extends StatelessWidget { class _BoardBody extends ConsumerWidget { const _BoardBody({ - required this.gameData, + required this.archivedGameData, required this.orientation, + this.error, this.initialCursor, }); - final LightArchivedGame gameData; + final LightArchivedGame? archivedGameData; final Side orientation; final int? initialCursor; + final Object? error; @override Widget build(BuildContext context, WidgetRef ref) { + final gameData = archivedGameData; + + if (gameData == null) { + return BoardTable.empty(showMoveListPlaceholder: true, errorMessage: error?.toString()); + } + if (initialCursor != null) { ref.listen(gameCursorProvider(gameData.id), (prev, cursor) { if (prev?.isLoading == true && cursor.hasValue) { - ref - .read(gameCursorProvider(gameData.id).notifier) - .cursorAt(initialCursor!); + ref.read(gameCursorProvider(gameData.id).notifier).cursorAt(initialCursor!); } }); } final isBoardTurned = ref.watch(isBoardTurnedProvider); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); - final black = GamePlayer( - key: const ValueKey('black-player'), - player: gameData.black, - ); - final white = GamePlayer( - key: const ValueKey('white-player'), - player: gameData.white, - ); + final black = GamePlayer(key: const ValueKey('black-player'), player: gameData.black); + final white = GamePlayer(key: const ValueKey('white-player'), player: gameData.white); final topPlayer = orientation == Side.white ? black : white; final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: (isBoardTurned ? orientation.opposite : orientation).cg, - fen: gameData.lastFen ?? kInitialBoardFEN, - ), + orientation: (isBoardTurned ? orientation.opposite : orientation), + fen: initialCursor == null ? gameData.lastFen ?? kEmptyBoardFEN : kEmptyBoardFEN, topTable: topPlayer, bottomTable: bottomPlayer, showMoveListPlaceholder: true, @@ -182,59 +206,40 @@ class _BoardBody extends ConsumerWidget { final black = GamePlayer( key: const ValueKey('black-player'), player: gameData.black, - clock: blackClock != null - ? CountdownClock( - duration: blackClock, - active: false, - ) - : null, + clock: blackClock != null ? Clock(timeLeft: blackClock) : null, materialDiff: game.materialDiffAt(cursor, Side.black), ); final white = GamePlayer( key: const ValueKey('white-player'), player: gameData.white, - clock: whiteClock != null - ? CountdownClock( - duration: whiteClock, - active: false, - ) - : null, + clock: whiteClock != null ? Clock(timeLeft: whiteClock) : null, materialDiff: game.materialDiffAt(cursor, Side.white), ); - final topPlayer = orientation == Side.white ? black : white; - final bottomPlayer = orientation == Side.white ? white : black; + + final topPlayerIsBlack = + orientation == Side.white && !isBoardTurned || + orientation == Side.black && isBoardTurned; + final topPlayer = topPlayerIsBlack ? black : white; + final bottomPlayer = topPlayerIsBlack ? white : black; final position = game.positionAt(cursor); return BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: - (isBoardTurned ? orientation.opposite : orientation).cg, - fen: position.fen, - lastMove: game.moveAt(cursor)?.cg, - sideToMove: position.turn.cg, - isCheck: position.isCheck, - ), + orientation: (isBoardTurned ? orientation.opposite : orientation), + fen: position.fen, + lastMove: game.moveAt(cursor) as NormalMove?, topTable: topPlayer, bottomTable: bottomPlayer, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: cursor, onSelectMove: (moveIndex) { - ref - .read(gameCursorProvider(gameData.id).notifier) - .cursorAt(moveIndex); + ref.read(gameCursorProvider(gameData.id).notifier).cursorAt(moveIndex); }, ); }, loading: () => loadingBoard, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace', - ); + debugPrint('SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace'); return loadingBoard; }, ); @@ -242,153 +247,117 @@ class _BoardBody extends ConsumerWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({required this.gameData, required this.orientation}); + const _BottomBar({required this.archivedGameData, required this.orientation}); final Side orientation; - final LightArchivedGame gameData; + final LightArchivedGame? archivedGameData; @override Widget build(BuildContext context, WidgetRef ref) { + final gameData = archivedGameData; + + if (gameData == null) { + return const BottomBar(children: []); + } + final canGoForward = ref.watch(canGoForwardProvider(gameData.id)); final canGoBackward = ref.watch(canGoBackwardProvider(gameData.id)); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showGameMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - gameCursor.when( - data: (data) { - return Expanded( - child: BottomBarButton( - label: context.l10n.mobileShowResult, - icon: Icons.info_outline, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => - ArchivedGameResultDialog(game: data.$1), - barrierDismissible: true, - ); - }, - ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.gameAnalysis, - onTap: ref.read(gameCursorProvider(gameData.id)).hasValue - ? () { - final (game, cursor) = ref - .read( - gameCursorProvider(gameData.id), - ) - .requireValue; - - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: gameData.variant, - initialMoveCursor: cursor, - orientation: orientation, - id: gameData.id, - opening: gameData.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, - ), + Future showGameMenu() { + final game = gameCursor.valueOrNull?.$1; + final cursor = gameCursor.valueOrNull?.$2; + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref.read(isBoardTurnedProvider.notifier).toggle(); + }, + ), + if (game != null && cursor != null) + ...makeFinishedGameShareActions( + game, + context: context, + ref: ref, + currentGamePosition: game.positionAt(cursor), + orientation: orientation, + lastMove: game.moveAt(cursor), + ), + ], + ); + } + + return BottomBar( + children: [ + BottomBarButton(label: context.l10n.menu, onTap: showGameMenu, icon: Icons.menu), + if (gameCursor.hasValue) + BottomBarButton( + label: context.l10n.mobileShowResult, + icon: Icons.info_outline, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => ArchivedGameResultDialog(game: gameCursor.requireValue.$1), + barrierDismissible: true, + ); + }, + ), + BottomBarButton( + label: context.l10n.gameAnalysis, + onTap: + gameCursor.hasValue + ? () { + final cursor = gameCursor.requireValue.$2; + pushPlatformRoute( + context, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: orientation, + gameId: gameData.id, + initialMoveCursor: cursor, ), - ); - } - : null, - icon: Icons.biotech, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: - canGoBackward ? () => _cursorBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('cursor-back'), - // TODO add translation - label: 'Backward', - showTooltip: false, - onTap: canGoBackward ? () => _cursorBackward(ref) : null, - icon: CupertinoIcons.chevron_back, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoForward ? () => _cursorForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('cursor-forward'), - // TODO add translation - label: 'Forward', - showTooltip: false, - onTap: canGoForward ? () => _cursorForward(ref) : null, - icon: CupertinoIcons.chevron_forward, - ), - ), - ), - ], + ), + ); + } + : null, + icon: Icons.biotech, + ), + RepeatButton( + onLongPress: canGoBackward ? () => _cursorBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('cursor-back'), + // TODO add translation + label: 'Backward', + showTooltip: false, + onTap: canGoBackward ? () => _cursorBackward(ref) : null, + icon: CupertinoIcons.chevron_back, ), ), - ), + RepeatButton( + onLongPress: canGoForward ? () => _cursorForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('cursor-forward'), + // TODO add translation + label: 'Forward', + showTooltip: false, + onTap: canGoForward ? () => _cursorForward(ref) : null, + icon: CupertinoIcons.chevron_forward, + ), + ), + ], ); } void _cursorForward(WidgetRef ref) { - ref.read(gameCursorProvider(gameData.id).notifier).cursorForward(); + if (archivedGameData == null) return; + ref.read(gameCursorProvider(archivedGameData!.id).notifier).cursorForward(); } void _cursorBackward(WidgetRef ref) { - ref.read(gameCursorProvider(gameData.id).notifier).cursorBackward(); - } - - Future _showGameMenu(BuildContext context, WidgetRef ref) { - final game = ref.read(gameCursorProvider(gameData.id)).valueOrNull?.$1; - final cursor = ref.read(gameCursorProvider(gameData.id)).valueOrNull?.$2; - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref.read(isBoardTurnedProvider.notifier).toggle(); - }, - ), - if (game != null && cursor != null) - ...makeFinishedGameShareActions( - game, - context: context, - ref: ref, - currentGamePosition: game.positionAt(cursor), - orientation: orientation, - lastMove: game.moveAt(cursor), - ), - ], - ); + if (archivedGameData == null) return; + ref.read(gameCursorProvider(archivedGameData!.id).notifier).cursorBackward(); } } diff --git a/lib/src/view/game/archived_game_screen_providers.dart b/lib/src/view/game/archived_game_screen_providers.dart index d3ae4fe2dd..bcbecf5721 100644 --- a/lib/src/view/game/archived_game_screen_providers.dart +++ b/lib/src/view/game/archived_game_screen_providers.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -19,7 +20,7 @@ class IsBoardTurned extends _$IsBoardTurned { } @riverpod -bool canGoForward(CanGoForwardRef ref, GameId id) { +bool canGoForward(Ref ref, GameId id) { final gameCursor = ref.watch(gameCursorProvider(id)); if (gameCursor.hasValue) { final (game, cursor) = gameCursor.value!; @@ -30,7 +31,7 @@ bool canGoForward(CanGoForwardRef ref, GameId id) { } @riverpod -bool canGoBackward(CanGoBackwardRef ref, GameId id) { +bool canGoBackward(Ref ref, GameId id) { final gameCursor = ref.watch(gameCursorProvider(id)); if (gameCursor.hasValue) { final (_, cursor) = gameCursor.value!; diff --git a/lib/src/view/game/correspondence_clock_widget.dart b/lib/src/view/game/correspondence_clock_widget.dart index eb7c07ae11..8743daf3c8 100644 --- a/lib/src/view/game/correspondence_clock_widget.dart +++ b/lib/src/view/game/correspondence_clock_widget.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; class CorrespondenceClock extends ConsumerStatefulWidget { /// The duration left on the clock. @@ -18,16 +18,10 @@ class CorrespondenceClock extends ConsumerStatefulWidget { /// Callback when the clock reaches zero. final VoidCallback? onFlag; - const CorrespondenceClock({ - required this.duration, - required this.active, - this.onFlag, - super.key, - }); + const CorrespondenceClock({required this.duration, required this.active, this.onFlag, super.key}); @override - ConsumerState createState() => - _CorrespondenceClockState(); + ConsumerState createState() => _CorrespondenceClockState(); } const _period = Duration(seconds: 1); @@ -95,27 +89,24 @@ class _CorrespondenceClockState extends ConsumerState { final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); final brightness = ref.watch(currentBrightnessProvider); - final clockStyle = brightness == Brightness.dark - ? ClockStyle.darkThemeStyle - : ClockStyle.lightThemeStyle; + final clockStyle = + brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle; final remainingHeight = estimateRemainingHeightLeftBoard(context); - final daysStr = days > 1 - ? context.l10n.nbDays(days) - : days == 1 + final daysStr = + days > 1 + ? context.l10n.nbDays(days) + : days == 1 ? context.l10n.oneDay : ''; - final hoursStr = - days > 0 && hours > 0 ? ' ${context.l10n.nbHours(hours)}' : ''; + final hoursStr = days > 0 && hours > 0 ? ' ${context.l10n.nbHours(hours)}' : ''; return RepaintBoundary( child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: widget.active - ? clockStyle.activeBackgroundColor - : clockStyle.backgroundColor, + color: widget.active ? clockStyle.activeBackgroundColor : clockStyle.backgroundColor, ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), @@ -127,19 +118,10 @@ class _CorrespondenceClockState extends ConsumerState { text: TextSpan( text: '$daysStr$hoursStr', style: TextStyle( - color: widget.active - ? clockStyle.activeTextColor - : clockStyle.textColor, + color: widget.active ? clockStyle.activeTextColor : clockStyle.textColor, fontSize: 18, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 1.0 - : null, - fontFeatures: days == 0 - ? const [ - FontFeature.tabularFigures(), - ] - : null, + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, + fontFeatures: days == 0 ? const [FontFeature.tabularFigures()] : null, ), children: [ if (days == 0) ...[ @@ -147,17 +129,14 @@ class _CorrespondenceClockState extends ConsumerState { TextSpan( text: ':', style: TextStyle( - color: widget.active && - timeLeft.inSeconds.remainder(2) == 0 - ? clockStyle.activeTextColor.withOpacity(0.5) - : null, + color: + widget.active && timeLeft.inSeconds.remainder(2) == 0 + ? clockStyle.activeTextColor.withValues(alpha: 0.5) + : null, ), ), TextSpan(text: mins.toString().padLeft(2, '0')), - if (hours == 0) ...[ - const TextSpan(text: ':'), - TextSpan(text: secs), - ], + if (hours == 0) ...[const TextSpan(text: ':'), TextSpan(text: secs)], ], ], ), diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 5bc42a50d8..187006c047 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'dart:math' as math; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -17,7 +14,6 @@ import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/gestures_exclusion.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -27,9 +23,11 @@ import 'package:lichess_mobile/src/view/game/correspondence_clock_widget.dart'; import 'package:lichess_mobile/src/view/game/message_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -96,122 +94,122 @@ class GameBody extends ConsumerWidget { ref.listen( ctrlProvider, - (prev, state) => _stateListener( - prev, - state, - context: context, - ref: ref, - ), + (prev, state) => _stateListener(prev, state, context: context, ref: ref), ); final boardPreferences = ref.watch(boardPreferencesProvider); - final emergencySoundEnabled = ref.watch(clockSoundProvider).maybeWhen( - data: (clockSound) => clockSound, - orElse: () => true, - ); - - final blindfoldMode = ref.watch( - gamePreferencesProvider.select( - (prefs) => prefs.blindfoldMode, - ), - ); + final blindfoldMode = ref.watch(gamePreferencesProvider.select((prefs) => prefs.blindfoldMode)); final gameStateAsync = ref.watch(ctrlProvider); return gameStateAsync.when( data: (gameState) { - final position = gameState.game.positionAt(gameState.stepCursor); + final (position, legalMoves) = gameState.currentPosition; final youAre = gameState.game.youAre ?? Side.white; - final archivedBlackClock = - gameState.game.archivedBlackClockAt(gameState.stepCursor); - final archivedWhiteClock = - gameState.game.archivedWhiteClockAt(gameState.stepCursor); + final archivedBlackClock = gameState.game.archivedBlackClockAt(gameState.stepCursor); + final archivedWhiteClock = gameState.game.archivedWhiteClockAt(gameState.stepCursor); final black = GamePlayer( player: gameState.game.black, - materialDiff: boardPreferences.showMaterialDifference - ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) - : null, - timeToMove: gameState.game.sideToMove == Side.black - ? gameState.timeToMove - : null, + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) + : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, + timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, mePlaying: youAre == Side.black, zenMode: gameState.isZenModeActive, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.black && gameState.moveToConfirm != null ? ( - confirm: () { - ref.read(ctrlProvider.notifier).confirmMove(); - }, - cancel: () { - ref.read(ctrlProvider.notifier).cancelMove(); - }, - ) + confirm: () { + ref.read(ctrlProvider.notifier).confirmMove(); + }, + cancel: () { + ref.read(ctrlProvider.notifier).cancelMove(); + }, + ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: blackClockKey, - duration: archivedBlackClock ?? gameState.game.clock!.black, - active: gameState.activeClockSide == Side.black, - emergencyThreshold: youAre == Side.black - ? gameState.game.meta.clock?.emergency - : null, - emergencySoundEnabled: emergencySoundEnabled, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), - ) - : gameState.game.correspondenceClock != null + clock: + archivedBlackClock != null + ? Clock(timeLeft: archivedBlackClock, active: false) + : gameState.liveClock != null + ? RepaintBoundary( + child: ValueListenableBuilder( + key: blackClockKey, + valueListenable: gameState.liveClock!.black, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.black, + emergencyThreshold: + youAre == Side.black ? gameState.game.meta.clock?.emergency : null, + ); + }, + ), + ) + : gameState.game.correspondenceClock != null ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.black, - active: gameState.activeClockSide == Side.black, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), - ) + duration: gameState.game.correspondenceClock!.black, + active: gameState.activeClockSide == Side.black, + onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + ) : null, ); final white = GamePlayer( player: gameState.game.white, - materialDiff: boardPreferences.showMaterialDifference - ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) - : null, - timeToMove: gameState.game.sideToMove == Side.white - ? gameState.timeToMove - : null, + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) + : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, + timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, mePlaying: youAre == Side.white, zenMode: gameState.isZenModeActive, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.white && gameState.moveToConfirm != null ? ( - confirm: () { - ref.read(ctrlProvider.notifier).confirmMove(); - }, - cancel: () { - ref.read(ctrlProvider.notifier).cancelMove(); - }, - ) + confirm: () { + ref.read(ctrlProvider.notifier).confirmMove(); + }, + cancel: () { + ref.read(ctrlProvider.notifier).cancelMove(); + }, + ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: whiteClockKey, - duration: archivedWhiteClock ?? gameState.game.clock!.white, - active: gameState.activeClockSide == Side.white, - emergencyThreshold: youAre == Side.white - ? gameState.game.meta.clock?.emergency - : null, - emergencySoundEnabled: emergencySoundEnabled, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), - ) - : gameState.game.correspondenceClock != null + clock: + archivedWhiteClock != null + ? Clock(timeLeft: archivedWhiteClock, active: false) + : gameState.liveClock != null + ? RepaintBoundary( + child: ValueListenableBuilder( + key: whiteClockKey, + valueListenable: gameState.liveClock!.white, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.white, + emergencyThreshold: + youAre == Side.white ? gameState.game.meta.clock?.emergency : null, + ); + }, + ), + ) + : gameState.game.correspondenceClock != null ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.white, - active: gameState.activeClockSide == Side.white, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), - ) + duration: gameState.game.correspondenceClock!.white, + active: gameState.activeClockSide == Side.white, + onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + ) : null, ); final isBoardTurned = ref.watch(isBoardTurnedProvider); - final topPlayerIsBlack = youAre == Side.white && !isBoardTurned || - youAre == Side.black && isBoardTurned; + final topPlayerIsBlack = + youAre == Side.white && !isBoardTurned || youAre == Side.black && isBoardTurned; final topPlayer = topPlayerIsBlack ? black : white; final bottomPlayer = topPlayerIsBlack ? white : black; @@ -224,8 +222,7 @@ class GameBody extends ConsumerWidget { final content = WakelockWidget( shouldEnableOnFocusGained: () => gameState.game.playable, child: PopScope( - canPop: gameState.game.meta.speed == Speed.correspondence || - !gameState.game.playable, + canPop: gameState.game.meta.speed == Speed.correspondence || !gameState.game.playable, child: Column( children: [ Expanded( @@ -236,47 +233,45 @@ class GameBody extends ConsumerWidget { boardSettingsOverrides: BoardSettingsOverrides( animationDuration: animationDuration, autoQueenPromotion: gameState.canAutoQueen, - autoQueenPromotionOnPremove: - gameState.canAutoQueenOnPremove, + autoQueenPromotionOnPremove: gameState.canAutoQueenOnPremove, blindfoldMode: blindfoldMode, ), - onMove: (move, {isDrop, isPremove}) { - ref.read(ctrlProvider.notifier).onUserMove( - Move.fromUci(move.uci)!, - isPremove: isPremove, - isDrop: isDrop, - ); - }, - onPremove: gameState.canPremove - ? (move) { - ref.read(ctrlProvider.notifier).setPremove(move); - } - : null, - boardData: cg.BoardData( - interactableSide: + orientation: isBoardTurned ? youAre.opposite : youAre, + fen: position.fen, + lastMove: gameState.game.moveAt(gameState.stepCursor) as NormalMove?, + gameData: GameData( + playerSide: gameState.game.playable && !gameState.isReplaying ? youAre == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black - : cg.InteractableSide.none, - orientation: - isBoardTurned ? youAre.opposite.cg : youAre.cg, - fen: position.fen, - lastMove: - gameState.game.moveAt(gameState.stepCursor)?.cg, - isCheck: boardPreferences.boardHighlights && - position.isCheck, - sideToMove: position.turn.cg, - validMoves: algebraicLegalMoves(position), - premove: gameState.premove, + ? PlayerSide.white + : PlayerSide.black + : PlayerSide.none, + isCheck: boardPreferences.boardHighlights && position.isCheck, + sideToMove: position.turn, + validMoves: legalMoves, + promotionMove: gameState.promotionMove, + onMove: (move, {isDrop}) { + ref.read(ctrlProvider.notifier).userMove(move, isDrop: isDrop); + }, + onPromotionSelection: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelection(role); + }, + premovable: + gameState.canPremove + ? ( + onSetPremove: (move) { + ref.read(ctrlProvider.notifier).setPremove(move); + }, + premove: gameState.premove, + ) + : null, ), topTable: topPlayer, - bottomTable: gameState.canShowClaimWinCountdown && - gameState.opponentLeftCountdown != null - ? _ClaimWinCountdown( - duration: gameState.opponentLeftCountdown!, - ) - : bottomPlayer, + bottomTable: + gameState.canShowClaimWinCountdown && + gameState.opponentLeftCountdown != null + ? _ClaimWinCountdown(duration: gameState.opponentLeftCountdown!) + : bottomPlayer, moves: gameState.game.steps .skip(1) .map((e) => e.sanMove!.san) @@ -301,42 +296,33 @@ class GameBody extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.android ? AndroidGesturesExclusionWidget( - boardKey: boardKey, - shouldExcludeGesturesOnFocusGained: () => - gameState.game.meta.speed != Speed.correspondence && - gameState.game.playable, - shouldSetImmersiveMode: - boardPreferences.immersiveModeWhilePlaying ?? false, - child: content, - ) + boardKey: boardKey, + shouldExcludeGesturesOnFocusGained: + () => + gameState.game.meta.speed != Speed.correspondence && gameState.game.playable, + shouldSetImmersiveMode: boardPreferences.immersiveModeWhilePlaying ?? false, + child: content, + ) : content; }, - loading: () => PopScope( - canPop: true, - child: Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: loadingBoardWidget, - ), - ), - _GameBottomBar( - id: id, - onLoadGameCallback: onLoadGameCallback, - onNewOpponentCallback: onNewOpponentCallback, + loading: + () => PopScope( + canPop: true, + child: Column( + children: [ + Expanded(child: SafeArea(bottom: false, child: loadingBoardWidget)), + _GameBottomBar( + id: id, + onLoadGameCallback: onLoadGameCallback, + onNewOpponentCallback: onNewOpponentCallback, + ), + ], ), - ], - ), - ), + ), error: (e, s) { - debugPrint( - 'SEVERE: [GameBody] could not load game data; $e\n$s', - ); + debugPrint('SEVERE: [GameBody] could not load game data; $e\n$s'); return const PopScope( - child: LoadGameError( - 'Sorry, we could not load the game. Please try again later.', - ), + child: LoadGameError('Sorry, we could not load the game. Please try again later.'), ); }, ); @@ -352,17 +338,15 @@ class GameBody extends ConsumerWidget { // If the game is no longer playable, show the game end dialog. // We want to show it only once, whether the game is already finished on // first load or not. - if ((prev?.hasValue != true || - prev!.requireValue.game.playable == true) && + if ((prev?.hasValue != true || prev!.requireValue.game.playable == true) && state.requireValue.game.playable == false) { Timer(const Duration(milliseconds: 500), () { if (context.mounted) { showAdaptiveDialog( context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), + builder: + (context) => + GameResultDialog(id: id, onNewOpponentCallback: onNewOpponentCallback), barrierDismissible: true, ); } @@ -370,8 +354,7 @@ class GameBody extends ConsumerWidget { } // true when the game was loaded, playable, and just finished - if (prev?.valueOrNull?.game.playable == true && - state.requireValue.game.playable == false) { + if (prev?.valueOrNull?.game.playable == true && state.requireValue.game.playable == false) { clearAndroidBoardGesturesExclusion(); } // true when the game was not loaded: handles rematches @@ -381,8 +364,7 @@ class GameBody extends ConsumerWidget { setAndroidBoardGesturesExclusion( boardKey, withImmersiveMode: - ref.read(boardPreferencesProvider).immersiveModeWhilePlaying ?? - false, + ref.read(boardPreferencesProvider).immersiveModeWhilePlaying ?? false, ); } } @@ -390,8 +372,7 @@ class GameBody extends ConsumerWidget { if (prev?.hasValue == true && state.hasValue) { // Opponent is gone long enough to show the claim win dialog. - if (!prev!.requireValue.game.canClaimWin && - state.requireValue.game.canClaimWin) { + if (!prev!.requireValue.game.canClaimWin && state.requireValue.game.canClaimWin) { if (context.mounted) { showAdaptiveDialog( context: context, @@ -426,83 +407,74 @@ class _GameBottomBar extends ConsumerWidget { final ongoingGames = ref.watch(ongoingGamesProvider); final gamePrefs = ref.watch(gamePreferencesProvider); final gameStateAsync = ref.watch(gameControllerProvider(id)); - final chatStateAsync = gamePrefs.enableChat == true - ? ref.watch(chatControllerProvider(id)) - : null; - - final List children = gameStateAsync.when( - data: (gameState) { - final isChatEnabled = - chatStateAsync != null && !gameState.isZenModeActive; - - final chatUnreadChip = isChatEnabled - ? chatStateAsync.maybeWhen( - data: (s) => s.unreadMessages > 0 - ? Text(math.min(9, s.unreadMessages).toString()) - : null, - orElse: () => null, - ) - : null; - - return [ - Expanded( - child: BottomBarButton( + final chatStateAsync = + gamePrefs.enableChat == true ? ref.watch(chatControllerProvider(id)) : null; + + return BottomBar( + children: gameStateAsync.when( + data: (gameState) { + final isChatEnabled = chatStateAsync != null && !gameState.isZenModeActive; + + final chatUnreadLabel = + isChatEnabled + ? chatStateAsync.maybeWhen( + data: + (s) => + s.unreadMessages > 0 + ? (s.unreadMessages < 10) + ? s.unreadMessages.toString() + : '9+' + : null, + orElse: () => null, + ) + : null; + + return [ + BottomBarButton( label: context.l10n.menu, onTap: () { _showGameMenu(context, ref); }, icon: Icons.menu, ), - ), - if (!gameState.game.playable) - Expanded( - child: BottomBarButton( + if (!gameState.game.playable) + BottomBarButton( label: context.l10n.mobileShowResult, onTap: () { showAdaptiveDialog( context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), + builder: + (context) => + GameResultDialog(id: id, onNewOpponentCallback: onNewOpponentCallback), barrierDismissible: true, ); }, icon: Icons.info_outline, ), - ), - if (gameState.game.playable && - gameState.game.opponent?.offeringDraw == true) - Expanded( - child: BottomBarButton( + if (gameState.game.playable && gameState.game.opponent?.offeringDraw == true) + BottomBarButton( label: context.l10n.yourOpponentOffersADraw, highlighted: true, onTap: () { showAdaptiveDialog( context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentOffersADraw), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ), + builder: + (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentOffersADraw), + onAccept: () { + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); + }, + onDecline: () { + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineDraw(); + }, + ), barrierDismissible: true, ); }, icon: Icons.handshake_outlined, - ), - ) - else if (gameState.game.playable && - gameState.game.isThreefoldRepetition == true) - Expanded( - child: BottomBarButton( + ) + else if (gameState.game.playable && gameState.game.isThreefoldRepetition == true) + BottomBarButton( label: context.l10n.threefoldRepetition, highlighted: true, onTap: () { @@ -513,81 +485,70 @@ class _GameBottomBar extends ConsumerWidget { ); }, icon: Icons.handshake_outlined, - ), - ) - else if (gameState.game.playable && - gameState.game.opponent?.proposingTakeback == true) - Expanded( - child: BottomBarButton( + ) + else if (gameState.game.playable && gameState.game.opponent?.proposingTakeback == true) + BottomBarButton( label: context.l10n.yourOpponentProposesATakeback, highlighted: true, onTap: () { showAdaptiveDialog( context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentProposesATakeback), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .acceptTakeback(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); - }, - ), + builder: + (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentProposesATakeback), + onAccept: () { + ref.read(gameControllerProvider(id).notifier).acceptTakeback(); + }, + onDecline: () { + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineTakeback(); + }, + ), barrierDismissible: true, ); }, icon: CupertinoIcons.arrowshape_turn_up_left, - ), - ) - else if (gameState.game.finished) - Expanded( - child: BottomBarButton( + ) + else if (gameState.game.finished) + BottomBarButton( label: context.l10n.gameAnalysis, icon: Icons.biotech, onTap: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, - options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, - ), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, - ), - ) - else - Expanded( - child: BottomBarButton( + ) + else if (gameState.game.meta.speed == Speed.bullet || + gameState.game.meta.speed == Speed.ultraBullet) + BottomBarButton( label: context.l10n.resign, - onTap: gameState.game.resignable - ? gameState.shouldConfirmResignAndDrawOffer - ? () => _showConfirmDialog( + onTap: + gameState.game.resignable + ? gameState.shouldConfirmResignAndDrawOffer + ? () => _showConfirmDialog( context, description: Text(context.l10n.resignTheGame), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); + ref.read(gameControllerProvider(id).notifier).resignGame(); }, ) - : () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - } - : null, + : () { + ref.read(gameControllerProvider(id).notifier).resignGame(); + } + : null, icon: Icons.flag, + ) + else + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () { + ref.read(isBoardTurnedProvider.notifier).toggle(); + }, + icon: CupertinoIcons.arrow_2_squarepath, ), - ), - if (gameState.game.meta.speed == Speed.correspondence && - !gameState.game.finished) - Expanded( - child: BottomBarButton( + if (gameState.game.meta.speed == Speed.correspondence && !gameState.game.finished) + BottomBarButton( label: 'Go to the next game', icon: Icons.skip_next, onTap: ongoingGames.maybeWhen( @@ -595,56 +556,45 @@ class _GameBottomBar extends ConsumerWidget { final nextTurn = games .whereNot((g) => g.fullId == id) .firstWhereOrNull((g) => g.isMyTurn); - return nextTurn != null - ? () => onLoadGameCallback(nextTurn.fullId) - : null; + return nextTurn != null ? () => onLoadGameCallback(nextTurn.fullId) : null; }, orElse: () => null, ), ), - ), - Expanded( - child: BottomBarButton( + BottomBarButton( label: context.l10n.chat, - onTap: isChatEnabled - ? () { - pushPlatformRoute( - context, - builder: (BuildContext context) { - return MessageScreen( - title: UserFullNameWidget( - user: gameState.game.opponent?.user, - ), - me: gameState.game.me?.user, - id: id, - ); - }, - ); - } - : null, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.chat_bubble - : Icons.chat_bubble_outline, - chip: chatUnreadChip, + onTap: + isChatEnabled + ? () { + pushPlatformRoute( + context, + builder: (BuildContext context) { + return MessageScreen( + title: UserFullNameWidget(user: gameState.game.opponent?.user), + me: gameState.game.me?.user, + id: id, + ); + }, + ); + } + : null, + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.chat_bubble + : Icons.chat_bubble_outline, + badgeLabel: chatUnreadLabel, ), - ), - Expanded( - child: RepeatButton( - onLongPress: - gameState.canGoBackward ? () => _moveBackward(ref) : null, + RepeatButton( + onLongPress: gameState.canGoBackward ? () => _moveBackward(ref) : null, child: BottomBarButton( - onTap: - gameState.canGoBackward ? () => _moveBackward(ref) : null, + onTap: gameState.canGoBackward ? () => _moveBackward(ref) : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), - ), - Expanded( - child: RepeatButton( - onLongPress: - gameState.canGoForward ? () => _moveForward(ref) : null, + RepeatButton( + onLongPress: gameState.canGoForward ? () => _moveForward(ref) : null, child: BottomBarButton( onTap: gameState.canGoForward ? () => _moveForward(ref) : null, label: context.l10n.next, @@ -652,25 +602,10 @@ class _GameBottomBar extends ConsumerWidget { showTooltip: false, ), ), - ), - ]; - }, - loading: () => [], - error: (e, s) => [], - ); - - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - children: children, - ), - ), + ]; + }, + loading: () => [], + error: (e, s) => [], ), ); } @@ -694,20 +629,13 @@ class _GameBottomBar extends ConsumerWidget { ref.read(isBoardTurnedProvider.notifier).toggle(); }, ), - if (gameState.game.playable && - gameState.game.meta.speed == Speed.correspondence) + if (gameState.game.playable && gameState.game.meta.speed == Speed.correspondence) BottomSheetAction( makeLabel: (context) => Text(context.l10n.analysis), onPressed: (context) { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, - options: gameState.analysisOptions.copyWith( - isLocalEvaluationAllowed: false, - ), - title: context.l10n.analysis, - ), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, ), @@ -720,11 +648,10 @@ class _GameBottomBar extends ConsumerWidget { ), if (gameState.game.meta.clock != null && gameState.game.canGiveTime) BottomSheetAction( - makeLabel: (context) => Text( - context.l10n.giveNbSeconds( - gameState.game.meta.clock!.moreTime?.inSeconds ?? 15, - ), - ), + makeLabel: + (context) => Text( + context.l10n.giveNbSeconds(gameState.game.meta.clock!.moreTime?.inSeconds ?? 15), + ), onPressed: (context) { ref.read(gameControllerProvider(id).notifier).moreTime(); }, @@ -738,60 +665,43 @@ class _GameBottomBar extends ConsumerWidget { ), if (gameState.game.me?.proposingTakeback == true) BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.mobileCancelTakebackOffer), + makeLabel: (context) => Text(context.l10n.mobileCancelTakebackOffer), isDestructiveAction: true, onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineTakeback(); }, ), - if (gameState.game.me?.offeringDraw == true) - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileCancelDrawOffer), - isDestructiveAction: true, - onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ) - else if (gameState.canOfferDraw) + if (gameState.canOfferDraw) BottomSheetAction( makeLabel: (context) => Text(context.l10n.offerDraw), - onPressed: gameState.shouldConfirmResignAndDrawOffer - ? (context) => _showConfirmDialog( + onPressed: + gameState.shouldConfirmResignAndDrawOffer + ? (context) => _showConfirmDialog( context, description: Text(context.l10n.offerDraw), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); }, ) - : (context) { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, + : (context) { + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); + }, ), if (gameState.game.resignable) BottomSheetAction( makeLabel: (context) => Text(context.l10n.resign), - onPressed: gameState.shouldConfirmResignAndDrawOffer - ? (context) => _showConfirmDialog( + onPressed: + gameState.shouldConfirmResignAndDrawOffer + ? (context) => _showConfirmDialog( context, description: Text(context.l10n.resignTheGame), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); + ref.read(gameControllerProvider(id).notifier).resignGame(); }, ) - : (context) { - ref.read(gameControllerProvider(id).notifier).resignGame(); - }, + : (context) { + ref.read(gameControllerProvider(id).notifier).resignGame(); + }, ), if (gameState.game.canClaimWin) ...[ BottomSheetAction( @@ -818,15 +728,12 @@ class _GameBottomBar extends ConsumerWidget { ref.read(gameControllerProvider(id).notifier).declineRematch(); }, ) - else if (gameState.canOfferRematch && - gameState.game.opponent?.onGame == true) + else if (gameState.canOfferRematch && gameState.game.opponent?.onGame == true) BottomSheetAction( makeLabel: (context) => Text(context.l10n.rematch), dismissOnPress: true, onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .proposeOrAcceptRematch(); + ref.read(gameControllerProvider(id).notifier).proposeOrAcceptRematch(); }, ), if (gameState.canGetNewOpponent) @@ -837,8 +744,7 @@ class _GameBottomBar extends ConsumerWidget { if (gameState.game.finished) ...makeFinishedGameShareActions( gameState.game, - currentGamePosition: - gameState.game.positionAt(gameState.stepCursor), + currentGamePosition: gameState.game.positionAt(gameState.stepCursor), lastMove: gameState.game.moveAt(gameState.stepCursor), orientation: gameState.game.youAre ?? Side.white, context: context, @@ -855,14 +761,15 @@ class _GameBottomBar extends ConsumerWidget { }) async { final result = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: description, - onYes: () { - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: description, + onYes: () { + return Navigator.of(context).pop(true); + }, + onNo: () => Navigator.of(context).pop(false), + ), ); if (result == true) { onConfirm(); @@ -893,42 +800,18 @@ class _GameNegotiationDialog extends StatelessWidget { onAccept(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: title, - actions: [ - CupertinoDialogAction( - onPressed: accept, - child: Text(context.l10n.accept), - ), - CupertinoDialogAction( - onPressed: decline, - child: Text(context.l10n.decline), - ), - ], - ); - } else { - return AlertDialog( - content: title, - actions: [ - TextButton( - onPressed: accept, - child: Text(context.l10n.accept), - ), - TextButton( - onPressed: decline, - child: Text(context.l10n.decline), - ), - ], - ); - } + return PlatformAlertDialog( + content: title, + actions: [ + PlatformDialogAction(onPressed: accept, child: Text(context.l10n.accept)), + PlatformDialogAction(onPressed: decline, child: Text(context.l10n.decline)), + ], + ); } } class _ThreefoldDialog extends ConsumerWidget { - const _ThreefoldDialog({ - required this.id, - }); + const _ThreefoldDialog({required this.id}); final GameFullId id; @@ -945,42 +828,18 @@ class _ThreefoldDialog extends ConsumerWidget { ref.read(gameControllerProvider(id).notifier).claimDraw(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: accept, - child: Text(context.l10n.claimADraw), - ), - CupertinoDialogAction( - onPressed: decline, - child: Text(context.l10n.cancel), - ), - ], - ); - } else { - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: accept, - child: Text(context.l10n.claimADraw), - ), - TextButton( - onPressed: decline, - child: Text(context.l10n.cancel), - ), - ], - ); - } + return PlatformAlertDialog( + content: content, + actions: [ + PlatformDialogAction(onPressed: accept, child: Text(context.l10n.claimADraw)), + PlatformDialogAction(onPressed: decline, child: Text(context.l10n.cancel)), + ], + ); } } class _ClaimWinDialog extends ConsumerWidget { - const _ClaimWinDialog({ - required this.id, - }); + const _ClaimWinDialog({required this.id}); final GameFullId id; @@ -1001,43 +860,25 @@ class _ClaimWinDialog extends ConsumerWidget { ref.read(ctrlProvider.notifier).forceDraw(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: gameState.game.canClaimWin ? onClaimWin : null, - isDefaultAction: true, - child: Text(context.l10n.forceResignation), - ), - CupertinoDialogAction( - onPressed: gameState.game.canClaimWin ? onClaimDraw : null, - child: Text(context.l10n.forceDraw), - ), - ], - ); - } else { - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: gameState.game.canClaimWin ? onClaimWin : null, - child: Text(context.l10n.forceResignation), - ), - TextButton( - onPressed: gameState.game.canClaimWin ? onClaimDraw : null, - child: Text(context.l10n.forceDraw), - ), - ], - ); - } + return PlatformAlertDialog( + content: content, + actions: [ + PlatformDialogAction( + onPressed: gameState.game.canClaimWin ? onClaimWin : null, + cupertinoIsDefaultAction: true, + child: Text(context.l10n.forceResignation), + ), + PlatformDialogAction( + onPressed: gameState.game.canClaimWin ? onClaimDraw : null, + child: Text(context.l10n.forceDraw), + ), + ], + ); } } class _ClaimWinCountdown extends StatelessWidget { - const _ClaimWinCountdown({ - required this.duration, - }); + const _ClaimWinCountdown({required this.duration}); final Duration duration; diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index ba381852a4..3a6a15735e 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -1,22 +1,25 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'game_screen_providers.dart'; import 'game_settings.dart'; @@ -29,9 +32,7 @@ class BookmarkButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return AppBarIconButton( onPressed: () { - ref.withClient( - (client) => GameRepository(client).bookmark(id, v: 1), - ); + ref.withClient((client) => GameRepository(client).bookmark(id, v: 1)); }, semanticsLabel: 'Bookmark Game', icon: const Icon(Icons.star_border_rounded), @@ -45,126 +46,61 @@ class _SettingButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => GameSettings(id: id), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (_) => GameSettings(id: id), + ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ); } } -class GameAppBar extends ConsumerWidget implements PreferredSizeWidget { - const GameAppBar({this.id, this.seek, this.challenge, super.key}); +final _gameTitledateFormat = DateFormat.yMMMd(); + +class GameAppBar extends ConsumerWidget { + const GameAppBar({this.id, this.seek, this.challenge, this.lastMoveAt, super.key}); final GameSeek? seek; final ChallengeRequest? challenge; final GameFullId? id; + /// The date of the last move played in the game. If null, the game is in progress. + final DateTime? lastMoveAt; + static const pingRating = Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0), - child: PingRating(size: 24.0), + child: SocketPingRating(size: 24.0), ); @override Widget build(BuildContext context, WidgetRef ref) { - final shouldPreventGoingBackAsync = id != null - ? ref.watch(shouldPreventGoingBackProvider(id!)) - : const AsyncValue.data(true); + final shouldPreventGoingBackAsync = + id != null ? ref.watch(shouldPreventGoingBackProvider(id!)) : const AsyncValue.data(true); - return AppBar( + return PlatformAppBar( leading: shouldPreventGoingBackAsync.maybeWhen( data: (prevent) => prevent ? pingRating : null, orElse: () => pingRating, ), - title: id != null - ? StandaloneGameTitle(id: id!) - : seek != null + title: + id != null + ? _StandaloneGameTitle(id: id!, lastMoveAt: lastMoveAt) + : seek != null ? _LobbyGameTitle(seek: seek!) : challenge != null - ? _ChallengeGameTitle(challenge: challenge!) - : const SizedBox.shrink(), + ? _ChallengeGameTitle(challenge: challenge!) + : const SizedBox.shrink(), actions: [ - if (id != null) ...[ - BookmarkButton( - id: id!.gameId, - ), - _SettingButton( - id: id!, - ), - ], + const ToggleSoundButton(), + if (id != null) ...[BookmarkButton(id: id!.gameId), _SettingButton(id: id!)], ], ); } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class GameCupertinoNavBar extends ConsumerWidget - implements ObstructingPreferredSizeWidget { - const GameCupertinoNavBar({this.id, this.seek, this.challenge, super.key}); - - final GameSeek? seek; - final ChallengeRequest? challenge; - final GameFullId? id; - - static const pingRating = Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: PingRating(size: 24.0), - ); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final shouldPreventGoingBackAsync = id != null - ? ref.watch(shouldPreventGoingBackProvider(id!)) - : const AsyncValue.data(true); - - return CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - leading: shouldPreventGoingBackAsync.maybeWhen( - data: (prevent) => prevent ? pingRating : null, - orElse: () => pingRating, - ), - middle: id != null - ? StandaloneGameTitle(id: id!) - : seek != null - ? _LobbyGameTitle(seek: seek!) - : challenge != null - ? _ChallengeGameTitle(challenge: challenge!) - : const SizedBox.shrink(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (id != null) ...[ - BookmarkButton( - id: id!.gameId, - ), - _SettingButton( - id: id!, - ), - ], - ], - ), - ); - } - - @override - Size get preferredSize => - const Size.fromHeight(kMinInteractiveDimensionCupertino); - - /// True if the navigation bar's background color has no transparency. - @override - bool shouldFullyObstruct(BuildContext context) { - final Color backgroundColor = CupertinoTheme.of(context).barBackgroundColor; - return backgroundColor.alpha == 0xFF; - } } List makeFinishedGameShareActions( @@ -177,146 +113,189 @@ List makeFinishedGameShareActions( }) { return [ BottomSheetAction( - makeLabel: (_) => Text(context.l10n.mobileShareGameURL), - dismissOnPress: false, + makeLabel: (_) => Text(context.l10n.studyShareAndExport), + dismissOnPress: true, onPressed: (context) { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.gameAsGIF), - dismissOnPress: false, - onPressed: (context) async { - try { - final gif = await ref - .read(gameShareServiceProvider) - .gameGif(game.id, orientation); - if (context.mounted) { - launchShareDialog( - context, - files: [gif], - subject: context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: + (context) => GameShareBottomSheet( + game: game, + currentGamePosition: currentGamePosition, + orientation: orientation, + lastMove: lastMove, ), - ); - } - } catch (e) { - debugPrint(e.toString()); - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } + ); }, ), - if (lastMove != null) - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), - dismissOnPress: false, - onPressed: (context) async { - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - game.id, - orientation, - currentGamePosition.fen, - lastMove, + ]; +} + +class GameShareBottomSheet extends ConsumerWidget { + const GameShareBottomSheet({ + required this.game, + required this.currentGamePosition, + required this.orientation, + this.lastMove, + super.key, + }); + + final BaseGame game; + final Position currentGamePosition; + final Side orientation; + final Move? lastMove; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomSheetScrollableContainer( + children: [ + BottomSheetContextMenuAction( + icon: CupertinoIcons.link, + closeOnPressed: false, + onPressed: () { + launchShareDialog(context, uri: lichessUri('/${game.id}')); + }, + child: Text(context.l10n.mobileShareGameURL), + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.gif, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.gameAsGIF), + onPressed: () async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .gameGif(game.id, orientation); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: + '${game.meta.perf.title} • ${context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context))}', ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/${game.id}').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - BottomSheetAction( - makeLabel: (context) => Text('PGN: ${context.l10n.downloadAnnotated}'), - dismissOnPress: false, - onPressed: (context) async { - try { - final pgn = - await ref.read(gameShareServiceProvider).annotatedPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); + } + } + }, ); - } - } - }, - ), - BottomSheetAction( - makeLabel: (context) => Text('PGN: ${context.l10n.downloadRaw}'), - dismissOnPress: false, - onPressed: (context) async { - try { - final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, + }, + ), + if (lastMove != null) + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.image, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.screenshotCurrentPosition), + onPressed: () async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition(orientation, currentGamePosition.fen, lastMove); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/${game.id}').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); + } + } + }, + ); + }, + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text('PGN: ${context.l10n.downloadAnnotated}'), + onPressed: () async { + try { + final pgn = await ref.read(gameShareServiceProvider).annotatedPgn(game.id); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); + } + } + }, ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, + }, + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + // TODO improve translation + child: Text('PGN: ${context.l10n.downloadRaw}'), + onPressed: () async { + try { + final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); + } + } + }, ); - } - } - }, - ), - ]; + }, + ), + ], + ); + } } class _LobbyGameTitle extends ConsumerWidget { - const _LobbyGameTitle({ - required this.seek, - }); + const _LobbyGameTitle({required this.seek}); final GameSeek seek; @override Widget build(BuildContext context, WidgetRef ref) { - final mode = - seek.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + final mode = seek.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - seek.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(seek.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), Text('${seek.timeIncrement?.display}$mode'), ], @@ -325,69 +304,68 @@ class _LobbyGameTitle extends ConsumerWidget { } class _ChallengeGameTitle extends ConsumerWidget { - const _ChallengeGameTitle({ - required this.challenge, - }); + const _ChallengeGameTitle({required this.challenge}); final ChallengeRequest challenge; @override Widget build(BuildContext context, WidgetRef ref) { - final mode = challenge.rated - ? ' • ${context.l10n.rated}' - : ' • ${context.l10n.casual}'; + final mode = challenge.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - challenge.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(challenge.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (challenge.timeIncrement != null) Text('${challenge.timeIncrement?.display}$mode') else if (challenge.days != null) - Text( - '${context.l10n.nbDays(challenge.days!)}$mode', - ), + Text('${context.l10n.nbDays(challenge.days!)}$mode'), ], ); } } -class StandaloneGameTitle extends ConsumerWidget { - const StandaloneGameTitle({ - required this.id, - }); +class _StandaloneGameTitle extends ConsumerWidget { + const _StandaloneGameTitle({required this.id, this.lastMoveAt}); final GameFullId id; + final DateTime? lastMoveAt; + @override Widget build(BuildContext context, WidgetRef ref) { final metaAsync = ref.watch(gameMetaProvider(id)); return metaAsync.maybeWhen( data: (meta) { - final mode = meta.rated - ? ' • ${context.l10n.rated}' - : ' • ${context.l10n.casual}'; + final mode = meta.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + + final info = lastMoveAt != null ? ' • ${_gameTitledateFormat.format(lastMoveAt!)}' : mode; + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - meta.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(meta.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (meta.clock != null) - Text( - '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$mode', + Expanded( + child: AutoSizeText( + '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$info', + maxLines: 1, + minFontSize: 14.0, + ), ) else if (meta.daysPerTurn != null) - Text( - '${context.l10n.nbDays(meta.daysPerTurn!)}$mode', + Expanded( + child: AutoSizeText( + '${context.l10n.nbDays(meta.daysPerTurn!)}$info', + maxLines: 1, + minFontSize: 14.0, + ), ) else - Text('${meta.perf.title}$mode'), + Expanded( + child: AutoSizeText('${meta.perf.title}$info', maxLines: 1, minFontSize: 14.0), + ), ], ); }, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index d23465e45c..b52114f5a5 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -4,14 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; @@ -24,9 +24,8 @@ import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); +final _dateFormatter = DateFormat.yMMMd().add_Hm(); /// A list tile that shows game info. class GameListTile extends StatelessWidget { @@ -62,26 +61,26 @@ class GameListTile extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, - builder: (context) => _ContextMenu( - game: game, - mySide: mySide, - oppponentTitle: opponentTitle, - icon: icon, - subtitle: subtitle, - trailing: trailing, - ), + builder: + (context) => _ContextMenu( + game: game, + mySide: mySide, + oppponentTitle: opponentTitle, + icon: icon, + subtitle: subtitle, + trailing: trailing, + ), ); }, leading: icon != null ? Icon(icon) : null, title: opponentTitle, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, trailing: trailing, padding: padding, ); @@ -112,348 +111,265 @@ class _ContextMenu extends ConsumerWidget { final customColors = Theme.of(context).extension(); - return DraggableScrollableSheet( - initialChildSize: .7, - expand: false, - snap: true, - snapSizes: const [.7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), - child: Text( - context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - ), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), - child: LayoutBuilder( - builder: (context, constraints) { - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - if (game.lastFen != null) - BoardThumbnail( - size: constraints.maxWidth - - (constraints.maxWidth / 1.618), - fen: game.lastFen!, - orientation: mySide.cg, - lastMove: game.lastMove?.cg, + return BottomSheetScrollableContainer( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ).add(const EdgeInsets.only(bottom: 8.0)), + child: Text( + context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context)), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: -0.5), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ).add(const EdgeInsets.only(bottom: 8.0)), + child: LayoutBuilder( + builder: (context, constraints) { + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + if (game.lastFen != null) + BoardThumbnail( + size: constraints.maxWidth - (constraints.maxWidth / 1.618), + fen: game.lastFen!, + orientation: mySide, + lastMove: game.lastMove, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + Text( + _dateFormatter.format(game.lastMoveAt), + style: TextStyle( + color: textShade(context, Styles.subtitleOpacity), + fontSize: 12, + ), + ), + ], ), - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', - style: const TextStyle( - fontWeight: FontWeight.w500, - ), - ), - Text( - _dateFormatter.format(game.lastMoveAt), - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), - fontSize: 12, - ), - ), - ], + if (game.lastFen != null) + Text( + gameStatusL10n( + context, + variant: game.variant, + status: game.status, + lastPosition: Position.setupPosition( + game.variant.rule, + Setup.parseFen(game.lastFen!), ), - if (game.lastFen != null) - Text( - gameStatusL10n( - context, - variant: game.variant, - status: game.status, - lastPosition: Position.setupPosition( - game.variant.rule, - Setup.parseFen(game.lastFen!), - ), - winner: game.winner, - ), - style: TextStyle( - color: game.winner == null - ? customColors?.brag - : game.winner == mySide - ? customColors?.good - : customColors?.error, - ), - ), - if (game.opening != null) - Text( - game.opening!.name, - maxLines: 2, - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - ), - ], + winner: game.winner, + ), + style: TextStyle( + color: + game.winner == null + ? customColors?.brag + : game.winner == mySide + ? customColors?.good + : customColors?.error, + ), ), - ), - ), - ], + if (game.opening != null) + Text( + game.opening!.name, + maxLines: 2, + style: TextStyle( + color: textShade(context, Styles.subtitleOpacity), + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ); - }, + ), + ], ), - ), - BottomSheetContextMenuAction( - icon: Icons.biotech, - onPressed: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.id.value, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), + ); + }, + ), + ), + BottomSheetContextMenuAction( + icon: Icons.biotech, + onPressed: + game.variant.isReadSupported + ? () { + pushPlatformRoute( + context, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions(orientation: orientation, gameId: game.id), ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, - child: Text(context.l10n.gameAnalysis), - ), - BottomSheetContextMenuAction( - onPressed: () { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), - ); - }, - icon: CupertinoIcons.link, - closeOnPressed: false, - child: Text(context.l10n.mobileShareGameURL), - ), - BottomSheetContextMenuAction( - onPressed: () { - ref.withClient( - (client) => GameRepository(client).bookmark(game.id, v: 1), - ); - }, - icon: Icons.star_border_rounded, - closeOnPressed: false, - child: const Text('Bookmark Game'), - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.gif, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text(context.l10n.gameAsGIF), - onPressed: () async { - try { - final gif = await ref - .read(gameShareServiceProvider) - .gameGif(game.id, orientation); - if (context.mounted) { - launchShareDialog( - context, - files: [gif], - subject: - '${game.perf.title} • ${context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - )}', - ); - } - } catch (e) { - debugPrint(e.toString()); - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ); - }, - ), - if (game.lastFen != null) - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.image, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text(context.l10n.screenshotCurrentPosition), - onPressed: () async { - try { - final image = await ref - .read(gameShareServiceProvider) - .screenshotPosition( - game.id, - orientation, - game.lastFen!, - game.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/${game.id}').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, + ); + } + : () { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, ); }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: CupertinoIcons.share, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text('PGN: ${context.l10n.downloadAnnotated}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .annotatedPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ); - }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: CupertinoIcons.share, - closeOnPressed: - false, // needed for the share dialog on iPad - // TODO improve translation - child: Text('PGN: ${context.l10n.downloadRaw}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .rawPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ); + child: Text(context.l10n.gameAnalysis), + ), + BottomSheetContextMenuAction( + onPressed: () { + launchShareDialog(context, uri: lichessUri('/${game.id}')); + }, + icon: CupertinoIcons.link, + closeOnPressed: false, + child: Text(context.l10n.mobileShareGameURL), + ), + BottomSheetContextMenuAction( + onPressed: () { + ref.withClient((client) => GameRepository(client).bookmark(game.id, v: 1)); + }, + icon: Icons.star_border_rounded, + closeOnPressed: false, + child: const Text('Bookmark Game'), + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.gif, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.gameAsGIF), + onPressed: () async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .gameGif(game.id, orientation); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: + '${game.perf.title} • ${context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context))}', + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); + } + } + }, + ); + }, + ), + if (game.lastFen != null) + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.image, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.screenshotCurrentPosition), + onPressed: () async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition(orientation, game.lastFen!, game.lastMove); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/${game.id}').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); + } + } }, - ), - ], + ); + }, ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text('PGN: ${context.l10n.downloadAnnotated}'), + onPressed: () async { + try { + final pgn = await ref.read(gameShareServiceProvider).annotatedPgn(game.id); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); + } + } + }, + ); + }, ), - ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + // TODO improve translation + child: Text('PGN: ${context.l10n.downloadRaw}'), + onPressed: () async { + try { + final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); + } + } + }, + ); + }, + ), + ], ); } } /// A list tile that shows extended game info including a result icon and analysis icon. class ExtendedGameListTile extends StatelessWidget { - const ExtendedGameListTile({ - required this.item, - this.padding, - }); + const ExtendedGameListTile({required this.item, this.padding}); final LightArchivedGameWithPov item; - final EdgeInsetsGeometry? padding; @override @@ -463,27 +379,14 @@ class ExtendedGameListTile extends StatelessWidget { final opponent = youAre == Side.white ? game.black : game.white; Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); + if (game.status == GameStatus.aborted || game.status == GameStatus.noStart) { + return const Icon(CupertinoIcons.xmark_square_fill, color: LichessColors.grey); } else { return game.winner == null - ? Icon( - CupertinoIcons.equal_square_fill, - color: context.lichessColors.brag, - ) + ? Icon(CupertinoIcons.equal_square_fill, color: context.lichessColors.brag) : game.winner == mySide - ? Icon( - CupertinoIcons.plus_square_fill, - color: context.lichessColors.good, - ) - : Icon( - CupertinoIcons.minus_square_fill, - color: context.lichessColors.error, - ); + ? Icon(CupertinoIcons.plus_square_fill, color: context.lichessColors.good) + : Icon(CupertinoIcons.minus_square_fill, color: context.lichessColors.error); } } @@ -491,41 +394,44 @@ class ExtendedGameListTile extends StatelessWidget { game: game, mySide: youAre, padding: padding, - onTap: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => game.fullId != null - ? GameScreen(initialGameId: game.fullId) - : ArchivedGameScreen( - gameData: game, - orientation: youAre, - ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, + onTap: + game.variant.isReadSupported + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => + game.fullId != null + ? GameScreen( + initialGameId: game.fullId, + loadingFen: game.lastFen, + loadingLastMove: game.lastMove, + loadingOrientation: youAre, + lastMoveAt: game.lastMoveAt, + ) + : ArchivedGameScreen(gameData: game, orientation: youAre), + ); + } + : () { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, + ); + }, icon: game.perf.icon, opponentTitle: UserFullNameWidget.player( user: opponent.user, aiLevel: opponent.aiLevel, rating: opponent.rating, ), - subtitle: Text(timeago.format(game.lastMoveAt)), + subtitle: Text(relativeDate(context.l10n, game.lastMoveAt, shortDate: false)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (me.analysis != null) ...[ - Icon( - CupertinoIcons.chart_bar_alt_fill, - color: textShade(context, 0.5), - ), + Icon(CupertinoIcons.chart_bar_alt_fill, color: textShade(context, 0.5)), const SizedBox(width: 5), ], getResultIcon(game, youAre), diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index 52145a83d4..0f7a3252c1 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -7,11 +6,11 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_numbers.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -29,11 +28,8 @@ class LobbyScreenLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -50,10 +46,7 @@ class LobbyScreenLoadingContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon( - seek.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(seek.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 8.0), Text( seek.timeIncrement?.display ?? @@ -78,7 +71,7 @@ class LobbyScreenLoadingContent extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () async { @@ -112,11 +105,8 @@ class ChallengeLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -157,7 +147,7 @@ class ChallengeLoadingContent extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () async { @@ -178,12 +168,7 @@ class ChallengeLoadingContent extends StatelessWidget { } class StandaloneGameLoadingBoard extends StatelessWidget { - const StandaloneGameLoadingBoard({ - this.fen, - this.lastMove, - this.orientation, - super.key, - }); + const StandaloneGameLoadingBoard({this.fen, this.lastMove, this.orientation, super.key}); final String? fen; final Side? orientation; @@ -192,12 +177,9 @@ class StandaloneGameLoadingBoard extends StatelessWidget { @override Widget build(BuildContext context) { return BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: orientation?.cg ?? cg.Side.white, - fen: fen ?? kEmptyFen, - lastMove: lastMove?.cg, - ), + orientation: orientation ?? Side.white, + fen: fen ?? kEmptyFen, + lastMove: lastMove as NormalMove?, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -218,11 +200,8 @@ class LoadGameError extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -230,7 +209,7 @@ class LoadGameError extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () => Navigator.of(context).pop(), @@ -245,36 +224,32 @@ class LoadGameError extends StatelessWidget { } } +/// A board that shows a message that a challenge has been declined. class ChallengeDeclinedBoard extends StatelessWidget { - const ChallengeDeclinedBoard({ - required this.declineReason, - this.destUser, - }); + const ChallengeDeclinedBoard({required this.declineReason, required this.challenge}); final String declineReason; - - final LightUser? destUser; + final Challenge challenge; @override Widget build(BuildContext context) { + final textColor = DefaultTextStyle.of(context).style.color; + return Column( children: [ Expanded( child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, boardOverlay: PlatformCard( elevation: 2.0, child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(16.0), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -284,18 +259,29 @@ class ChallengeDeclinedBoard extends StatelessWidget { context.l10n.challengeChallengeDeclined, style: Theme.of(context).textTheme.titleMedium, ), - const SizedBox(height: 16.0), - Text( - declineReason, - style: const TextStyle(fontStyle: FontStyle.italic), - ), - if (destUser != null) ...[ - const SizedBox(height: 8.0), + const SizedBox(height: 8.0), + Divider(height: 26.0, thickness: 0.0, color: textColor), + Text(declineReason, style: const TextStyle(fontStyle: FontStyle.italic)), + Divider(height: 26.0, thickness: 0.0, color: textColor), + if (challenge.destUser != null) Align( alignment: Alignment.centerRight, - child: UserFullNameWidget(user: destUser), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text(' — '), + UserFullNameWidget(user: challenge.destUser?.user), + if (challenge.destUser?.lagRating != null) ...[ + const SizedBox(width: 6.0), + LagIndicator( + lagRating: challenge.destUser!.lagRating!, + size: 13.0, + showLoadingIndicator: false, + ), + ], + ], + ), ), - ], ], ), ), @@ -304,7 +290,7 @@ class ChallengeDeclinedBoard extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () => Navigator.of(context).pop(), @@ -319,33 +305,6 @@ class ChallengeDeclinedBoard extends StatelessWidget { } } -class _BottomBar extends StatelessWidget { - const _BottomBar({ - required this.children, - }); - - final List children; - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - ), - ), - ), - ); - } -} - class _LobbyNumbers extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { @@ -354,13 +313,9 @@ class _LobbyNumbers extends ConsumerWidget { if (lobbyNumbers == null) { return Column( children: [ - Text( - context.l10n.nbPlayers(0).replaceAll('0', '...'), - ), + Text(context.l10n.nbPlayers(0).replaceAll('0', '...')), const SizedBox(height: 8.0), - Text( - context.l10n.nbGamesInPlay(0).replaceAll('0', '...'), - ), + Text(context.l10n.nbGamesInPlay(0).replaceAll('0', '...')), ], ); } else { diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 9dee3c6ad5..e73dc5b354 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -2,11 +2,15 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -25,17 +29,20 @@ class GamePlayer extends StatelessWidget { required this.player, this.clock, this.materialDiff, + this.materialDifferenceFormat, this.confirmMoveCallbacks, this.timeToMove, this.shouldLinkToUserProfile = true, this.mePlaying = false, this.zenMode = false, + this.clockPosition = ClockPosition.right, super.key, }); final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; + final MaterialDifferenceFormat? materialDifferenceFormat; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -43,6 +50,7 @@ class GamePlayer extends StatelessWidget { final bool shouldLinkToUserProfile; final bool mePlaying; final bool zenMode; + final ClockPosition clockPosition; /// Time left for the player to move at the start of the game. final Duration? timeToMove; @@ -50,8 +58,7 @@ class GamePlayer extends StatelessWidget { @override Widget build(BuildContext context) { final remaingHeight = estimateRemainingHeightLeftBoard(context); - final playerFontSize = - remaingHeight <= kSmallRemainingHeightLeftBoardThreshold ? 14.0 : 16.0; + final playerFontSize = remaingHeight <= kSmallRemainingHeightLeftBoardThreshold ? 14.0 : 16.0; final playerWidget = Column( mainAxisAlignment: MainAxisAlignment.center, @@ -59,13 +66,18 @@ class GamePlayer extends StatelessWidget { children: [ if (!zenMode) Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: + clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, children: [ - Icon( - player.onGame == true ? Icons.cloud : Icons.cloud_off, - color: player.onGame == true ? LichessColors.green : null, - size: 14, - ), + if (player.user != null) ...[ + Icon( + player.onGame == true ? Icons.cloud : Icons.cloud_off, + color: player.onGame == true ? LichessColors.green : null, + size: 14, + ), + ], const SizedBox(width: 5), if (player.user?.isPatron == true) ...[ Icon( @@ -80,11 +92,11 @@ class GamePlayer extends StatelessWidget { player.user!.title!, style: TextStyle( fontSize: playerFontSize, - fontWeight: - player.user?.title == 'BOT' ? null : FontWeight.bold, - color: player.user?.title == 'BOT' - ? context.lichessColors.fancy - : context.lichessColors.brag, + fontWeight: player.user?.title == 'BOT' ? null : FontWeight.bold, + color: + player.user?.title == 'BOT' + ? context.lichessColors.fancy + : context.lichessColors.brag, ), ), const SizedBox(width: 5), @@ -93,10 +105,7 @@ class GamePlayer extends StatelessWidget { child: Text( player.displayName(context), overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: playerFontSize, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: playerFontSize, fontWeight: FontWeight.w600), ), ), if (player.user?.flair != null) ...[ @@ -112,26 +121,22 @@ class GamePlayer extends StatelessWidget { RatingPrefAware( child: Text.rich( TextSpan( - text: - ' ${player.rating}${player.provisional == true ? '?' : ''}', + text: ' ${player.rating}${player.provisional == true ? '?' : ''}', children: [ if (player.ratingDiff != null) TextSpan( - text: - ' ${player.ratingDiff! > 0 ? '+' : ''}${player.ratingDiff}', + text: ' ${player.ratingDiff! > 0 ? '+' : ''}${player.ratingDiff}', style: TextStyle( - color: player.ratingDiff! > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + player.ratingDiff! > 0 + ? context.lichessColors.good + : context.lichessColors.error, ), ), ], ), overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: textShade(context, 0.7), - ), + style: TextStyle(fontSize: 14, color: textShade(context, 0.7)), ), ), ], @@ -139,30 +144,12 @@ class GamePlayer extends StatelessWidget { if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) else if (materialDiff != null) - Row( - children: [ - for (final role in Role.values) - for (int i = 0; i < materialDiff!.pieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff != null && materialDiff!.score > 0 - ? '+${materialDiff!.score}' - : '', - ), - ], - ) - else - // to avoid shifts use an empty text widget - const Text('', style: TextStyle(fontSize: 13)), + MaterialDifferenceDisplay( + materialDiff: materialDiff!, + materialDifferenceFormat: materialDifferenceFormat, + ), + // to avoid shifts use an empty text widget + const Text('', style: TextStyle(fontSize: 13)), ], ); @@ -170,11 +157,12 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (clock != null && clockPosition == ClockPosition.left) Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), + padding: const EdgeInsets.only(right: 16.0), child: ConfirmMove( onConfirm: confirmMoveCallbacks!.confirm, onCancel: confirmMoveCallbacks!.cancel, @@ -185,38 +173,36 @@ class GamePlayer extends StatelessWidget { Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), - child: shouldLinkToUserProfile - ? GestureDetector( - onTap: player.user != null - ? () { - pushPlatformRoute( - context, - builder: (context) => mePlaying - ? const ProfileScreen() - : UserScreen( - user: player.user!, - ), - ); - } - : null, - child: playerWidget, - ) - : playerWidget, + padding: const EdgeInsets.only(right: 16.0), + child: + shouldLinkToUserProfile + ? GestureDetector( + onTap: + player.user != null + ? () { + pushPlatformRoute( + context, + builder: + (context) => + mePlaying + ? const ProfileScreen() + : UserScreen(user: player.user!), + ); + } + : null, + child: playerWidget, + ) + : playerWidget, ), ), - if (clock != null) Flexible(flex: 3, child: clock!), + if (clock != null && clockPosition == ClockPosition.right) Flexible(flex: 3, child: clock!), ], ); } } class ConfirmMove extends StatelessWidget { - const ConfirmMove({ - required this.onConfirm, - required this.onCancel, - super.key, - }); + const ConfirmMove({required this.onConfirm, required this.onCancel, super.key}); final VoidCallback onConfirm; final VoidCallback onCancel; @@ -255,24 +241,21 @@ class ConfirmMove extends StatelessWidget { } } -class MoveExpiration extends StatefulWidget { - const MoveExpiration({ - required this.timeToMove, - required this.mePlaying, - super.key, - }); +class MoveExpiration extends ConsumerStatefulWidget { + const MoveExpiration({required this.timeToMove, required this.mePlaying, super.key}); final Duration timeToMove; final bool mePlaying; @override - State createState() => _MoveExpirationState(); + ConsumerState createState() => _MoveExpirationState(); } -class _MoveExpirationState extends State { +class _MoveExpirationState extends ConsumerState { static const _period = Duration(milliseconds: 1000); Timer? _timer; Duration timeLeft = Duration.zero; + bool playedEmergencySound = false; Timer startTimer() { return Timer.periodic(_period, (timer) { @@ -310,19 +293,56 @@ class _MoveExpirationState extends State { Widget build(BuildContext context) { final secs = timeLeft.inSeconds.remainder(60); final emerg = timeLeft <= const Duration(seconds: 8); + + if (emerg && widget.mePlaying && !playedEmergencySound) { + ref.read(soundServiceProvider).play(Sound.lowTime); + setState(() { + playedEmergencySound = true; + }); + } + return secs <= 20 ? Text( - context.l10n.nbSecondsToPlayTheFirstMove(secs), - style: TextStyle( - color: widget.mePlaying && emerg - ? context.lichessColors.error - : null, - ), - ) + context.l10n.nbSecondsToPlayTheFirstMove(secs), + style: TextStyle(color: widget.mePlaying && emerg ? context.lichessColors.error : null), + ) : const Text(''); } } +class MaterialDifferenceDisplay extends StatelessWidget { + const MaterialDifferenceDisplay({ + required this.materialDiff, + this.materialDifferenceFormat = MaterialDifferenceFormat.materialDifference, + }); + + final MaterialDiffSide materialDiff; + final MaterialDifferenceFormat? materialDifferenceFormat; + + @override + Widget build(BuildContext context) { + final IMap piecesToRender = + (materialDifferenceFormat == MaterialDifferenceFormat.capturedPieces + ? materialDiff.capturedPieces + : materialDiff.pieces); + + return materialDifferenceFormat?.visible ?? true + ? Row( + children: [ + for (final role in Role.values) + for (int i = 0; i < piecesToRender[role]!; i++) + Icon(_iconByRole[role], size: 13, color: Colors.grey), + const SizedBox(width: 3), + Text( + style: const TextStyle(fontSize: 13, color: Colors.grey), + materialDiff.score > 0 ? '+${materialDiff.score}' : '', + ), + ], + ) + : const SizedBox.shrink(); + } +} + const Map _iconByRole = { Role.king: LichessIcons.chess_king, Role.queen: LichessIcons.chess_queen, diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4c3dd5da43..6c456921ba 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -1,37 +1,29 @@ import 'dart:async'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/over_the_board_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/annotations.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'status_l10n.dart'; class GameResultDialog extends ConsumerStatefulWidget { - const GameResultDialog({ - required this.id, - required this.onNewOpponentCallback, - super.key, - }); + const GameResultDialog({required this.id, required this.onNewOpponentCallback, super.key}); final GameFullId id; @@ -50,19 +42,18 @@ Widget _adaptiveDialog(BuildContext context, Widget content) { ); final screenWidth = MediaQuery.of(context).size.width; - final paddedContent = Padding( - padding: const EdgeInsets.all(16.0), - child: content, - ); + final paddedContent = Padding(padding: const EdgeInsets.all(16.0), child: content); return Dialog( - backgroundColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve(dialogColor, context) - : null, + backgroundColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve(dialogColor, context) + : null, child: SizedBox( width: min(screenWidth, kMaterialPopupMenuMaxWidth), - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPopupSurface(child: paddedContent) - : paddedContent, + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoPopupSurface(child: paddedContent) + : paddedContent, ), ); } @@ -70,7 +61,6 @@ Widget _adaptiveDialog(BuildContext context, Widget content) { class _GameEndDialogState extends ConsumerState { late Timer _buttonActivationTimer; bool _activateButtons = false; - Future? _pendingAnalysisRequestFuture; @override void initState() { @@ -94,8 +84,6 @@ class _GameEndDialogState extends ConsumerState { Widget build(BuildContext context) { final ctrlProvider = gameControllerProvider(widget.id); final gameState = ref.watch(ctrlProvider).requireValue; - final session = ref.watch(authSessionProvider); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); final content = Column( mainAxisSize: MainAxisSize.min, @@ -105,104 +93,79 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: GameResult(game: gameState.game), ), - if (currentGameAnalysis == gameState.game.id) - const Padding( - padding: EdgeInsets.only(bottom: 16.0), - child: WaitingForServerAnalysis(), - ), - if (gameState.game.evals != null) - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: _AcplChart(evals: gameState.game.evals!), - ), - if (gameState.game.white.analysis != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: PlayerSummary(game: gameState.game), + AnimatedCrossFade( + duration: const Duration(milliseconds: 400), + firstCurve: Curves.easeOutExpo, + secondCurve: Curves.easeInExpo, + sizeCurve: Curves.easeInOut, + firstChild: const SizedBox.shrink(), + secondChild: Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Text('Your opponent has offered a rematch', textAlign: TextAlign.center), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + ), + ], + ), + ), + ], ), + crossFadeState: + gameState.game.opponent?.offeringRematch ?? false + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + ), if (gameState.game.me?.offeringRematch == true) SecondaryButton( semanticsLabel: context.l10n.cancelRematchOffer, onPressed: () { ref.read(ctrlProvider.notifier).declineRematch(); }, - child: Text( - context.l10n.cancelRematchOffer, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.cancelRematchOffer, textAlign: TextAlign.center), ) else if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, - onPressed: _activateButtons && - gameState.game.opponent?.onGame == true - ? () { - ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - } - : null, - glowing: gameState.game.opponent?.offeringRematch == true, - child: Text( - context.l10n.rematch, - textAlign: TextAlign.center, - ), + onPressed: + _activateButtons && + gameState.game.opponent?.onGame == true && + gameState.game.opponent?.offeringRematch != true + ? () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + } + : null, + child: Text(context.l10n.rematch, textAlign: TextAlign.center), ), if (gameState.canGetNewOpponent) SecondaryButton( semanticsLabel: context.l10n.newOpponent, - onPressed: _activateButtons - ? () { - Navigator.of(context) - .popUntil((route) => route is! PopupRoute); - widget.onNewOpponentCallback(gameState.game); - } - : null, - child: Text( - context.l10n.newOpponent, - textAlign: TextAlign.center, - ), - ), - if (currentGameAnalysis != gameState.game.id && - gameState.game.userAnalysable && - gameState.game.evals == null && - gameState.game.white.analysis == null) - FutureBuilder( - future: _pendingAnalysisRequestFuture, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: context.l10n.requestAComputerAnalysis, - onPressed: session == null + onPressed: + _activateButtons ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : _activateButtons - ? snapshot.connectionState == ConnectionState.waiting - ? null - : () { - setState(() { - _pendingAnalysisRequestFuture = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - } - : null, - child: Text( - context.l10n.requestAComputerAnalysis, - textAlign: TextAlign.center, - ), - ); - }, + Navigator.of(context).popUntil((route) => route is! PopupRoute); + widget.onNewOpponentCallback(gameState.game); + } + : null, + child: Text(context.l10n.newOpponent, textAlign: TextAlign.center), ), if (gameState.game.userAnalysable) SecondaryButton( @@ -210,17 +173,10 @@ class _GameEndDialogState extends ConsumerState { onPressed: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, - options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, - ), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, - child: Text( - context.l10n.analysis, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.analysis, textAlign: TextAlign.center), ), ], ); @@ -229,69 +185,29 @@ class _GameEndDialogState extends ConsumerState { } } -class _AcplChart extends StatelessWidget { - final IList evals; +class ArchivedGameResultDialog extends StatelessWidget { + const ArchivedGameResultDialog({required this.game, super.key}); - const _AcplChart({required this.evals}); + final BaseGame game; @override Widget build(BuildContext context) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - final spots = evals - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - return AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0), - child: LineChart( - LineChartData( - minY: -1.0, - maxY: 1.0, - lineTouchData: const LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: true, - color: mainLineColor.withOpacity(0.3), - barWidth: 1, - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), + final content = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [GameResult(game: game), const SizedBox(height: 16.0), PlayerSummary(game: game)], ); + + return _adaptiveDialog(context, content); } } -class ArchivedGameResultDialog extends StatelessWidget { - const ArchivedGameResultDialog({required this.game, super.key}); +class OverTheBoardGameResultDialog extends StatelessWidget { + const OverTheBoardGameResultDialog({super.key, required this.game, required this.onRematch}); - final BaseGame game; + final OverTheBoardGame game; + + final void Function() onRematch; @override Widget build(BuildContext context) { @@ -300,8 +216,31 @@ class ArchivedGameResultDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ GameResult(game: game), - const SizedBox(height: 16.0), - PlayerSummary(game: game), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + onPressed: onRematch, + child: Text(context.l10n.rematch, textAlign: TextAlign.center), + ), + SecondaryButton( + semanticsLabel: context.l10n.analysis, + onPressed: () { + pushPlatformRoute( + context, + builder: + (_) => AnalysisScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: game.makePgn(), + isComputerAnalysisAllowed: true, + variant: game.meta.variant, + ), + ), + ), + ); + }, + child: Text(context.l10n.analysis, textAlign: TextAlign.center), + ), ], ); @@ -324,22 +263,14 @@ class PlayerSummary extends ConsumerWidget { return const SizedBox.shrink(); } - Widget makeStatCol( - int value, - String Function(int count) labelFn, - Color? color, - ) { + Widget makeStatCol(int value, String Function(int count) labelFn, Color? color) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( value.toString(), - style: TextStyle( - fontSize: 18.0, - color: color, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontSize: 18.0, color: color, fontWeight: FontWeight.bold), ), const SizedBox(height: 4.0), FittedBox( @@ -384,9 +315,10 @@ class GameResult extends StatelessWidget { @override Widget build(BuildContext context) { - final showWinner = game.winner != null - ? ' • ${game.winner == Side.white ? context.l10n.whiteIsVictorious : context.l10n.blackIsVictorious}' - : ''; + final showWinner = + game.winner != null + ? ' • ${game.winner == Side.white ? context.l10n.whiteIsVictorious : context.l10n.blackIsVictorious}' + : ''; return Column( mainAxisSize: MainAxisSize.min, @@ -396,27 +328,15 @@ class GameResult extends StatelessWidget { game.winner == null ? '½-½' : game.winner == Side.white - ? '1-0' - : '0-1', - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), + ? '1-0' + : '0-1', + style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 6.0), Text( - '${gameStatusL10n( - context, - variant: game.meta.variant, - status: game.status, - lastPosition: game.lastPosition, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - )}$showWinner', - style: const TextStyle( - fontStyle: FontStyle.italic, - ), + '${gameStatusL10n(context, variant: game.meta.variant, status: game.status, lastPosition: game.lastPosition, winner: game.winner, isThreefoldRepetition: game.isThreefoldRepetition)}$showWinner', + style: const TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center, ), ], diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 94528e2550..7b806549f5 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -4,64 +4,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_loading_board.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen_providers.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'game_body.dart'; import 'game_common_widgets.dart'; -part 'game_screen.g.dart'; - -@riverpod -class _LoadGame extends _$LoadGame { - @override - Future<(GameFullId?, DeclineReason?)> build( - GameSeek? seek, - ChallengeRequest? challenge, - GameFullId? gameId, - ) { - assert( - gameId != null || seek != null || challenge != null, - 'Either a seek, challenge or a game id must be provided.', - ); - - final service = ref.watch(createGameServiceProvider); - - if (seek != null) { - return service.newLobbyGame(seek).then((id) => (id, null)); - } else if (challenge != null) { - return service.newChallenge(challenge).then((c) => (c.$1, c.$2)); - } - - return Future.value((gameId!, null)); - } - - /// Search for a new opponent (lobby only). - Future newOpponent() async { - if (seek != null) { - final service = ref.read(createGameServiceProvider); - state = const AsyncValue.loading(); - state = AsyncValue.data( - await service.newLobbyGame(seek!).then((id) => (id, null)), - ); - } - } - - /// Load a game from its id. - void loadGame(GameFullId id) { - state = AsyncValue.data((id, null)); - } -} - +/// Screen to play a game, or to show a challenge or to show current user's past games. +/// +/// The screen can be created in three ways: +/// - From the lobby, to play a game with a random opponent: using a [GameSeek] as [seek]. +/// - From a challenge, to accept or decline a challenge: using a [ChallengeRequest] as [challenge]. +/// - From a game id, to show a game that is already in progress: using a [GameFullId] as [initialGameId]. +/// +/// The screen will show a loading board while the game is being created. class GameScreen extends ConsumerStatefulWidget { const GameScreen({ this.seek, @@ -70,12 +36,14 @@ class GameScreen extends ConsumerStatefulWidget { this.loadingFen, this.loadingLastMove, this.loadingOrientation, + this.lastMoveAt, super.key, }) : assert( - initialGameId != null || seek != null || challenge != null, - 'Either a seek, a challenge or an initial game id must be provided.', - ); + initialGameId != null || seek != null || challenge != null, + 'Either a seek, a challenge or an initial game id must be provided.', + ); + // tweak final GameSeek? seek; final GameFullId? initialGameId; @@ -86,6 +54,9 @@ class GameScreen extends ConsumerStatefulWidget { final Move? loadingLastMove; final Side? loadingOrientation; + /// The date of the last move played in the game. If null, the game is in progress. + final DateTime? lastMoveAt; + _GameSource get source { if (initialGameId != null) { return _GameSource.game; @@ -124,9 +95,7 @@ class _GameScreenState extends ConsumerState with RouteAware { @override void didPop() { - if (mounted && - (widget.source == _GameSource.lobby || - widget.source == _GameSource.challenge)) { + if (mounted && (widget.source == _GameSource.lobby || widget.source == _GameSource.challenge)) { ref.invalidate(myRecentGamesProvider); ref.invalidate(accountProvider); } @@ -135,119 +104,99 @@ class _GameScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { - final gameProvider = - _loadGameProvider(widget.seek, widget.challenge, widget.initialGameId); - - return ref.watch(gameProvider).when( - data: (data) { - final (gameId, declineReason) = data; - final body = gameId != null - ? GameBody( - id: gameId, - loadingBoardWidget: const StandaloneGameLoadingBoard(), - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, - boardKey: _boardKey, - onLoadGameCallback: (id) { - ref.read(gameProvider.notifier).loadGame(id); - }, - onNewOpponentCallback: (game) { - if (widget.source == _GameSource.lobby) { - ref.read(gameProvider.notifier).newOpponent(); - } else { - final savedSetup = ref.read(gameSetupPreferencesProvider); - pushReplacementPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.newOpponentFromGame(game, savedSetup), + final provider = currentGameProvider(widget.seek, widget.challenge, widget.initialGameId); + + return ref + .watch(provider) + .when( + data: (data) { + final (gameFullId: gameId, challenge: challenge, declineReason: declineReason) = data; + final body = + gameId != null + ? GameBody( + id: gameId, + loadingBoardWidget: StandaloneGameLoadingBoard( + fen: widget.loadingFen, + lastMove: widget.loadingLastMove, + orientation: widget.loadingOrientation, ), + whiteClockKey: _whiteClockKey, + blackClockKey: _blackClockKey, + boardKey: _boardKey, + onLoadGameCallback: (id) { + ref.read(provider.notifier).loadGame(id); + }, + onNewOpponentCallback: (game) { + if (widget.source == _GameSource.lobby) { + ref.read(provider.notifier).newOpponent(); + } else { + final savedSetup = ref.read(gameSetupPreferencesProvider); + pushReplacementPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.newOpponentFromGame(game, savedSetup), + ), + ); + } + }, + ) + : widget.challenge != null && challenge != null + ? ChallengeDeclinedBoard( + challenge: challenge, + declineReason: + declineReason != null + ? declineReason.label(context.l10n) + : ChallengeDeclineReason.generic.label(context.l10n), + ) + : const LoadGameError('Could not create the game.'); + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(id: gameId, lastMoveAt: widget.lastMoveAt), + body: body, + ); + }, + loading: () { + final loadingBoard = + widget.seek != null + ? LobbyScreenLoadingContent( + widget.seek!, + () => ref.read(createGameServiceProvider).cancelSeek(), + ) + : widget.challenge != null + ? ChallengeLoadingContent( + widget.challenge!, + () => ref.read(createGameServiceProvider).cancelChallenge(), + ) + : const StandaloneGameLoadingBoard(); + + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), + body: PopScope(canPop: false, child: loadingBoard), + ); + }, + error: (e, s) { + debugPrint('SEVERE: [GameScreen] could not create game; $e\n$s'); + + // lichess sends a 400 response if user has disallowed challenges + final message = + e is ServerException && e.statusCode == 400 + ? LoadGameError( + 'Could not create the game: ${e.jsonError?['error'] as String?}', + ) + : const LoadGameError( + 'Sorry, we could not create the game. Please try again later.', ); - } - }, - ) - : widget.challenge != null - ? ChallengeDeclinedBoard( - declineReason: declineReason != null - ? declineReasonMessage(context, declineReason) - : declineReasonMessage(context, DeclineReason.generic), - destUser: widget.challenge?.destUser, - ) - : const LoadGameError('Could not create the game.'); - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(id: gameId), - body: body, - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(id: gameId), - child: body, - ), - ); - }, - loading: () { - final loadingBoard = widget.seek != null - ? LobbyScreenLoadingContent( - widget.seek!, - () => ref.read(createGameServiceProvider).cancelSeek(), - ) - : widget.challenge != null - ? ChallengeLoadingContent( - widget.challenge!, - () => ref.read(createGameServiceProvider).cancelChallenge(), - ) - : const StandaloneGameLoadingBoard(); - - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek), - body: PopScope( - canPop: false, - child: loadingBoard, - ), - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(seek: widget.seek), - child: PopScope( - canPop: false, - child: loadingBoard, - ), - ), - ); - }, - error: (e, s) { - debugPrint( - 'SEVERE: [GameScreen] could not create game; $e\n$s', - ); - // lichess sends a 400 response if user has disallowed challenges - final message = e is ServerException && e.statusCode == 400 - ? LoadGameError( - 'Could not create the game: ${e.jsonError?['error'] as String?}', - ) - : const LoadGameError( - 'Sorry, we could not create the game. Please try again later.', - ); - - final body = PopScope(child: message); - - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek), - body: body, - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(seek: widget.seek), - child: body, - ), + final body = PopScope(child: message); + + return PlatformScaffold( + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), + body: body, + ); + }, ); - }, - ); } } diff --git a/lib/src/view/game/game_screen_providers.dart b/lib/src/view/game/game_screen_providers.dart index 73265b8446..8f138ae38c 100644 --- a/lib/src/view/game/game_screen_providers.dart +++ b/lib/src/view/game/game_screen_providers.dart @@ -1,11 +1,60 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; +import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'game_screen_providers.g.dart'; +/// A provider that returns the currently loaded [GameFullId] for the [GameScreen]. +/// +/// If the [gameId] is provided, it will simply return it. +/// If not, it uses the [CreateGameService] to create a new game from the lobby or from a challenge. +@riverpod +class CurrentGame extends _$CurrentGame { + @override + Future build(GameSeek? seek, ChallengeRequest? challenge, GameFullId? gameId) { + assert( + gameId != null || seek != null || challenge != null, + 'Either a seek, challenge or a game id must be provided.', + ); + + final service = ref.watch(createGameServiceProvider); + + if (seek != null) { + return service + .newLobbyGame(seek) + .then((id) => (gameFullId: id, challenge: null, declineReason: null)); + } else if (challenge != null) { + return service.newRealTimeChallenge(challenge); + } + + return Future.value((gameFullId: gameId!, challenge: null, declineReason: null)); + } + + /// Search for a new opponent (lobby only). + Future newOpponent() async { + if (seek != null) { + final service = ref.read(createGameServiceProvider); + state = const AsyncValue.loading(); + state = AsyncValue.data( + await service + .newLobbyGame(seek!) + .then((id) => (gameFullId: id, challenge: null, declineReason: null)), + ); + } + } + + /// Load a game from its id. + void loadGame(GameFullId id) { + state = AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); + } +} + @riverpod class IsBoardTurned extends _$IsBoardTurned { @override @@ -19,53 +68,35 @@ class IsBoardTurned extends _$IsBoardTurned { } @riverpod -Future shouldPreventGoingBack( - ShouldPreventGoingBackRef ref, - GameFullId gameId, -) { +Future shouldPreventGoingBack(Ref ref, GameFullId gameId) { return ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => - state.game.meta.speed != Speed.correspondence && state.game.playable, - ), + gameControllerProvider( + gameId, + ).selectAsync((state) => state.game.meta.speed != Speed.correspondence && state.game.playable), ); } /// User game preferences, defined server-side. @riverpod -Future< - ({ - GamePrefs? prefs, - bool shouldConfirmMove, - bool isZenModeEnabled, - bool canAutoQueen - })> userGamePrefs( - UserGamePrefsRef ref, - GameFullId gameId, -) async { +Future<({ServerGamePrefs? prefs, bool shouldConfirmMove, bool isZenModeEnabled, bool canAutoQueen})> +userGamePrefs(Ref ref, GameFullId gameId) async { final prefs = await ref.watch( gameControllerProvider(gameId).selectAsync((state) => state.game.prefs), ); final shouldConfirmMove = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.shouldConfirmMove, - ), + gameControllerProvider(gameId).selectAsync((state) => state.shouldConfirmMove), ); final isZenModeEnabled = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.isZenModeEnabled, - ), + gameControllerProvider(gameId).selectAsync((state) => state.isZenModeEnabled), ); final canAutoQueen = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.canAutoQueen, - ), + gameControllerProvider(gameId).selectAsync((state) => state.canAutoQueen), ); return ( prefs: prefs, shouldConfirmMove: shouldConfirmMove, isZenModeEnabled: isZenModeEnabled, - canAutoQueen: canAutoQueen + canAutoQueen: canAutoQueen, ); } @@ -73,11 +104,6 @@ Future< /// /// This is data that won't change during the game. @riverpod -Future gameMeta( - GameMetaRef ref, - GameFullId gameId, -) async { - return await ref.watch( - gameControllerProvider(gameId).selectAsync((state) => state.game.meta), - ); +Future gameMeta(Ref ref, GameFullId gameId) async { + return await ref.watch(gameControllerProvider(gameId).selectAsync((state) => state.game.meta)); } diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 9818b140ae..592709eb63 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,17 +1,15 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; - import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { @@ -21,137 +19,65 @@ class GameSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select( - (prefs) => prefs.isSoundEnabled, - ), - ); - final boardPrefs = ref.watch(boardPreferencesProvider); final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); - final List content = [ - PlatformListTile( - title: Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); - }, - ), - ...userPrefsAsync.maybeWhen( - data: (data) { - return [ - if (data.prefs?.submitMove == true) - SwitchSettingTile( - title: Text( - context.l10n.preferencesMoveConfirmation, + return BottomSheetScrollableContainer( + children: [ + ...userPrefsAsync.maybeWhen( + data: (data) { + return [ + if (data.prefs?.submitMove == true) + SwitchSettingTile( + title: Text(context.l10n.preferencesMoveConfirmation), + value: data.shouldConfirmMove, + onChanged: (value) { + ref.read(gameControllerProvider(id).notifier).toggleMoveConfirmation(); + }, ), - value: data.shouldConfirmMove, - onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleMoveConfirmation(); - }, - ), - if (data.prefs?.autoQueen == AutoQueen.always) - SwitchSettingTile( - title: Text( - context.l10n.preferencesPromoteToQueenAutomatically, + if (data.prefs?.autoQueen == AutoQueen.always) + SwitchSettingTile( + title: Text(context.l10n.preferencesPromoteToQueenAutomatically), + value: data.canAutoQueen, + onChanged: (value) { + ref.read(gameControllerProvider(id).notifier).toggleAutoQueen(); + }, ), - value: data.canAutoQueen, + SwitchSettingTile( + title: Text(context.l10n.preferencesZenMode), + value: data.isZenModeEnabled, onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleAutoQueen(); + ref.read(gameControllerProvider(id).notifier).toggleZenMode(); }, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesZenMode, - ), - value: data.isZenModeEnabled, - onChanged: (value) { - ref.read(gameControllerProvider(id).notifier).toggleZenMode(); - }, - ), - ]; - }, - orElse: () => [], - ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, + ]; + }, + orElse: () => [], ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, + PlatformListTile( + // TODO translate + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute(context, fullscreenDialog: true, screen: const BoardSettingsScreen()); + }, ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, + SwitchSettingTile( + title: Text(context.l10n.toggleTheChat), + value: gamePrefs.enableChat ?? false, + onChanged: (value) { + ref.read(gamePreferencesProvider.notifier).toggleChat(); + ref.read(gameControllerProvider(id).notifier).onToggleChat(value); + }, ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.toggleTheChat, + SwitchSettingTile( + title: Text(context.l10n.preferencesBlindfold), + value: gamePrefs.blindfoldMode ?? false, + onChanged: (value) { + ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); + }, ), - value: gamePrefs.enableChat ?? false, - onChanged: (value) { - ref.read(gamePreferencesProvider.notifier).toggleChat(); - ref.read(gameControllerProvider(id).notifier).onToggleChat(value); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileBlindfoldMode), - value: gamePrefs.blindfoldMode ?? false, - onChanged: (value) { - ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); - }, - ), - const SizedBox(height: 16.0), - ]; - - return DraggableScrollableSheet( - initialChildSize: 1.0, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: content, - ), + ], ); } } diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index 3b91dbb864..2e21301209 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -12,18 +12,14 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class MessageScreen extends ConsumerStatefulWidget { final GameFullId id; final Widget title; final LightUser? me; - const MessageScreen({ - required this.id, - required this.title, - this.me, - }); + const MessageScreen({required this.id, required this.title, this.me}); @override ConsumerState createState() => _MessageScreenState(); @@ -53,37 +49,9 @@ class _MessageScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { - final body = _Body(me: widget.me, id: widget.id); - - return PlatformWidget( - androidBuilder: (context) => - _androidBuilder(context: context, body: body), - iosBuilder: (context) => _iosBuilder(context: context, body: body), - ); - } - - Widget _androidBuilder({ - required BuildContext context, - required Widget body, - }) { - return Scaffold( - appBar: AppBar( - title: widget.title, - centerTitle: true, - ), - body: body, - ); - } - - Widget _iosBuilder({ - required BuildContext context, - required Widget body, - }) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: widget.title, - ), - child: body, + return PlatformScaffold( + appBar: PlatformAppBar(title: widget.title, centerTitle: true), + body: _Body(me: widget.me, id: widget.id), ); } } @@ -92,10 +60,7 @@ class _Body extends ConsumerWidget { final GameFullId id; final LightUser? me; - const _Body({ - required this.id, - required this.me, - }); + const _Body({required this.id, required this.me}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -108,35 +73,29 @@ class _Body extends ConsumerWidget { child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: chatStateAsync.when( - data: (chatState) => ListView.builder( - // remove the automatic bottom padding of the ListView, which on iOS - // corresponds to the safe area insets - // and which is here taken care of by the _ChatBottomBar - padding: MediaQuery.of(context).padding.copyWith(bottom: 0), - reverse: true, - itemCount: chatState.messages.length, - itemBuilder: (context, index) { - final message = - chatState.messages[chatState.messages.length - index - 1]; - return (message.username == 'lichess') - ? _MessageAction(message: message.message) - : (message.username == me?.name) - ? _MessageBubble( - you: true, - message: message.message, - ) - : _MessageBubble( - you: false, - message: message.message, - ); - }, - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, _) => Center( - child: Text(error.toString()), - ), + data: (chatState) { + final selectedMessages = + chatState.messages.where((m) => !m.troll && !m.deleted && !isSpam(m)).toList(); + final messagesCount = selectedMessages.length; + return ListView.builder( + // remove the automatic bottom padding of the ListView, which on iOS + // corresponds to the safe area insets + // and which is here taken care of by the _ChatBottomBar + padding: MediaQuery.of(context).padding.copyWith(bottom: 0), + reverse: true, + itemCount: messagesCount, + itemBuilder: (context, index) { + final message = selectedMessages[messagesCount - index - 1]; + return (message.username == 'lichess') + ? _MessageAction(message: message.message) + : (message.username == me?.name) + ? _MessageBubble(you: true, message: message.message) + : _MessageBubble(you: false, message: message.message); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center(child: Text(error.toString())), ), ), ), @@ -158,10 +117,10 @@ class _MessageBubble extends ConsumerWidget { ? Theme.of(context).colorScheme.primaryContainer : CupertinoColors.systemGrey4.resolveFrom(context) : you - ? Theme.of(context).colorScheme.primaryContainer - : brightness == Brightness.light - ? lighten(LichessColors.grey) - : darken(LichessColors.grey, 0.5); + ? Theme.of(context).colorScheme.primaryContainer + : brightness == Brightness.light + ? lighten(LichessColors.grey) + : darken(LichessColors.grey, 0.5); Color _textColor(BuildContext context, Brightness brightness) => Theme.of(context).platform == TargetPlatform.iOS @@ -169,10 +128,10 @@ class _MessageBubble extends ConsumerWidget { ? Theme.of(context).colorScheme.onPrimaryContainer : CupertinoColors.label.resolveFrom(context) : you - ? Theme.of(context).colorScheme.onPrimaryContainer - : brightness == Brightness.light - ? Colors.black - : Colors.white; + ? Theme.of(context).colorScheme.onPrimaryContainer + : brightness == Brightness.light + ? Colors.black + : Colors.white; @override Widget build(BuildContext context, WidgetRef ref) { @@ -190,12 +149,7 @@ class _MessageBubble extends ConsumerWidget { borderRadius: BorderRadius.circular(16.0), color: _bubbleColor(context, brightness), ), - child: Text( - message, - style: TextStyle( - color: _textColor(context, brightness), - ), - ), + child: Text(message, style: TextStyle(color: _textColor(context, brightness))), ), ), ); @@ -247,40 +201,36 @@ class _ChatBottomBarState extends ConsumerState<_ChatBottomBar> { final session = ref.watch(authSessionProvider); final sendButton = ValueListenableBuilder( valueListenable: _textController, - builder: (context, value, child) => PlatformIconButton( - onTap: session != null && value.text.isNotEmpty - ? () { - ref - .read(chatControllerProvider(widget.id).notifier) - .sendMessage(_textController.text); - _textController.clear(); - } - : null, - icon: Icons.send, - padding: EdgeInsets.zero, - semanticsLabel: context.l10n.send, - ), + builder: + (context, value, child) => PlatformIconButton( + onTap: + session != null && value.text.isNotEmpty + ? () { + ref + .read(chatControllerProvider(widget.id).notifier) + .sendMessage(_textController.text); + _textController.clear(); + } + : null, + icon: Icons.send, + padding: EdgeInsets.zero, + semanticsLabel: context.l10n.send, + ), ); - final placeholder = - session != null ? context.l10n.talkInChat : context.l10n.loginToChat; + final placeholder = session != null ? context.l10n.talkInChat : context.l10n.loginToChat; return SafeArea( top: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: AdaptiveTextField( materialDecoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), suffixIcon: sendButton, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20.0)), - ), + border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20.0))), hintText: placeholder, ), cupertinoDecoration: BoxDecoration( - border: Border.all( - color: CupertinoColors.separator.resolveFrom(context), - ), + border: Border.all(color: CupertinoColors.separator.resolveFrom(context)), borderRadius: const BorderRadius.all(Radius.circular(30.0)), ), placeholder: placeholder, diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index 5e6cc6198f..c3aad1e893 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -4,41 +4,26 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; class OfflineCorrespondenceGamesScreen extends ConsumerWidget { const OfflineCorrespondenceGamesScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context, WidgetRef ref) { final offlineGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); - return Scaffold( - appBar: offlineGames.maybeWhen( - data: (data) => - AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), - orElse: () => AppBar(title: const SizedBox.shrink()), + return PlatformScaffold( + appBar: PlatformAppBar( + title: offlineGames.maybeWhen( + data: (data) => Text(context.l10n.nbGamesInPlay(data.length)), + orElse: () => const SizedBox.shrink(), + ), ), body: _Body(), ); @@ -50,17 +35,15 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final offlineGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineGames.maybeWhen( - data: (data) => ListView( - children: [ - const SizedBox(height: 8.0), - ...data.map( - (game) => OfflineCorrespondenceGamePreview( - game: game.$2, - lastModified: game.$1, - ), + data: + (data) => ListView( + children: [ + const SizedBox(height: 8.0), + ...data.map( + (game) => OfflineCorrespondenceGamePreview(game: game.$2, lastModified: game.$1), + ), + ], ), - ], - ), orElse: () => const SizedBox.shrink(), ); } @@ -77,38 +60,24 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SmallBoardPreview( - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, + orientation: game.orientation, + lastMove: game.lastMove, fen: game.lastPosition.fen, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - UserFullNameWidget( - user: game.opponent.user, - style: Styles.boardPreviewTitle, - ), + UserFullNameWidget(user: game.opponent.user, style: Styles.boardPreviewTitle), if (game.myTimeLeft(lastModified) != null) - Text( - timeago.format( - DateTime.now().add(game.myTimeLeft(lastModified)!), - allowFromNow: true, - ), - ), - Icon( - game.perf.icon, - size: 40, - color: DefaultTextStyle.of(context).style.color, - ), + Text(relativeDate(context.l10n, DateTime.now().add(game.myTimeLeft(lastModified)!))), + Icon(game.perf.icon, size: 40, color: DefaultTextStyle.of(context).style.color), ], ), onTap: () { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => OfflineCorrespondenceGameScreen( - initialGame: (lastModified, game), - ), + builder: (_) => OfflineCorrespondenceGameScreen(initialGame: (lastModified, game)), ); }, ); diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index e5297b7ce5..f38a98645b 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -1,82 +1,34 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:signal_strength_indicator/signal_strength_indicator.dart'; - -part 'ping_rating.g.dart'; - -const spinKit = SpinKitThreeBounce( - color: Colors.grey, - size: 15, -); @riverpod -int pingRating(PingRatingRef ref) { +final pingRatingProvider = Provider.autoDispose((ref) { final ping = ref.watch(averageLagProvider).inMicroseconds / 1000; return ping == 0 ? 0 : ping < 150 - ? 4 - : ping < 300 - ? 3 - : ping < 500 - ? 2 - : 1; -} + ? 4 + : ping < 300 + ? 3 + : ping < 500 + ? 2 + : 1; +}); -class PingRating extends ConsumerWidget { - const PingRating({ - required this.size, - super.key, - }); +class SocketPingRating extends ConsumerWidget { + const SocketPingRating({required this.size, super.key}); final double size; - static const cupertinoLevels = { - 0: CupertinoColors.systemRed, - 1: CupertinoColors.systemYellow, - 2: CupertinoColors.systemGreen, - 3: CupertinoColors.systemGreen, - }; - - static const materialLevels = { - 0: Colors.red, - 1: Colors.yellow, - 2: Colors.green, - 3: Colors.green, - }; - @override Widget build(BuildContext context, WidgetRef ref) { final pingRating = ref.watch(pingRatingProvider); - return SizedBox.square( - dimension: size, - child: Stack( - children: [ - SignalStrengthIndicator.bars( - barCount: 4, - minValue: 1, - maxValue: 4, - value: pingRating, - size: size, - inactiveColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey, - context, - ).withOpacity(0.2) - : Colors.grey.withOpacity(0.2), - levels: Theme.of(context).platform == TargetPlatform.iOS - ? cupertinoLevels - : materialLevels, - ), - if (pingRating == 0) spinKit, - ], - ), - ); + return LagIndicator(lagRating: pingRating, size: size, showLoadingIndicator: true); } } diff --git a/lib/src/view/game/status_l10n.dart b/lib/src/view/game/status_l10n.dart index 2b74280e86..a08a3d003c 100644 --- a/lib/src/view/game/status_l10n.dart +++ b/lib/src/view/game/status_l10n.dart @@ -20,9 +20,7 @@ String gameStatusL10n( case GameStatus.mate: return context.l10n.checkmate; case GameStatus.resign: - return winner == Side.black - ? context.l10n.whiteResigned - : context.l10n.blackResigned; + return winner == Side.black ? context.l10n.whiteResigned : context.l10n.blackResigned; case GameStatus.stalemate: return context.l10n.stalemate; case GameStatus.timeout: @@ -31,8 +29,8 @@ String gameStatusL10n( ? '${context.l10n.whiteLeftTheGame} • ${context.l10n.draw}' : '${context.l10n.blackLeftTheGame} • ${context.l10n.draw}' : winner == Side.black - ? context.l10n.whiteLeftTheGame - : context.l10n.blackLeftTheGame; + ? context.l10n.whiteLeftTheGame + : context.l10n.blackLeftTheGame; case GameStatus.draw: if (lastPosition.isInsufficientMaterial) { return '${context.l10n.insufficientMaterial} • ${context.l10n.draw}'; @@ -47,12 +45,10 @@ String gameStatusL10n( ? '${context.l10n.whiteTimeOut} • ${context.l10n.draw}' : '${context.l10n.blackTimeOut} • ${context.l10n.draw}' : winner == Side.black - ? context.l10n.whiteTimeOut - : context.l10n.blackTimeOut; + ? context.l10n.whiteTimeOut + : context.l10n.blackTimeOut; case GameStatus.noStart: - return winner == Side.black - ? context.l10n.whiteDidntMove - : context.l10n.blackDidntMove; + return winner == Side.black ? context.l10n.whiteDidntMove : context.l10n.blackDidntMove; case GameStatus.unknownFinish: return context.l10n.finished; case GameStatus.cheat: diff --git a/lib/src/view/home/create_game_options.dart b/lib/src/view/home/create_game_options.dart deleted file mode 100644 index 653da12cd5..0000000000 --- a/lib/src/view/home/create_game_options.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/play/create_custom_game_screen.dart'; -import 'package:lichess_mobile/src/view/play/online_bots_screen.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; - -class CreateGameOptions extends ConsumerWidget { - const CreateGameOptions(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Theme.of(context).platform == TargetPlatform.iOS - ? ListSection( - hasLeading: true, - children: [ - PlatformListTile( - leading: const Icon( - Icons.tune, - size: 28, - ), - trailing: const CupertinoListTileChevron(), - title: - Text(context.l10n.custom, style: Styles.mainListTileTitle), - onTap: () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - }, - ), - PlatformListTile( - title: Text( - context.l10n.onlineBots, - style: Styles.mainListTileTitle, - ), - leading: const Icon( - Icons.computer, - size: 28, - ), - trailing: const CupertinoListTileChevron(), - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - }, - ), - ], - ) - : Padding( - padding: Styles.bodySectionPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton.icon( - onPressed: () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - }, - icon: const Icon(Icons.tune), - label: Text(context.l10n.custom), - ), - ElevatedButton.icon( - onPressed: () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - }, - icon: const Icon(Icons.computer), - label: Text(context.l10n.onlineBots), - ), - ], - ), - ); - } -} diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 7a04182745..e357b686b3 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -6,13 +6,15 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -21,20 +23,19 @@ import 'package:lichess_mobile/src/view/account/profile_screen.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/game/offline_correspondence_games_screen.dart'; -import 'package:lichess_mobile/src/view/home/create_game_options.dart'; +import 'package:lichess_mobile/src/view/play/create_game_options.dart'; import 'package:lichess_mobile/src/view/play/ongoing_games_screen.dart'; import 'package:lichess_mobile/src/view/play/play_screen.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; import 'package:lichess_mobile/src/view/play/quick_game_matrix.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:lichess_mobile/src/view/user/player_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/board_carousel_item.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/misc.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; final editModeProvider = StateProvider((ref) => false); @@ -61,359 +62,184 @@ class _HomeScreenState extends ConsumerState with RouteAware { if (!hasRefreshed && !wasOnline && isNowOnline) { hasRefreshed = true; - _refreshData(); + _refreshData(isOnline: isNowOnline); } wasOnline = isNowOnline; } }); - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - final isTablet = isTabletOrLarger(context); - final isEditing = ref.watch(editModeProvider); - return Scaffold( - appBar: AppBar( - title: const Text('lichess.org'), - actions: [ - IconButton( - onPressed: () { - ref.read(editModeProvider.notifier).state = !isEditing; - }, - icon: Icon(isEditing ? Icons.save : Icons.app_registration), - tooltip: isEditing ? 'Save' : 'Edit', - ), - const _PlayerScreenButton(), - ], - ), - body: RefreshIndicator( - key: _androidRefreshKey, - onRefresh: () => _refreshData(), - child: const Column( - children: [ - ConnectivityBanner(), - Expanded(child: _HomeBody()), - ], - ), - ), - floatingActionButton: isTablet - ? null - : FloatingActionButton.extended( - onPressed: () { - pushPlatformRoute( - context, - builder: (_) => const PlayScreen(), - ); - }, - icon: const Icon(Icons.add), - label: Text(context.l10n.play), - ), - ); - } - - Widget _iosBuilder(BuildContext context) { - final isEditing = ref.watch(editModeProvider); - return CupertinoPageScaffold( - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - CustomScrollView( - controller: homeScrollController, - slivers: [ - CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), - largeTitle: Text(context.l10n.mobileHomeTab), - leading: CupertinoButton( - alignment: Alignment.centerLeft, - padding: EdgeInsets.zero, - onPressed: () { - ref.read(editModeProvider.notifier).state = !isEditing; - }, - child: Text(isEditing ? 'Done' : 'Edit'), - ), - trailing: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - _PlayerScreenButton(), - ], - ), - ), - CupertinoSliverRefreshControl( - onRefresh: () => _refreshData(), - ), - const SliverToBoxAdapter(child: ConnectivityBanner()), - const SliverSafeArea(top: false, sliver: _HomeBody()), - ], - ), - if (getScreenType(context) == ScreenType.handset) - Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 16.0, - right: 8.0, - child: FloatingActionButton.extended( - backgroundColor: CupertinoTheme.of(context).primaryColor, - foregroundColor: - CupertinoTheme.of(context).primaryContrastingColor, - onPressed: () { - pushPlatformRoute( - context, - title: context.l10n.play, - builder: (_) => const PlayScreen(), - ); - }, - icon: const Icon(Icons.add), - label: Text(context.l10n.play), - ), - ), - ], - ), - ); - } - - Future _refreshData() { - return Future.wait([ - ref.refresh(accountProvider.future), - ref.refresh(myRecentGamesProvider.future), - ref.refresh(ongoingGamesProvider.future), - ]); - } -} - -class _SignInWidget extends ConsumerWidget { - const _SignInWidget(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authController = ref.watch(authControllerProvider); - - return SecondaryButton( - semanticsLabel: context.l10n.signIn, - onPressed: authController.isLoading - ? null - : () => ref.read(authControllerProvider.notifier).signIn(), - child: Text(context.l10n.signIn), - ); - } -} - -class _HomeBody extends ConsumerWidget { - const _HomeBody(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isOnlineAsync = ref.watch(connectivityChangesProvider); + final connectivity = ref.watch(connectivityChangesProvider); final isEditing = ref.watch(editModeProvider); - return isOnlineAsync.when( + return connectivity.when( + skipLoadingOnReload: true, data: (status) { final session = ref.watch(authSessionProvider); + final ongoingGames = ref.watch(ongoingGamesProvider); + final emptyRecent = ref + .watch(myRecentGamesProvider) + .maybeWhen(data: (data) => data.isEmpty, orElse: () => false); final isTablet = isTabletOrLarger(context); - final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( - data: (data) => data.isEmpty, - orElse: () => false, - ); // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) - if (emptyRecent) { - return _WelcomeScreen( - session: session, - status: status, - isTablet: isTablet, - isEditing: isEditing, - ); - } - - final widgets = isTablet - ? [ - _EditableWidget( - widget: EnabledWidget.hello, - isEditing: isEditing, - shouldShow: true, - child: const _HelloWidget(), - ), - _EditableWidget( - widget: EnabledWidget.perfCards, - isEditing: isEditing, - shouldShow: session != null, - child: AccountPerfCards(padding: Styles.bodySectionPadding), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Column( - children: [ - const SizedBox(height: 8.0), - if (status.isOnline) - _TabletCreateAGameSection(isEditing: isEditing), - if (status.isOnline) - const _OngoingGamesPreview(maxGamesToShow: 5) - else - const _OfflineCorrespondencePreview( - maxGamesToShow: 5, - ), - ], + final widgets = + emptyRecent + ? _welcomeScreenWidgets(session: session, status: status, isTablet: isTablet) + : isTablet + ? _tabletWidgets(session: session, status: status, ongoingGames: ongoingGames) + : _handsetWidgets(session: session, status: status, ongoingGames: ongoingGames); + + if (Theme.of(context).platform == TargetPlatform.iOS) { + return CupertinoPageScaffold( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + CustomScrollView( + controller: homeScrollController, + slivers: [ + CupertinoSliverNavigationBar( + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), + largeTitle: Text(context.l10n.mobileHomeTab), + leading: CupertinoButton( + alignment: Alignment.centerLeft, + padding: EdgeInsets.zero, + onPressed: () { + ref.read(editModeProvider.notifier).state = !isEditing; + }, + child: Text(isEditing ? 'Done' : 'Edit'), ), - ), - const Flexible( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(height: 8.0), - RecentGamesWidget(), - ], + trailing: const Row( + mainAxisSize: MainAxisSize.min, + children: [_ChallengeScreenButton(), _PlayerScreenButton()], ), ), + CupertinoSliverRefreshControl( + onRefresh: () => _refreshData(isOnline: status.isOnline), + ), + const SliverToBoxAdapter(child: ConnectivityBanner()), + SliverSafeArea( + top: false, + sliver: SliverList(delegate: SliverChildListDelegate(widgets)), + ), ], ), - ] - : [ - _EditableWidget( - widget: EnabledWidget.hello, - isEditing: isEditing, - shouldShow: true, - child: const _HelloWidget(), - ), - _EditableWidget( - widget: EnabledWidget.perfCards, - isEditing: isEditing, - shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.horizontalBodyPadding, - ), - ), - _EditableWidget( - widget: EnabledWidget.quickPairing, - isEditing: isEditing, - shouldShow: status.isOnline, - child: Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), + if (getScreenType(context) == ScreenType.handset) + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 16.0, + right: 8.0, + child: FloatingActionButton.extended( + backgroundColor: CupertinoTheme.of(context).primaryColor, + foregroundColor: CupertinoTheme.of(context).primaryContrastingColor, + onPressed: () { + pushPlatformRoute( + context, + title: context.l10n.play, + builder: (_) => const PlayScreen(), + ); + }, + icon: const Icon(Icons.add), + label: Text(context.l10n.play), + ), ), + ], + ), + ); + } else { + return Scaffold( + appBar: AppBar( + title: const Text('lichess.org'), + actions: [ + IconButton( + onPressed: () { + ref.read(editModeProvider.notifier).state = !isEditing; + }, + icon: Icon(isEditing ? Icons.save_outlined : Icons.app_registration), + tooltip: isEditing ? 'Save' : 'Edit', ), - if (status.isOnline) - const _OngoingGamesCarousel(maxGamesToShow: 20) - else - const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), - const RecentGamesWidget(), - if (Theme.of(context).platform == TargetPlatform.iOS) - const SizedBox(height: 70.0) - else - const SizedBox(height: 54.0), - ]; - - return Theme.of(context).platform == TargetPlatform.android - ? ListView( - controller: homeScrollController, - children: widgets, - ) - : SliverList( - delegate: SliverChildListDelegate(widgets), - ); - }, - loading: () { - const child = CenterLoadingIndicator(); - return Theme.of(context).platform == TargetPlatform.android - ? child - : const SliverFillRemaining(child: child); - }, - error: (error, stack) { - const child = SizedBox.shrink(); - return Theme.of(context).platform == TargetPlatform.android - ? child - : const SliverFillRemaining(child: child); + const _ChallengeScreenButton(), + const _PlayerScreenButton(), + ], + ), + body: RefreshIndicator( + key: _androidRefreshKey, + onRefresh: () => _refreshData(isOnline: status.isOnline), + child: Column( + children: [ + const ConnectivityBanner(), + Expanded(child: ListView(controller: homeScrollController, children: widgets)), + ], + ), + ), + floatingActionButton: + isTablet + ? null + : FloatingActionButton.extended( + onPressed: () { + pushPlatformRoute(context, builder: (_) => const PlayScreen()); + }, + icon: const Icon(Icons.add), + label: Text(context.l10n.play), + ), + ); + } }, + error: (_, __) => const CenterLoadingIndicator(), + loading: () => const CenterLoadingIndicator(), ); } -} -class _EditableWidget extends ConsumerWidget { - const _EditableWidget({ - required this.child, - required this.widget, - required this.isEditing, - required this.shouldShow, - }); - - final Widget child; - final EnabledWidget widget; - final bool isEditing; - final bool shouldShow; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final enabledWidgets = ref.watch(homePreferencesProvider).enabledWidgets; - final isEnabled = enabledWidgets.contains(widget); - - if (!shouldShow) { - return const SizedBox.shrink(); - } - - return isEditing - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Checkbox.adaptive( - value: isEnabled, - onChanged: (_) { - ref - .read(homePreferencesProvider.notifier) - .toggleWidget(widget); - }, - ), - ), - Expanded(child: child), - ], - ) - : isEnabled - ? child - : const SizedBox.shrink(); + List _handsetWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required AsyncValue> ongoingGames, + }) { + return [ + const _EditableWidget(widget: EnabledWidget.hello, shouldShow: true, child: _HelloWidget()), + if (status.isOnline) + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards(padding: Styles.horizontalBodyPadding), + ), + _EditableWidget( + widget: EnabledWidget.quickPairing, + shouldShow: status.isOnline, + child: const Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), + ), + if (status.isOnline) + _OngoingGamesCarousel(ongoingGames, maxGamesToShow: 20) + else + const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), + const RecentGamesWidget(), + if (Theme.of(context).platform == TargetPlatform.iOS) + const SizedBox(height: 70.0) + else + const SizedBox(height: 54.0), + ]; } -} - -class _WelcomeScreen extends StatelessWidget { - const _WelcomeScreen({ - required this.session, - required this.status, - required this.isTablet, - required this.isEditing, - }); - - final AuthSessionState? session; - final ConnectivityStatus status; - final bool isTablet; - final bool isEditing; - @override - Widget build(BuildContext context) { + List _welcomeScreenWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required bool isTablet, + }) { final welcomeWidgets = [ Padding( padding: Styles.horizontalBodyPadding, child: LichessMessage( - style: Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle(fontSize: 18) - : Theme.of(context).textTheme.bodyLarge, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 18) + : Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ), const SizedBox(height: 24.0), - if (session == null) ...[ - const Center(child: _SignInWidget()), - const SizedBox(height: 16.0), - ], + if (session == null) ...[const Center(child: _SignInWidget()), const SizedBox(height: 16.0)], if (Theme.of(context).platform != TargetPlatform.iOS && - (session == null || session!.user.isPatron != true)) ...[ + (session == null || session.user.isPatron != true)) ...[ Center( child: SecondaryButton( semanticsLabel: context.l10n.patronDonate, @@ -436,54 +262,142 @@ class _WelcomeScreen extends StatelessWidget { ), ]; - final emptyScreenWidgets = [ + return [ if (isTablet) Row( children: [ - if (status.isOnline) - Flexible( - child: _TabletCreateAGameSection(isEditing: isEditing), - ), - Flexible( - child: Column( - children: welcomeWidgets, - ), - ), + const Flexible(child: _TabletCreateAGameSection()), + Flexible(child: Column(children: welcomeWidgets)), ], ) else ...[ if (status.isOnline) - _EditableWidget( + const _EditableWidget( widget: EnabledWidget.quickPairing, - isEditing: isEditing, shouldShow: true, - child: Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), - ), + child: Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), ), ...welcomeWidgets, ], ]; + } - return Theme.of(context).platform == TargetPlatform.android - ? Center( - child: ListView(shrinkWrap: true, children: emptyScreenWidgets), - ) - : SliverFillRemaining( - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.viewPaddingOf(context).vertical + 50.0, - ), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: emptyScreenWidgets, - ), + List _tabletWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required AsyncValue> ongoingGames, + }) { + return [ + const _EditableWidget(widget: EnabledWidget.hello, shouldShow: true, child: _HelloWidget()), + if (status.isOnline) + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards(padding: Styles.bodySectionPadding), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Column( + children: [ + const SizedBox(height: 8.0), + const _TabletCreateAGameSection(), + if (status.isOnline) + _OngoingGamesPreview(ongoingGames, maxGamesToShow: 5) + else + const _OfflineCorrespondencePreview(maxGamesToShow: 5), + ], + ), + ), + const Flexible( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [SizedBox(height: 8.0), RecentGamesWidget()], + ), + ), + ], + ), + ]; + } + + Future _refreshData({required bool isOnline}) { + return Future.wait([ + ref.refresh(myRecentGamesProvider.future), + if (isOnline) ref.refresh(accountProvider.future), + if (isOnline) ref.refresh(ongoingGamesProvider.future), + ]); + } +} + +class _SignInWidget extends ConsumerWidget { + const _SignInWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authController = ref.watch(authControllerProvider); + + return SecondaryButton( + semanticsLabel: context.l10n.signIn, + onPressed: + authController.isLoading + ? null + : () => ref.read(authControllerProvider.notifier).signIn(), + child: Text(context.l10n.signIn), + ); + } +} + +/// A widget that can be enabled or disabled by the user. +/// +/// This widget is used to show or hide certain sections of the home screen. +/// +/// The [homePreferencesProvider] provides a list of enabled widgets. +/// +/// * The [widget] parameter is the widget that can be enabled or disabled. +/// +/// * The [shouldShow] parameter is useful when the widget should be shown only +/// when certain conditions are met. For example, we only want to show the quick +/// pairing matrix when the user is online. +/// This parameter is only active when the user is not in edit mode, as we +/// always want to display the widget in edit mode. +class _EditableWidget extends ConsumerWidget { + const _EditableWidget({required this.child, required this.widget, required this.shouldShow}); + + final Widget child; + final EnabledWidget widget; + final bool shouldShow; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enabledWidgets = ref.watch(homePreferencesProvider).enabledWidgets; + final isEditing = ref.watch(editModeProvider); + final isEnabled = enabledWidgets.contains(widget); + + if (!shouldShow) { + return const SizedBox.shrink(); + } + + return isEditing + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Checkbox.adaptive( + value: isEnabled, + onChanged: (_) { + ref.read(homePreferencesProvider.notifier).toggleWidget(widget); + }, ), ), - ); + Expanded(child: IgnorePointer(ignoring: isEditing, child: child)), + ], + ) + : isEnabled + ? child + : const SizedBox.shrink(); } } @@ -493,41 +407,33 @@ class _HelloWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - final style = Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle(fontSize: 20) - : Theme.of(context).textTheme.bodyLarge; + final style = + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 20) + : Theme.of(context).textTheme.bodyLarge; - final iconSize = - Theme.of(context).platform == TargetPlatform.iOS ? 26.0 : 24.0; + final iconSize = Theme.of(context).platform == TargetPlatform.iOS ? 26.0 : 24.0; // fetch the account user to be sure we have the latest data (flair, etc.) - final accountUser = ref.watch(accountProvider).maybeWhen( - data: (data) => data?.lightUser, - orElse: () => null, - ); + final accountUser = ref + .watch(accountProvider) + .maybeWhen(data: (data) => data?.lightUser, orElse: () => null); final user = accountUser ?? session?.user; return Padding( - padding: - Styles.horizontalBodyPadding.add(Styles.sectionBottomPadding).add( - const EdgeInsets.only(top: 8.0), - ), + padding: Styles.horizontalBodyPadding + .add(Styles.sectionBottomPadding) + .add(const EdgeInsets.only(top: 8.0)), child: GestureDetector( onTap: () { - pushPlatformRoute( - context, - builder: (context) => const ProfileScreen(), - ); + ref.invalidate(accountActivityProvider); + pushPlatformRoute(context, builder: (context) => const ProfileScreen()); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.wb_sunny, - size: iconSize, - color: context.lichessColors.brag, - ), + Icon(Icons.wb_sunny, size: iconSize, color: context.lichessColors.brag), const SizedBox(width: 5.0), if (user != null) l10nWithWidget( @@ -545,64 +451,58 @@ class _HelloWidget extends ConsumerWidget { } class _TabletCreateAGameSection extends StatelessWidget { - const _TabletCreateAGameSection({required this.isEditing}); - - final bool isEditing; + const _TabletCreateAGameSection(); @override Widget build(BuildContext context) { - return Column( + return const Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _EditableWidget( widget: EnabledWidget.quickPairing, - isEditing: isEditing, shouldShow: true, - child: Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), - ), - ), - Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameButton(), + child: Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), ), - const CreateGameOptions(), + Padding(padding: Styles.bodySectionPadding, child: QuickGameButton()), + CreateGameOptions(), ], ); } } class _OngoingGamesCarousel extends ConsumerWidget { - const _OngoingGamesCarousel({required this.maxGamesToShow}); + const _OngoingGamesCarousel(this.games, {required this.maxGamesToShow}); + + final AsyncValue> games; final int maxGamesToShow; @override Widget build(BuildContext context, WidgetRef ref) { - final ongoingGames = ref.watch(ongoingGamesProvider); - return ongoingGames.maybeWhen( + return games.maybeWhen( data: (data) { if (data.isEmpty) { return const SizedBox.shrink(); } return _GamesCarousel( list: data, - builder: (game) => _GamePreviewCarouselItem( - game: game, - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => GameScreen( - initialGameId: game.fullId, - loadingFen: game.fen, - loadingOrientation: game.orientation, - loadingLastMove: game.lastMove, - ), - ); - }, - ), + builder: + (game) => _GamePreviewCarouselItem( + game: game, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => GameScreen( + initialGameId: game.fullId, + loadingFen: game.fen, + loadingOrientation: game.orientation, + loadingLastMove: game.lastMove, + ), + ); + }, + ), moreScreenBuilder: (_) => const OngoingGamesScreen(), maxGamesToShow: maxGamesToShow, ); @@ -619,8 +519,7 @@ class _OfflineCorrespondenceCarousel extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final offlineCorresGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineCorresGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineCorresGames.maybeWhen( data: (data) { if (data.isEmpty) { @@ -628,32 +527,31 @@ class _OfflineCorrespondenceCarousel extends ConsumerWidget { } return _GamesCarousel( list: data, - builder: (el) => _GamePreviewCarouselItem( - game: OngoingGame( - id: el.$2.id, - fullId: el.$2.fullId, - orientation: el.$2.orientation, - fen: el.$2.lastPosition.fen, - perf: el.$2.perf, - speed: el.$2.speed, - variant: el.$2.variant, - opponent: el.$2.opponent.user, - isMyTurn: el.$2.isMyTurn, - opponentRating: el.$2.opponent.rating, - opponentAiLevel: el.$2.opponent.aiLevel, - lastMove: el.$2.lastMove, - secondsLeft: el.$2.myTimeLeft(el.$1)?.inSeconds, - ), - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => OfflineCorrespondenceGameScreen( - initialGame: (el.$1, el.$2), + builder: + (el) => _GamePreviewCarouselItem( + game: OngoingGame( + id: el.$2.id, + fullId: el.$2.fullId, + orientation: el.$2.orientation, + fen: el.$2.lastPosition.fen, + perf: el.$2.perf, + speed: el.$2.speed, + variant: el.$2.variant, + opponent: el.$2.opponent.user, + isMyTurn: el.$2.isMyTurn, + opponentRating: el.$2.opponent.rating, + opponentAiLevel: el.$2.opponent.aiLevel, + lastMove: el.$2.lastMove, + secondsLeft: el.$2.myTimeLeft(el.$1)?.inSeconds, ), - ); - }, - ), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => OfflineCorrespondenceGameScreen(initialGame: (el.$1, el.$2)), + ); + }, + ), moreScreenBuilder: (_) => const OfflineCorrespondenceGamesScreen(), maxGamesToShow: maxGamesToShow, ); @@ -742,80 +640,71 @@ class _GamePreviewCarouselItem extends StatelessWidget { @override Widget build(BuildContext context) { - return BoardCarouselItem( - fen: game.fen, - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, - description: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Opacity( - opacity: game.isMyTurn ? 1.0 : 0.6, - child: Row( + return Opacity( + opacity: game.speed != Speed.correspondence || game.isMyTurn ? 1.0 : 0.7, + child: BoardCarouselItem( + fen: game.fen, + orientation: game.orientation, + lastMove: game.lastMove, + description: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ - Icon( - game.isMyTurn ? Icons.timer : Icons.timer_off, - size: 16.0, - color: Colors.white, - ), - const SizedBox(width: 4.0), + if (game.isMyTurn) ...const [ + Icon(Icons.timer, size: 16.0, color: Colors.white), + SizedBox(width: 4.0), + ], Text( game.secondsLeft != null && game.isMyTurn - ? timeago.format( - DateTime.now().add( - Duration(seconds: game.secondsLeft!), - ), - allowFromNow: true, - ) + ? relativeDate( + context.l10n, + DateTime.now().add(Duration(seconds: game.secondsLeft!)), + ) : game.isMyTurn - ? context.l10n.yourTurn - : context.l10n.waitingForOpponent, - style: Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ) - : TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium - ?.fontSize, - fontWeight: FontWeight.w500, - ), + ? context.l10n.yourTurn + : context.l10n.waitingForOpponent, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 12, fontWeight: FontWeight.w500) + : TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium?.fontSize, + fontWeight: FontWeight.w500, + ), ), ], ), - ), - const SizedBox(height: 4.0), - UserFullNameWidget.player( - user: game.opponent, - rating: game.opponentRating, - aiLevel: game.opponentAiLevel, - style: Styles.boardPreviewTitle, - ), - ], + const SizedBox(height: 4.0), + UserFullNameWidget.player( + user: game.opponent, + rating: game.opponentRating, + aiLevel: game.opponentAiLevel, + style: Styles.boardPreviewTitle, + ), + ], + ), ), ), + onTap: onTap, ), - onTap: onTap, ); } } class _OngoingGamesPreview extends ConsumerWidget { - const _OngoingGamesPreview({required this.maxGamesToShow}); + const _OngoingGamesPreview(this.games, {required this.maxGamesToShow}); + final AsyncValue> games; final int maxGamesToShow; @override Widget build(BuildContext context, WidgetRef ref) { - final ongoingGames = ref.watch(ongoingGamesProvider); - return ongoingGames.maybeWhen( + return games.maybeWhen( data: (data) { return PreviewGameList( list: data, @@ -836,17 +725,13 @@ class _OfflineCorrespondencePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final offlineCorresGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineCorresGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineCorresGames.maybeWhen( data: (data) { return PreviewGameList( list: data, maxGamesToShow: maxGamesToShow, - builder: (el) => OfflineCorrespondenceGamePreview( - game: el.$2, - lastModified: el.$1, - ), + builder: (el) => OfflineCorrespondenceGamePreview(game: el.$2, lastModified: el.$1), moreScreenBuilder: (_) => const OfflineCorrespondenceGamesScreen(), ); }, @@ -877,9 +762,7 @@ class PreviewGameList extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: Styles.horizontalBodyPadding.add( - const EdgeInsets.only(top: 16.0), - ), + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(top: 16.0)), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -921,24 +804,70 @@ class _PlayerScreenButton extends ConsumerWidget { final connectivity = ref.watch(connectivityChangesProvider); return connectivity.maybeWhen( - data: (connectivity) => AppBarIconButton( - icon: const Icon(Icons.group), - semanticsLabel: context.l10n.players, - onPressed: !connectivity.isOnline - ? null - : () { - pushPlatformRoute( - context, - title: context.l10n.players, - builder: (_) => const PlayerScreen(), - ); - }, - ), - orElse: () => AppBarIconButton( - icon: const Icon(Icons.group), - semanticsLabel: context.l10n.players, - onPressed: null, - ), + data: + (connectivity) => AppBarIconButton( + icon: const Icon(Icons.group_outlined), + semanticsLabel: context.l10n.players, + onPressed: + !connectivity.isOnline + ? null + : () { + pushPlatformRoute( + context, + title: context.l10n.players, + builder: (_) => const PlayerScreen(), + ); + }, + ), + orElse: + () => AppBarIconButton( + icon: const Icon(Icons.group_outlined), + semanticsLabel: context.l10n.players, + onPressed: null, + ), + ); + } +} + +class _ChallengeScreenButton extends ConsumerWidget { + const _ChallengeScreenButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(authSessionProvider); + + if (session == null) { + return const SizedBox.shrink(); + } + + final connectivity = ref.watch(connectivityChangesProvider); + final challenges = ref.watch(challengesProvider); + final count = challenges.valueOrNull?.inward.length; + + return connectivity.maybeWhen( + data: + (connectivity) => AppBarNotificationIconButton( + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: + !connectivity.isOnline + ? null + : () { + ref.invalidate(challengesProvider); + pushPlatformRoute( + context, + title: context.l10n.preferencesNotifyChallenge, + builder: (_) => const ChallengeRequestsScreen(), + ); + }, + count: count ?? 0, + ), + orElse: + () => AppBarIconButton( + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: null, + ), ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart new file mode 100644 index 0000000000..6aa6c1d9a9 --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -0,0 +1,306 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/move_list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +import 'opening_explorer_settings.dart'; + +const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); + +class OpeningExplorerScreen extends ConsumerWidget { + const OpeningExplorerScreen({required this.options}); + + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(options); + + final body = switch (ref.watch(ctrlProvider)) { + AsyncData(value: final state) => _Body(options: options, state: state), + AsyncError(:final error) => Center( + child: Padding(padding: const EdgeInsets.all(16.0), child: Text(error.toString())), + ), + _ => const CenterLoadingIndicator(), + }; + + return PlatformWidget( + androidBuilder: + (_) => Scaffold( + body: body, + appBar: AppBar( + title: Text(context.l10n.openingExplorer), + bottom: _MoveList(options: options), + ), + ), + iosBuilder: + (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(context.l10n.openingExplorer), + automaticBackgroundVisibility: false, + border: null, + ), + child: body, + ), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({required this.options, required this.state}); + + final AnalysisOptions options; + final AnalysisState state; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isTablet = isTabletOrLarger(context); + + return SafeArea( + bottom: false, + child: Column( + children: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + Padding( + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, + child: _MoveList(options: options), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; + if (orientation == Orientation.landscape) { + final sideWidth = + constraints.biggest.longestSide - constraints.biggest.shortestSide; + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); + final boardSize = + sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( + options, + boardSize, + borderRadius: isTablet ? _kTabletBoardRadius : null, + shouldReplaceChildOnUserMove: true, + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + margin: const EdgeInsets.all(kTabletBoardTableSidePadding), + semanticContainer: false, + child: OpeningExplorerView( + position: state.position, + onMoveSelected: (move) { + ref + .read(analysisControllerProvider(options).notifier) + .onUserMove(move); + }, + ), + ), + ), + ], + ), + ), + ], + ); + } else { + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return ListView( + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, + children: [ + GestureDetector( + // disable scrolling when dragging the board + onVerticalDragStart: (_) {}, + child: AnalysisBoard( + options, + boardSize, + shouldReplaceChildOnUserMove: true, + ), + ), + OpeningExplorerView( + position: state.position, + opening: + state.currentNode.isRoot + ? LightOpening(eco: '', name: context.l10n.startPosition) + : state.currentNode.opening ?? state.currentBranchOpening, + onMoveSelected: (move) { + ref.read(analysisControllerProvider(options).notifier).onUserMove(move); + }, + scrollable: false, + ), + ], + ); + } + }, + ), + ), + _BottomBar(options: options), + ], + ), + ); + } +} + +class _MoveList extends ConsumerWidget implements PreferredSizeWidget { + const _MoveList({required this.options}); + + final AnalysisOptions options; + + @override + Size get preferredSize => const Size.fromHeight(40.0); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(options); + + switch (ref.watch(ctrlProvider)) { + case AsyncData(value: final state): + final slicedMoves = state.root.mainline + .map((e) => e.sanMove.san) + .toList() + .asMap() + .entries + .slices(2); + final currentMoveIndex = state.currentNode.position.ply; + + return MoveList( + inlineDecoration: + Theme.of(context).platform == TargetPlatform.iOS + ? BoxDecoration( + color: Styles.cupertinoAppBarColor.resolveFrom(context), + border: const Border(bottom: BorderSide(color: Color(0x4D000000), width: 0.0)), + ) + : null, + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: currentMoveIndex, + onSelectMove: (index) { + ref.read(ctrlProvider.notifier).jumpToNthNodeOnMainline(index - 1); + }, + ); + case _: + return const SizedBox.shrink(); + } + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({required this.options}); + + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final db = ref.watch(openingExplorerPreferencesProvider.select((value) => value.db)); + final ctrlProvider = analysisControllerProvider(options); + final canGoBack = ref.watch(ctrlProvider.select((value) => value.requireValue.canGoBack)); + final canGoNext = ref.watch(ctrlProvider.select((value) => value.requireValue.canGoNext)); + + final dbLabel = switch (db) { + OpeningDatabase.master => 'Masters', + OpeningDatabase.lichess => 'Lichess', + OpeningDatabase.player => context.l10n.player, + }; + + return BottomBar( + children: [ + BottomBarButton( + label: dbLabel, + showLabel: true, + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + icon: Icons.tune, + ), + BottomBarButton( + label: 'Flip', + tooltip: context.l10n.flipBoard, + showLabel: true, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + showLabel: true, + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + icon: CupertinoIcons.chevron_forward, + label: 'Next', + showLabel: true, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); +} diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart new file mode 100644 index 0000000000..3a31eb28c2 --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -0,0 +1,271 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/user/search_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +class OpeningExplorerSettings extends ConsumerWidget { + const OpeningExplorerSettings(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(openingExplorerPreferencesProvider); + + final List masterDbSettings = [ + PlatformListTile( + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: MasterDb.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: prefs.masterDb.sinceYear == MasterDb.datesMap[key], + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbSince(MasterDb.datesMap[key]!), + ), + ) + .toList(growable: false), + ), + ), + ]; + + final List lichessDbSettings = [ + PlatformListTile( + title: Text(context.l10n.timeControl), + subtitle: Wrap( + spacing: 5, + children: LichessDb.kAvailableSpeeds + .map( + (speed) => FilterChip( + label: Text( + String.fromCharCode(speed.icon.codePoint), + style: TextStyle(fontFamily: speed.icon.fontFamily, fontSize: 18.0), + ), + tooltip: Perf.fromVariantAndSpeed(Variant.standard, speed).title, + selected: prefs.lichessDb.speeds.contains(speed), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbSpeed(speed), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.rating), + subtitle: Wrap( + spacing: 5, + children: LichessDb.kAvailableRatings + .map( + (rating) => FilterChip( + label: Text(rating.toString()), + tooltip: + rating == 400 + ? '400-1000' + : rating == 2500 + ? '2500+' + : '$rating-${rating + 200}', + selected: prefs.lichessDb.ratings.contains(rating), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbRating(rating), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: LichessDb.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: prefs.lichessDb.since == LichessDb.datesMap[key], + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setLichessDbSince(LichessDb.datesMap[key]!), + ), + ) + .toList(growable: false), + ), + ), + ]; + final List playerDbSettings = [ + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.player}: ', + style: const TextStyle(fontWeight: FontWeight.normal), + children: [ + TextSpan( + recognizer: + TapGestureRecognizer() + ..onTap = + () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: + (_) => SearchScreen( + onUserTap: + (user) => { + ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbUsernameOrId(user.name), + Navigator.of(context).pop(), + }, + ), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + decoration: TextDecoration.underline, + ), + text: prefs.playerDb.username ?? 'Select a Lichess player', + ), + ], + ), + ), + ), + PlatformListTile( + title: Text(context.l10n.side), + subtitle: Wrap( + spacing: 5, + children: Side.values + .map( + (side) => ChoiceChip( + label: switch (side) { + Side.white => const Text('White'), + Side.black => const Text('Black'), + }, + selected: prefs.playerDb.side == side, + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSide(side), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.timeControl), + subtitle: Wrap( + spacing: 5, + children: PlayerDb.kAvailableSpeeds + .map( + (speed) => FilterChip( + label: Text( + String.fromCharCode(speed.icon.codePoint), + style: TextStyle(fontFamily: speed.icon.fontFamily, fontSize: 18.0), + ), + tooltip: Perf.fromVariantAndSpeed(Variant.standard, speed).title, + selected: prefs.playerDb.speeds.contains(speed), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbSpeed(speed), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.mode), + subtitle: Wrap( + spacing: 5, + children: GameMode.values + .map( + (gameMode) => FilterChip( + label: Text(switch (gameMode) { + GameMode.casual => 'Casual', + GameMode.rated => 'Rated', + }), + selected: prefs.playerDb.gameModes.contains(gameMode), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbGameMode(gameMode), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: PlayerDb.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: prefs.playerDb.since == PlayerDb.datesMap[key], + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSince(PlayerDb.datesMap[key]!), + ), + ) + .toList(growable: false), + ), + ), + ]; + + return BottomSheetScrollableContainer( + children: [ + PlatformListTile( + title: Text(context.l10n.database), + subtitle: Wrap( + spacing: 5, + children: [ + ChoiceChip( + label: const Text('Masters'), + selected: prefs.db == OpeningDatabase.master, + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.master), + ), + ChoiceChip( + label: const Text('Lichess'), + selected: prefs.db == OpeningDatabase.lichess, + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.lichess), + ), + ChoiceChip( + label: Text(context.l10n.player), + selected: prefs.db == OpeningDatabase.player, + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.player), + ), + ], + ), + ), + ...switch (prefs.db) { + OpeningDatabase.master => masterDbSettings, + OpeningDatabase.lichess => lichessDbSettings, + OpeningDatabase.player => playerDbSettings, + }, + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_view.dart b/lib/src/view/opening_explorer/opening_explorer_view.dart new file mode 100644 index 0000000000..c577a252c8 --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_view.dart @@ -0,0 +1,215 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +/// Displays an opening explorer for the given position. +/// +/// It shows the top moves, games, and recent games for the given position in a list view. +/// By default, the view is scrollable, but it can be disabled by setting [scrollable] to false. +/// +/// This widget is meant to be embedded in the analysis, broadcast, and study screens. +/// +/// Network requests are debounced and cached to avoid unnecessary requests. +class OpeningExplorerView extends ConsumerStatefulWidget { + const OpeningExplorerView({ + required this.position, + required this.onMoveSelected, + this.opening, + this.scrollable = true, + }); + + final Position position; + final Opening? opening; + final void Function(NormalMove) onMoveSelected; + final bool scrollable; + + @override + ConsumerState createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + if (widget.position.ply >= 50) { + return Center(child: Text(context.l10n.maxDepthReached)); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return const Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ); + } + + final cacheKey = OpeningExplorerCacheKey(fen: widget.position.fen, prefs: prefs); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = + cacheOpeningExplorer != null + ? AsyncValue.data((entry: cacheOpeningExplorer, isIndexing: false)) + : ref.watch(openingExplorerProvider(fen: widget.position.fen)); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: widget.position.fen), (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + switch (openingExplorerAsync) { + case AsyncData(:final value): + if (value == null) { + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: true, + children: + lastExplorerWidgets ?? + [ + const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading(), + ), + ), + ], + ); + } + + final topGames = value.entry.topGames; + final recentGames = value.entry.recentGames; + + final ply = widget.position.ply; + + final children = [ + if (widget.opening != null) OpeningNameHeader(opening: widget.opening!), + OpeningExplorerMoveTable( + moves: value.entry.moves, + whiteWins: value.entry.white, + draws: value.entry.draws, + blackWins: value.entry.black, + onMoveSelected: widget.onMoveSelected, + isIndexing: value.isIndexing, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate(topGames.length, (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, growable: false), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate(recentGames.length, (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, growable: false), + ], + ]; + + lastExplorerWidgets = children; + + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: false, + children: children, + ); + case AsyncError(:final error): + debugPrint('SEVERE: [OpeningExplorerView] could not load opening explorer data; $error'); + final connectivity = ref.watch(connectivityChangesProvider); + // TODO l10n + final message = connectivity.whenIs( + online: () => 'Could not load opening explorer data.', + offline: () => 'Opening Explorer is not available offline.', + ); + return Center(child: Padding(padding: const EdgeInsets.all(16.0), child: Text(message))); + case _: + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: true, + children: + lastExplorerWidgets ?? + [ + const Shimmer( + child: ShimmerLoading(isLoading: true, child: OpeningExplorerMoveTable.loading()), + ), + ], + ); + } + } +} + +class _ExplorerListView extends StatelessWidget { + const _ExplorerListView({ + required this.children, + required this.isLoading, + required this.scrollable, + }); + + final List children; + final bool isLoading; + final bool scrollable; + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox(color: brightness == Brightness.dark ? Colors.black : Colors.white), + ), + ), + ); + + return Stack( + children: [ + if (scrollable) + ListView(padding: EdgeInsets.zero, children: children) + else + ListBody(children: children), + loadingOverlay, + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart new file mode 100644 index 0000000000..c953f2d83c --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -0,0 +1,516 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const _kTableRowVerticalPadding = 10.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); +const _kHeaderTextStyle = TextStyle(fontSize: 12); + +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.7) + : Colors.black; + +class OpeningNameHeader extends StatelessWidget { + const OpeningNameHeader({required this.opening, super.key}); + + final Opening opening; + + @override + Widget build(BuildContext context) { + return Container( + padding: _kTableRowPadding, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), + child: GestureDetector( + onTap: + opening.name == context.l10n.startPosition + ? null + : () => launchUrl(Uri.parse('https://lichess.org/opening/${opening.name}')), + child: Row( + children: [ + if (opening.name != context.l10n.startPosition) ...[ + Icon( + Icons.open_in_browser_outlined, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const SizedBox(width: 6.0), + ], + Expanded( + child: Text( + opening.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} + +/// Table of moves for the opening explorer. +class OpeningExplorerMoveTable extends ConsumerWidget { + const OpeningExplorerMoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + this.onMoveSelected, + this.isIndexing = false, + }) : _isLoading = false; + + const OpeningExplorerMoveTable.loading() + : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + isIndexing = false, + onMoveSelected = null; + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final void Function(NormalMove)? onMoveSelected; + final bool isIndexing; + + final bool _isLoading; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (_isLoading) { + return loadingTable; + } + + final games = whiteWins + draws + blackWins; + + return Table( + columnWidths: columnWidths, + children: [ + TableRow( + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.move, style: _kHeaderTextStyle), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.games, style: _kHeaderTextStyle), + ), + Padding( + padding: _kTableRowPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + context.l10n.whiteDrawBlack, + style: _kHeaderTextStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isIndexing) const IndexingIndicator(), + ], + ), + ), + ], + ), + ...List.generate(moves.length, (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding(padding: _kTableRowPadding, child: Text(move.san)), + ), + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), + ), + ), + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, + ), + ), + ), + ], + ); + }), + if (moves.isNotEmpty) + TableRow( + decoration: BoxDecoration( + color: + moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: _kTableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Padding(padding: _kTableRowPadding, child: Text('${formatNum(games)} (100%)')), + Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: whiteWins, + draws: draws, + blackWins: blackWins, + ), + ), + ], + ) + else + TableRow( + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerLow), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle(fontFamily: Icons.not_interested_outlined.fontFamily), + ), + ), + Padding(padding: _kTableRowPadding, child: Text(context.l10n.noGameFound)), + const Padding(padding: _kTableRowPadding, child: SizedBox.shrink()), + ], + ), + ], + ); + } + + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); +} + +class IndexingIndicator extends StatefulWidget { + const IndexingIndicator(); + + @override + State createState() => _IndexingIndicatorState(); +} + +class _IndexingIndicatorState extends State with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + controller = AnimationController(vsync: this, duration: const Duration(seconds: 3)) + ..addListener(() { + setState(() {}); + }); + controller.repeat(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 12.0, + height: 12.0, + child: CircularProgressIndicator( + strokeWidth: 1.5, + value: controller.value, + // TODO: l10n + semanticsLabel: 'Indexing', + ), + ); + } +} + +class OpeningExplorerHeaderTile extends StatelessWidget { + const OpeningExplorerHeaderTile({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), + child: child, + ); + } +} + +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerStatefulWidget { + const OpeningExplorerGameTile({ + required this.game, + required this.color, + required this.ply, + super.key, + }); + + final OpeningExplorerGame game; + final Color color; + final int ply; + + @override + ConsumerState createState() => _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState extends ConsumerState { + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: _kTableRowPadding, + color: widget.color, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: + (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.name, overflow: TextOverflow.ellipsis), + Text(widget.game.black.name, overflow: TextOverflow.ellipsis), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whiteWins, + required this.draws, + required this.blackWins, + }); + + final int whiteWins; + final int draws; + final int blackWins; + + int percentGames(int games) => ((games / (whiteWins + draws + blackWins)) * 100).round(); + String label(int percent) => percent < 20 ? '' : '$percent%'; + + @override + Widget build(BuildContext context) { + final percentWhite = percentGames(whiteWins); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(blackWins); + + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ + Expanded( + flex: percentWhite, + child: ColoredBox( + color: _whiteBoxColor(context), + child: Text( + label(percentWhite), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: percentDraws, + child: ColoredBox( + color: Colors.grey, + child: Text( + label(percentDraws), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: percentBlack, + child: ColoredBox( + color: _blackBoxColor(context), + child: Text( + label(percentBlack), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/view/over_the_board/configure_over_the_board_game.dart b/lib/src/view/over_the_board/configure_over_the_board_game.dart new file mode 100644 index 0000000000..7836038b61 --- /dev/null +++ b/lib/src/view/over_the_board/configure_over_the_board_game.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +void showConfigureGameSheet(BuildContext context, {required bool isDismissible}) { + final double screenHeight = MediaQuery.sizeOf(context).height; + showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: isDismissible, + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), + builder: (BuildContext context) { + return const _ConfigureOverTheBoardGameSheet(); + }, + ); +} + +class _ConfigureOverTheBoardGameSheet extends ConsumerStatefulWidget { + const _ConfigureOverTheBoardGameSheet(); + + @override + ConsumerState<_ConfigureOverTheBoardGameSheet> createState() => + _ConfigureOverTheBoardGameSheetState(); +} + +class _ConfigureOverTheBoardGameSheetState extends ConsumerState<_ConfigureOverTheBoardGameSheet> { + late Variant chosenVariant; + + late TimeIncrement timeIncrement; + + @override + void initState() { + final gameState = ref.read(overTheBoardGameControllerProvider); + chosenVariant = gameState.game.meta.variant; + final clockProvider = ref.read(overTheBoardClockProvider); + timeIncrement = clockProvider.timeIncrement; + super.initState(); + } + + void _setTotalTime(num seconds) { + setState(() { + timeIncrement = TimeIncrement(seconds.toInt(), timeIncrement.increment); + }); + } + + void _setIncrement(num seconds) { + setState(() { + timeIncrement = TimeIncrement(timeIncrement.time, seconds.toInt()); + }); + } + + @override + Widget build(BuildContext context) { + return BottomSheetScrollableContainer( + padding: Styles.bodyPadding, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.variant), + settingsValue: chosenVariant.label, + onTap: () { + showChoicePicker( + context, + choices: + playSupportedVariants + .where((variant) => variant != Variant.fromPosition) + .toList(), + selectedItem: chosenVariant, + labelBuilder: (Variant variant) => Text(variant.label), + onSelectedItemChanged: + (Variant variant) => setState(() { + chosenVariant = variant; + }), + ); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.minutesPerSide}: ', + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: clockLabelInMinutes(timeIncrement.time), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: timeIncrement.time, + values: kAvailableTimesInSeconds, + labelBuilder: clockLabelInMinutes, + onChange: Theme.of(context).platform == TargetPlatform.iOS ? _setTotalTime : null, + onChangeEnd: _setTotalTime, + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.incrementInSeconds}: ', + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: timeIncrement.increment.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: timeIncrement.increment, + values: kAvailableIncrementsInSeconds, + onChange: Theme.of(context).platform == TargetPlatform.iOS ? _setIncrement : null, + onChangeEnd: _setIncrement, + ), + ), + SecondaryButton( + onPressed: () { + ref.read(overTheBoardClockProvider.notifier).setupClock(timeIncrement); + ref + .read(overTheBoardGameControllerProvider.notifier) + .startNewGame(chosenVariant, timeIncrement); + Navigator.pop(context); + }, + semanticsLabel: context.l10n.play, + child: Text(context.l10n.play, style: Styles.bold), + ), + ], + ); + } +} + +void showConfigureDisplaySettings(BuildContext context) { + showAdaptiveBottomSheet( + context: context, + isDismissible: true, + showDragHandle: true, + builder: (BuildContext context) { + return const OverTheBoardDisplaySettings(); + }, + ); +} + +class OverTheBoardDisplaySettings extends ConsumerWidget { + const OverTheBoardDisplaySettings(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(overTheBoardPreferencesProvider); + + return BottomSheetScrollableContainer( + children: [ + SwitchSettingTile( + title: const Text('Use symmetric pieces'), + value: prefs.symmetricPieces, + onChanged: + (_) => ref.read(overTheBoardPreferencesProvider.notifier).toggleSymmetricPieces(), + ), + SwitchSettingTile( + title: const Text('Flip pieces and oponent info after move'), + value: prefs.flipPiecesAfterMove, + onChanged: + (_) => ref.read(overTheBoardPreferencesProvider.notifier).toggleFlipPiecesAfterMove(), + ), + ], + ); + } +} diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart new file mode 100644 index 0000000000..c4e5f82985 --- /dev/null +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -0,0 +1,305 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/utils/immersive_mode.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/game/game_player.dart'; +import 'package:lichess_mobile/src/view/game/game_result_dialog.dart'; +import 'package:lichess_mobile/src/view/over_the_board/configure_over_the_board_game.dart'; +import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class OverTheBoardScreen extends StatelessWidget { + const OverTheBoardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: const Text('Over the board'), // TODO: l10n + actions: [ + AppBarIconButton( + onPressed: () => showConfigureDisplaySettings(context), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: const _Body(), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + final _boardKey = GlobalKey(debugLabel: 'boardOnOverTheBoardScreen'); + + Side orientation = Side.white; + + @override + void initState() { + SchedulerBinding.instance.addPostFrameCallback((_) { + showConfigureGameSheet(context, isDismissible: false); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + final boardPreferences = ref.watch(boardPreferencesProvider); + + final overTheBoardPrefs = ref.watch(overTheBoardPreferencesProvider); + + ref.listen(overTheBoardClockProvider.select((value) => value.flagSide), (previous, flagSide) { + if (previous == null && flagSide != null) { + ref.read(overTheBoardGameControllerProvider.notifier).onFlag(flagSide); + } + }); + + ref.listen(overTheBoardGameControllerProvider, (previous, newGameState) { + if (previous?.finished == false && newGameState.finished) { + ref.read(overTheBoardClockProvider.notifier).pause(); + Timer(const Duration(milliseconds: 500), () { + if (context.mounted) { + showAdaptiveDialog( + context: context, + builder: + (context) => OverTheBoardGameResultDialog( + game: newGameState.game, + onRematch: () { + setState(() { + orientation = orientation.opposite; + ref.read(overTheBoardGameControllerProvider.notifier).rematch(); + ref.read(overTheBoardClockProvider.notifier).restart(); + Navigator.pop(context); + }); + }, + ), + barrierDismissible: true, + ); + } + }); + } + }); + + return WakelockWidget( + child: PopScope( + child: Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: BoardTable( + key: _boardKey, + topTable: _Player( + side: orientation.opposite, + upsideDown: + !overTheBoardPrefs.flipPiecesAfterMove || orientation != gameState.turn, + clockKey: const ValueKey('topClock'), + ), + bottomTable: _Player( + side: orientation, + upsideDown: + overTheBoardPrefs.flipPiecesAfterMove && orientation != gameState.turn, + clockKey: const ValueKey('bottomClock'), + ), + orientation: orientation, + fen: gameState.currentPosition.fen, + lastMove: gameState.lastMove, + gameData: GameData( + isCheck: boardPreferences.boardHighlights && gameState.currentPosition.isCheck, + playerSide: + gameState.game.finished + ? PlayerSide.none + : gameState.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + sideToMove: gameState.turn, + validMoves: gameState.legalMoves, + onPromotionSelection: + ref.read(overTheBoardGameControllerProvider.notifier).onPromotionSelection, + promotionMove: gameState.promotionMove, + onMove: (move, {isDrop}) { + ref + .read(overTheBoardClockProvider.notifier) + .onMove(newSideToMove: gameState.turn.opposite); + ref.read(overTheBoardGameControllerProvider.notifier).makeMove(move); + }, + ), + moves: gameState.moves, + currentMoveIndex: gameState.stepCursor, + boardSettingsOverrides: BoardSettingsOverrides( + drawShape: const DrawShapeOptions(enable: false), + pieceOrientationBehavior: + overTheBoardPrefs.flipPiecesAfterMove + ? PieceOrientationBehavior.sideToPlay + : PieceOrientationBehavior.opponentUpsideDown, + pieceAssets: + overTheBoardPrefs.symmetricPieces ? PieceSet.symmetric.assets : null, + ), + ), + ), + ), + _BottomBar( + onFlipBoard: () { + setState(() { + orientation = orientation.opposite; + }); + }, + ), + ], + ), + ), + ); + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({required this.onFlipBoard}); + + final VoidCallback onFlipBoard; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + + final clock = ref.watch(overTheBoardClockProvider); + + return BottomBar( + children: [ + BottomBarButton( + label: 'Configure game', + onTap: () => showConfigureGameSheet(context, isDismissible: true), + icon: Icons.add, + ), + BottomBarButton( + key: const Key('flip-button'), + label: context.l10n.flipBoard, + onTap: onFlipBoard, + icon: CupertinoIcons.arrow_2_squarepath, + ), + if (!clock.timeIncrement.isInfinite) + BottomBarButton( + label: clock.active ? 'Pause' : 'Resume', + onTap: + gameState.finished + ? null + : () { + if (clock.active) { + ref.read(overTheBoardClockProvider.notifier).pause(); + } else { + ref.read(overTheBoardClockProvider.notifier).resume(gameState.turn); + } + }, + icon: clock.active ? CupertinoIcons.pause : CupertinoIcons.play, + ), + BottomBarButton( + label: 'Previous', + onTap: + gameState.canGoBack + ? () { + ref.read(overTheBoardGameControllerProvider.notifier).goBack(); + if (clock.active) { + ref + .read(overTheBoardClockProvider.notifier) + .switchSide(newSideToMove: gameState.turn.opposite, addIncrement: false); + } + } + : null, + icon: CupertinoIcons.chevron_back, + ), + BottomBarButton( + label: 'Next', + onTap: + gameState.canGoForward + ? () { + ref.read(overTheBoardGameControllerProvider.notifier).goForward(); + if (clock.active) { + ref + .read(overTheBoardClockProvider.notifier) + .switchSide(newSideToMove: gameState.turn.opposite, addIncrement: false); + } + } + : null, + icon: CupertinoIcons.chevron_forward, + ), + ], + ); + } +} + +class _Player extends ConsumerWidget { + const _Player({required this.clockKey, required this.side, required this.upsideDown}); + + final Side side; + + final Key clockKey; + + final bool upsideDown; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + final boardPreferences = ref.watch(boardPreferencesProvider); + final clock = ref.watch(overTheBoardClockProvider); + + final brightness = ref.watch(currentBrightnessProvider); + final clockStyle = + brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle; + + return RotatedBox( + quarterTurns: upsideDown ? 2 : 0, + child: GamePlayer( + player: Player( + onGame: true, + user: LightUser(id: UserId(side.name), name: side.name.capitalize()), + ), + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.currentMaterialDiff(side) + : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, + shouldLinkToUserProfile: false, + clock: + clock.timeIncrement.isInfinite + ? null + : Clock( + timeLeft: Duration(milliseconds: max(0, clock.timeLeft(side)!.inMilliseconds)), + key: clockKey, + active: clock.activeClock == side, + clockStyle: clockStyle, + // https://github.com/lichess-org/mobile/issues/785#issuecomment-2183903498 + emergencyThreshold: Duration( + seconds: (clock.timeIncrement.time * 0.125).clamp(10, 60).toInt(), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart new file mode 100644 index 0000000000..c8d55a696a --- /dev/null +++ b/lib/src/view/play/challenge_list_item.dart @@ -0,0 +1,179 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; + +class ChallengeListItem extends ConsumerWidget { + const ChallengeListItem({ + super.key, + required this.challenge, + required this.challengerUser, + this.onPressed, + this.onAccept, + this.onDecline, + this.onCancel, + }); + + final Challenge challenge; + final LightUser challengerUser; + final VoidCallback? onPressed; + final VoidCallback? onAccept; + final VoidCallback? onCancel; + final void Function(ChallengeDeclineReason? reason)? onDecline; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final me = ref.watch(authSessionProvider)?.user; + final isMyChallenge = me != null && me.id == challengerUser.id; + + final color = isMyChallenge ? context.lichessColors.good.withValues(alpha: 0.2) : null; + + final isFromPosition = challenge.variant == Variant.fromPosition; + + final leading = Icon(challenge.perf.icon, size: 36); + final trailing = + challenge.challenger?.lagRating != null + ? LagIndicator(lagRating: challenge.challenger!.lagRating!) + : null; + final title = + isMyChallenge + // shows destUser if it exists, otherwise shows the challenger (me) + // if no destUser, it's an open challenge I sent + ? UserFullNameWidget( + user: challenge.destUser != null ? challenge.destUser!.user : challengerUser, + rating: challenge.destUser?.rating ?? challenge.challenger?.rating, + ) + : UserFullNameWidget(user: challengerUser, rating: challenge.challenger?.rating); + final subtitle = Text(challenge.description(context.l10n)); + + final screenWidth = MediaQuery.sizeOf(context).width; + + return Container( + color: color, + child: Slidable( + enabled: onAccept != null || onDecline != null || (isMyChallenge && onCancel != null), + dragStartBehavior: DragStartBehavior.start, + endActionPane: ActionPane( + motion: const StretchMotion(), + extentRatio: 0.6, + children: [ + if (onAccept != null) + SlidableAction( + icon: Icons.check, + onPressed: (_) => onAccept!(), + spacing: 8.0, + backgroundColor: context.lichessColors.good, + foregroundColor: Colors.white, + label: context.l10n.accept, + ), + if (onDecline != null || (isMyChallenge && onCancel != null)) + SlidableAction( + icon: Icons.close, + onPressed: + isMyChallenge + ? (_) => onCancel!() + : onDecline != null + ? (_) => onDecline!(null) + : null, + spacing: 8.0, + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + label: isMyChallenge ? context.l10n.cancel : context.l10n.decline, + ), + ], + ), + child: + isFromPosition + ? ExpansionTile( + childrenPadding: Styles.bodyPadding.subtract(const EdgeInsets.only(top: 8.0)), + leading: leading, + title: title, + subtitle: subtitle, + children: [ + if (challenge.variant == Variant.fromPosition && challenge.initialFen != null) + BoardThumbnail( + size: min(400, screenWidth - 2 * Styles.bodyPadding.horizontal), + orientation: + challenge.sideChoice == SideChoice.white ? Side.white : Side.black, + fen: challenge.initialFen!, + onTap: onPressed, + ), + ], + // onTap: onPressed, + ) + : AdaptiveListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + onTap: onPressed, + ), + ), + ); + } +} + +class CorrespondenceChallengeListItem extends StatelessWidget { + const CorrespondenceChallengeListItem({ + super.key, + required this.challenge, + required this.challengerUser, + this.onPressed, + this.onCancel, + }); + + final CorrespondenceChallenge challenge; + final LightUser challengerUser; + final VoidCallback? onPressed; + final VoidCallback? onCancel; + + @override + Widget build(BuildContext context) { + return ChallengeListItem( + challenge: Challenge( + id: ChallengeId(challenge.id.value), + status: ChallengeStatus.created, + variant: challenge.variant, + speed: Speed.correspondence, + timeControl: + challenge.days != null + ? ChallengeTimeControlType.correspondence + : ChallengeTimeControlType.unlimited, + rated: challenge.rated, + sideChoice: + challenge.side == null + ? SideChoice.random + : challenge.side == Side.white + ? SideChoice.white + : SideChoice.black, + days: challenge.days, + challenger: ( + user: challengerUser, + rating: challenge.rating, + provisionalRating: challenge.provisional, + lagRating: null, + ), + ), + challengerUser: challengerUser, + onPressed: onPressed, + onCancel: onCancel, + ); + } +} diff --git a/lib/src/view/play/common_play_widgets.dart b/lib/src/view/play/common_play_widgets.dart index 450473adf1..abe9d2b5e9 100644 --- a/lib/src/view/play/common_play_widgets.dart +++ b/lib/src/view/play/common_play_widgets.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -49,9 +49,7 @@ class _PlayRatingRangeState extends State { opacity: isRatingRangeAvailable ? 1 : 0.5, child: PlatformListTile( harmonizeCupertinoTitleStyle: true, - title: Text( - context.l10n.ratingRange, - ), + title: Text(context.l10n.ratingRange), subtitle: Row( mainAxisSize: MainAxisSize.max, children: [ @@ -61,27 +59,25 @@ class _PlayRatingRangeState extends State { NonLinearSlider( value: _subtract, values: kSubtractingRatingRange, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - _subtract = value.toInt(); - }); - } - : null, - onChangeEnd: isRatingRangeAvailable - ? (num value) { - widget.onRatingDeltaChange( - value.toInt(), - value == 0 && _add == 0 - ? kAddingRatingRange[1] - : _add, - ); - } - : null, - ), - Center( - child: Text('${_subtract == 0 ? '-' : ''}$_subtract'), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + _subtract = value.toInt(); + }); + } + : null, + onChangeEnd: + isRatingRangeAvailable + ? (num value) { + widget.onRatingDeltaChange( + value.toInt(), + value == 0 && _add == 0 ? kAddingRatingRange[1] : _add, + ); + } + : null, ), + Center(child: Text('${_subtract == 0 ? '-' : ''}$_subtract')), ], ), ), @@ -98,28 +94,27 @@ class _PlayRatingRangeState extends State { NonLinearSlider( value: _add, values: kAddingRatingRange, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - _add = value.toInt(); - }); - } - : null, - onChangeEnd: isRatingRangeAvailable - ? (num value) { - widget.onRatingDeltaChange( - value == 0 && _subtract == 0 - ? kSubtractingRatingRange[ - kSubtractingRatingRange.length - 2] - : _subtract, - value.toInt(), - ); - } - : null, - ), - Center( - child: Text('+$_add'), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + _add = value.toInt(); + }); + } + : null, + onChangeEnd: + isRatingRangeAvailable + ? (num value) { + widget.onRatingDeltaChange( + value == 0 && _subtract == 0 + ? kSubtractingRatingRange[kSubtractingRatingRange.length - 2] + : _subtract, + value.toInt(), + ); + } + : null, ), + Center(child: Text('+$_add')), ], ), ), diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart similarity index 66% rename from lib/src/view/play/challenge_screen.dart rename to lib/src/view/play/create_challenge_screen.dart index 536750fc07..62319a0df5 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -1,8 +1,6 @@ import 'dart:async'; -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,13 +9,17 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; @@ -26,28 +28,17 @@ import 'package:lichess_mobile/src/widgets/expanded_section.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class ChallengeScreen extends StatelessWidget { - const ChallengeScreen(this.user); +class CreateChallengeScreen extends StatelessWidget { + const CreateChallengeScreen(this.user); final LightUser user; @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _ChallengeBody(user), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.challengeChallengesX(user.name))), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.challengeChallengesX(user.name))), body: _ChallengeBody(user), ); } @@ -63,7 +54,7 @@ class _ChallengeBody extends ConsumerStatefulWidget { } class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { - Future? _pendingCreateGame; + Future? _pendingCorrespondenceChallenge; final _controller = TextEditingController(); String? fromPositionFenInput; @@ -91,12 +82,12 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { final isValidTimeControl = preferences.timeControl != ChallengeTimeControlType.clock || - preferences.clock.time > Duration.zero || - preferences.clock.increment > Duration.zero; + preferences.clock.time > Duration.zero || + preferences.clock.increment > Duration.zero; final isValidPosition = (fromPositionFenInput != null && fromPositionFenInput!.isNotEmpty) || - preferences.variant != Variant.fromPosition; + preferences.variant != Variant.fromPosition; return accountAsync.when( data: (account) { @@ -105,9 +96,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { return Center( child: ListView( shrinkWrap: true, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.sectionBottomPadding - : Styles.verticalBodyPadding, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionBottomPadding + : Styles.verticalBodyPadding, children: [ PlatformListTile( harmonizeCupertinoTitleStyle: true, @@ -119,22 +111,16 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { choices: [ ChallengeTimeControlType.clock, ChallengeTimeControlType.correspondence, - ChallengeTimeControlType.unlimited, ], selectedItem: preferences.timeControl, - labelBuilder: (ChallengeTimeControlType timeControl) => - Text( - timeControl == ChallengeTimeControlType.clock - ? context.l10n.realTime - : timeControl == - ChallengeTimeControlType.correspondence - ? context.l10n.correspondence - : context.l10n.unlimited, - ), + labelBuilder: + (ChallengeTimeControlType timeControl) => Text(switch (timeControl) { + ChallengeTimeControlType.clock => context.l10n.realTime, + ChallengeTimeControlType.correspondence => context.l10n.correspondence, + ChallengeTimeControlType.unlimited => context.l10n.unlimited, + }), onSelectedItemChanged: (ChallengeTimeControlType value) { - ref - .read(challengePreferencesProvider.notifier) - .setTimeControl(value); + ref.read(challengePreferencesProvider.notifier).setTimeControl(value); }, ); }, @@ -158,10 +144,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.minutesPerSide}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: clockLabelInMinutes(seconds), ), ], @@ -174,10 +157,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - seconds = value.toInt(); - }); - } + setState(() { + seconds = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -198,8 +181,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), Builder( builder: (context) { - int incrementSeconds = - preferences.clock.increment.inSeconds; + int incrementSeconds = preferences.clock.increment.inSeconds; return StatefulBuilder( builder: (context, setState) { return PlatformListTile( @@ -209,10 +191,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.incrementInSeconds}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: incrementSeconds.toString(), ), ], @@ -224,10 +203,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - incrementSeconds = value.toInt(); - }); - } + setState(() { + incrementSeconds = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -259,10 +238,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.daysPerTurn}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: _daysLabel(daysPerTurn), ), ], @@ -275,10 +251,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - daysPerTurn = value.toInt(); - }); - } + setState(() { + daysPerTurn = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -302,17 +278,11 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onPressed: () { showChoicePicker( context, - choices: [ - Variant.standard, - Variant.chess960, - Variant.fromPosition, - ], + choices: [Variant.standard, Variant.chess960, Variant.fromPosition], selectedItem: preferences.variant, labelBuilder: (Variant variant) => Text(variant.label), onSelectedItemChanged: (Variant variant) { - ref - .read(challengePreferencesProvider.notifier) - .setVariant(variant); + ref.read(challengePreferencesProvider.notifier).setVariant(variant); }, ); }, @@ -322,9 +292,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ExpandedSection( expand: preferences.variant == Variant.fromPosition, child: SmallBoardPreview( - orientation: preferences.sideChoice == SideChoice.black - ? cg.Side.black - : cg.Side.white, + orientation: preferences.sideChoice == SideChoice.black ? Side.black : Side.white, fen: fromPositionFenInput ?? kEmptyFen, description: AdaptiveTextField( maxLines: 5, @@ -336,8 +304,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), ), ExpandedSection( - expand: preferences.rated == false || - preferences.variant == Variant.fromPosition, + expand: preferences.rated == false || preferences.variant == Variant.fromPosition, child: PlatformListTile( harmonizeCupertinoTitleStyle: true, title: Text(context.l10n.side), @@ -347,18 +314,13 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { context, choices: SideChoice.values, selectedItem: preferences.sideChoice, - labelBuilder: (SideChoice side) => - Text(sideChoiceL10n(context.l10n, side)), + labelBuilder: (SideChoice side) => Text(side.label(context.l10n)), onSelectedItemChanged: (SideChoice side) { - ref - .read(challengePreferencesProvider.notifier) - .setSideChoice(side); + ref.read(challengePreferencesProvider.notifier).setSideChoice(side); }, ); }, - child: Text( - sideChoiceL10n(context.l10n, preferences.sideChoice), - ), + child: Text(preferences.sideChoice.label(context.l10n)), ), ), ), @@ -372,54 +334,69 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { applyCupertinoTheme: true, value: preferences.rated, onChanged: (bool value) { - ref - .read(challengePreferencesProvider.notifier) - .setRated(value); + ref.read(challengePreferencesProvider.notifier).setRated(value); }, ), ), ), const SizedBox(height: 20), FutureBuilder( - future: _pendingCreateGame, + future: _pendingCorrespondenceChallenge, builder: (context, snapshot) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: FatButton( semanticsLabel: context.l10n.challengeChallengeToPlay, - onPressed: timeControl == ChallengeTimeControlType.clock - ? isValidTimeControl && isValidPosition - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - challenge: preferences.makeRequest( - widget.user, - preferences.variant != - Variant.fromPosition - ? null - : fromPositionFenInput, - ), - ); - }, - ); - } - : null - : snapshot.connectionState == ConnectionState.waiting - ? null - // TODO handle correspondence time control - : () async { - showPlatformSnackbar( - context, - 'Correspondence time control is not supported yet', - ); - }, - child: Text( - context.l10n.challengeChallengeToPlay, - style: Styles.bold, - ), + onPressed: + timeControl == ChallengeTimeControlType.clock + ? isValidTimeControl && isValidPosition + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + challenge: preferences.makeRequest( + widget.user, + preferences.variant != Variant.fromPosition + ? null + : fromPositionFenInput, + ), + ); + }, + ); + } + : null + : timeControl == ChallengeTimeControlType.correspondence && + snapshot.connectionState != ConnectionState.waiting + ? () async { + final createGameService = ref.read(createGameServiceProvider); + _pendingCorrespondenceChallenge = createGameService + .newCorrespondenceChallenge( + preferences.makeRequest( + widget.user, + preferences.variant != Variant.fromPosition + ? null + : fromPositionFenInput, + ), + ); + + await _pendingCorrespondenceChallenge!; + + if (!context.mounted) return; + + Navigator.of(context).pop(); + + // Switch to the home tab + ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; + + // Navigate to the challenges screen where + // the new correspondence challenge will be + // displayed + pushPlatformRoute(context, screen: const ChallengeRequestsScreen()); + } + : null, + child: Text(context.l10n.challengeChallengeToPlay, style: Styles.bold), ), ); }, @@ -429,9 +406,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stackTrace) => const Center( - child: Text('Could not load account data'), - ), + error: (error, stackTrace) => const Center(child: Text('Could not load account data')), ); } @@ -441,13 +416,9 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { try { Chess.fromSetup(Setup.parseFen(data.text!.trim())); _controller.text = data.text!; - } catch (_, __) { + } catch (_) { if (mounted) { - showPlatformSnackbar( - context, - context.l10n.invalidFen, - type: SnackBarType.error, - ); + showPlatformSnackbar(context, context.l10n.invalidFen, type: SnackBarType.error); } } } diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index fe7c97f903..e6fc127d3e 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -1,29 +1,29 @@ import 'dart:async'; +import 'dart:ui'; -import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/play/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -32,7 +32,6 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'common_play_widgets.dart'; @@ -47,9 +46,13 @@ class CreateCustomGameScreen extends StatelessWidget { } Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _CupertinoBody(), + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor.resolveFrom(context).withValues(alpha: 0.0), + border: null, + ), + child: const _CupertinoBody(), ); } @@ -65,8 +68,7 @@ class _AndroidBody extends StatefulWidget { State<_AndroidBody> createState() => _AndroidBodyState(); } -class _AndroidBodyState extends State<_AndroidBody> - with TickerProviderStateMixin { +class _AndroidBodyState extends State<_AndroidBody> with TickerProviderStateMixin { late final TabController _tabController; @override @@ -105,8 +107,8 @@ class _AndroidBodyState extends State<_AndroidBody> body: TabBarView( controller: _tabController, children: [ - _CreateGameBody(setViewMode: setViewMode), - _ChallengesBody(setViewMode: setViewMode), + _TabView(sliver: _CreateGameBody(setViewMode: setViewMode)), + _TabView(sliver: _ChallengesBody(setViewMode: setViewMode)), ], ), ); @@ -122,45 +124,128 @@ class _CupertinoBody extends StatefulWidget { class _CupertinoBodyState extends State<_CupertinoBody> { _ViewMode _selectedSegment = _ViewMode.create; + double headerOpacity = 0; void setViewMode(_ViewMode mode) { setState(() { _selectedSegment = mode; + headerOpacity = 0.0; }); } + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; + } + @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Styles.bodyPadding, - child: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.create: Text(context.l10n.createAGame), - _ViewMode.challenges: - Text(context.l10n.mobileCustomGameJoinAGame), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, + final tabSwitcher = CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.create: Text(context.l10n.createAGame), + _ViewMode.challenges: Text(context.l10n.mobileCustomGameJoinAGame), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ); + return NotificationListener( + onNotification: handleScrollNotification, + child: + _selectedSegment == _ViewMode.create + ? _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _CreateGameBody(setViewMode: setViewMode), + ) + : _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _ChallengesBody(setViewMode: setViewMode), + ), + ); + } +} + +class _TabView extends StatelessWidget { + const _TabView({ + required this.sliver, + this.cupertinoTabSwitcher, + this.cupertinoHeaderOpacity = 0.0, + }); + + final Widget sliver; + final Widget? cupertinoTabSwitcher; + final double cupertinoHeaderOpacity; + + @override + Widget build(BuildContext context) { + final edgeInsets = + MediaQuery.paddingOf(context) - + (cupertinoTabSwitcher != null + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.verticalBodyPadding; + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); + return CustomScrollView( + slivers: [ + if (cupertinoTabSwitcher != null) + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: + cupertinoHeaderOpacity == 1.0 + ? backgroundColor + : backgroundColor.withAlpha(0), + shape: LinearBorder.bottom( + side: BorderSide( + color: + cupertinoHeaderOpacity == 1.0 + ? const Color(0x4D000000) + : Colors.transparent, + width: 0.0, + ), + ), + ), + padding: + Styles.bodyPadding + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: cupertinoTabSwitcher, + ), + ), ), ), - Expanded( - child: _selectedSegment == _ViewMode.create - ? _CreateGameBody(setViewMode: setViewMode) - : _ChallengesBody(setViewMode: setViewMode), - ), - ], - ), + SliverPadding(padding: edgeInsets, sliver: sliver), + ], ); } } @@ -183,8 +268,7 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { void initState() { super.initState(); - socketClient = - ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); + socketClient = ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); _socketSubscription = socketClient.stream.listen((event) { switch (event.topic) { @@ -224,100 +308,59 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { return challengesAsync.when( data: (challenges) { - final supportedChallenges = challenges - .where((challenge) => challenge.variant.isPlaySupported) - .toList(); - return ListView.separated( + final supportedChallenges = + challenges.where((challenge) => challenge.variant.isPlaySupported).toList(); + return SliverList.separated( itemCount: supportedChallenges.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final challenge = supportedChallenges[index]; - final time = challenge.days == null - ? '∞' - : '${context.l10n.daysPerTurn}: ${challenge.days}'; - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; - final isMySeek = - UserId.fromUserName(challenge.username) == session?.user.id; - - return Container( - color: isMySeek ? LichessColors.green.withOpacity(0.2) : null, - child: Slidable( - endActionPane: isMySeek - ? ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.3, - children: [ - SlidableAction( - onPressed: (BuildContext context) { - socketClient.send( - 'cancelSeek', - challenge.id.toString(), - ); - }, - backgroundColor: context.lichessColors.error, - foregroundColor: Colors.white, - icon: Icons.cancel, - label: context.l10n.cancel, - ), - ], - ) - : null, - child: PlatformListTile( - padding: Styles.bodyPadding, - leading: Icon(challenge.perf.icon), - trailing: Icon( - challenge.side == null - ? LichessIcons.adjust - : challenge.side == Side.white - ? LichessIcons.circle - : LichessIcons.circle_empty, - ), - title: UserFullNameWidget( - user: LightUser( - id: UserId.fromUserName(challenge.username), - name: challenge.username, - title: challenge.title, - ), - rating: challenge.rating, - provisional: challenge.provisional, - ), - subtitle: Text(subtitle), - onTap: isMySeek + final isMySeek = UserId.fromUserName(challenge.username) == session?.user.id; + + return CorrespondenceChallengeListItem( + challenge: challenge, + challengerUser: LightUser( + id: UserId.fromUserName(challenge.username), + name: challenge.username, + title: challenge.title, + ), + onPressed: + isMySeek ? null : session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) { - socketClient.send( - 'joinSeek', - challenge.id.toString(), - ); - }, - ); - }, - ), - ), + ? () { + showPlatformSnackbar(context, context.l10n.youNeedAnAccountToDoThat); + } + : () { + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { + socketClient.send('joinSeek', challenge.id.toString()); + }, + ); + }, + onCancel: + isMySeek + ? () { + socketClient.send('cancelSeek', challenge.id.toString()); + } + : null, ); }, ); }, loading: () { - return const Center(child: CircularProgressIndicator.adaptive()); + return const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ); }, - error: (error, stack) => - Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), + error: + (error, stack) => SliverFillRemaining( + child: Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), + ), ); } } @@ -338,8 +381,8 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { Widget build(BuildContext context) { final accountAsync = ref.watch(accountProvider); final preferences = ref.watch(gameSetupPreferencesProvider); - final isValidTimeControl = preferences.customTimeSeconds > 0 || - preferences.customIncrementSeconds > 0; + final isValidTimeControl = + preferences.customTimeSeconds > 0 || preferences.customIncrementSeconds > 0; final realTimeSelector = [ Builder( @@ -354,10 +397,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.minutesPerSide}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: clockLabelInMinutes(customTimeSeconds), ), ], @@ -367,13 +407,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { value: customTimeSeconds, values: kAvailableTimesInSeconds, labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - customTimeSeconds = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + customTimeSeconds = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { customTimeSeconds = value.toInt(); @@ -400,10 +441,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.incrementInSeconds}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: customIncrementSeconds.toString(), ), ], @@ -412,13 +450,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { subtitle: NonLinearSlider( value: customIncrementSeconds, values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - customIncrementSeconds = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + customIncrementSeconds = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { customIncrementSeconds = value.toInt(); @@ -448,10 +487,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.daysPerTurn}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: _daysLabel(daysPerTurn), ), ], @@ -461,13 +497,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { value: daysPerTurn, values: kAvailableDaysPerTurn, labelBuilder: _daysLabel, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - daysPerTurn = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + daysPerTurn = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { daysPerTurn = value.toInt(); @@ -486,20 +523,16 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { return accountAsync.when( data: (account) { - final timeControl = account == null - ? TimeControl.realTime - : preferences.customTimeControl; - - final userPerf = account?.perfs[timeControl == TimeControl.realTime - ? preferences.perfFromCustom - : Perf.correspondence]; - return Center( - child: ListView( - shrinkWrap: true, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.sectionBottomPadding - : Styles.verticalBodyPadding, - children: [ + final timeControl = account == null ? TimeControl.realTime : preferences.customTimeControl; + + final userPerf = + account?.perfs[timeControl == TimeControl.realTime + ? preferences.perfFromCustom + : Perf.correspondence]; + return SliverPadding( + padding: Styles.sectionBottomPadding, + sliver: SliverList( + delegate: SliverChildListDelegate([ if (account != null) PlatformListTile( harmonizeCupertinoTitleStyle: true, @@ -508,16 +541,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { onPressed: () { showChoicePicker( context, - choices: [ - TimeControl.realTime, - TimeControl.correspondence, - ], + choices: [TimeControl.realTime, TimeControl.correspondence], selectedItem: preferences.customTimeControl, - labelBuilder: (TimeControl timeControl) => Text( - timeControl == TimeControl.realTime - ? context.l10n.realTime - : context.l10n.correspondence, - ), + labelBuilder: + (TimeControl timeControl) => Text( + timeControl == TimeControl.realTime + ? context.l10n.realTime + : context.l10n.correspondence, + ), onSelectedItemChanged: (TimeControl value) { ref .read(gameSetupPreferencesProvider.notifier) @@ -547,9 +578,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { selectedItem: preferences.customVariant, labelBuilder: (Variant variant) => Text(variant.label), onSelectedItemChanged: (Variant variant) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomVariant(variant); + ref.read(gameSetupPreferencesProvider.notifier).setCustomVariant(variant); }, ); }, @@ -562,23 +591,8 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { harmonizeCupertinoTitleStyle: true, title: Text(context.l10n.side), trailing: AdaptiveTextButton( - onPressed: () { - showChoicePicker( - context, - choices: PlayableSide.values, - selectedItem: preferences.customSide, - labelBuilder: (PlayableSide side) => - Text(playableSideL10n(context.l10n, side)), - onSelectedItemChanged: (PlayableSide side) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomSide(side); - }, - ); - }, - child: Text( - playableSideL10n(context.l10n, preferences.customSide), - ), + onPressed: null, + child: Text(SideChoice.random.label(context.l10n)), ), ), ), @@ -590,9 +604,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { applyCupertinoTheme: true, value: preferences.customRated, onChanged: (bool value) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomRated(value); + ref.read(gameSetupPreferencesProvider.notifier).setCustomRated(value); }, ), ), @@ -614,51 +626,48 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { padding: const EdgeInsets.symmetric(horizontal: 20.0), child: FatButton( semanticsLabel: context.l10n.createAGame, - onPressed: timeControl == TimeControl.realTime - ? isValidTimeControl - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - seek: GameSeek.custom( - preferences, - account, - ), - ); - }, - ); - } - : null - : snapshot.connectionState == ConnectionState.waiting + onPressed: + timeControl == TimeControl.realTime + ? isValidTimeControl + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + seek: GameSeek.custom(preferences, account), + ); + }, + ); + } + : null + : snapshot.connectionState == ConnectionState.waiting ? null : () async { - _pendingCreateGame = ref - .read(createGameServiceProvider) - .newCorrespondenceGame( - GameSeek.correspondence( - preferences, - account, - ), - ); - - await _pendingCreateGame; - widget.setViewMode(_ViewMode.challenges); - }, + _pendingCreateGame = ref + .read(createGameServiceProvider) + .newCorrespondenceGame( + GameSeek.correspondence(preferences, account), + ); + + await _pendingCreateGame; + widget.setViewMode(_ViewMode.challenges); + }, child: Text(context.l10n.createAGame, style: Styles.bold), ), ); }, ), - ], + ]), ), ); }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stackTrace) => const Center( - child: Text('Could not load account data'), - ), + loading: + () => + const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), + error: + (error, stackTrace) => + const SliverFillRemaining(child: Center(child: Text('Could not load account data'))), ); } } diff --git a/lib/src/view/play/create_game_options.dart b/lib/src/view/play/create_game_options.dart new file mode 100644 index 0000000000..c5c1bdb528 --- /dev/null +++ b/lib/src/view/play/create_game_options.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; +import 'package:lichess_mobile/src/view/play/create_custom_game_screen.dart'; +import 'package:lichess_mobile/src/view/play/online_bots_screen.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +/// A widget that displays the options for creating a game. +class CreateGameOptions extends ConsumerWidget { + const CreateGameOptions(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + return Column( + children: [ + _Section( + children: [ + _CreateGamePlatformButton( + onTap: + isOnline + ? () { + ref.invalidate(accountProvider); + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, + icon: Icons.tune, + label: context.l10n.custom, + ), + _CreateGamePlatformButton( + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.onlineBots, + builder: (_) => const OnlineBotsScreen(), + ); + } + : null, + icon: Icons.computer, + label: context.l10n.onlineBots, + ), + ], + ), + _Section( + children: [ + _CreateGamePlatformButton( + onTap: () { + pushPlatformRoute( + context, + title: 'Over the Board', + rootNavigator: true, + builder: (_) => const OverTheBoardScreen(), + ); + }, + icon: LichessIcons.chess_board, + label: 'Over the board', + ), + ], + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + const _Section({required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return Theme.of(context).platform == TargetPlatform.iOS + ? ListSection(hasLeading: true, children: children) + : Padding( + padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: children), + ); + } +} + +class _CreateGamePlatformButton extends StatelessWidget { + const _CreateGamePlatformButton({required this.icon, required this.label, required this.onTap}); + + final IconData icon; + + final String label; + + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return Theme.of(context).platform == TargetPlatform.iOS + ? PlatformListTile( + leading: Icon(icon, size: 28), + trailing: const CupertinoListTileChevron(), + title: Text(label, style: Styles.mainListTileTitle), + onTap: onTap, + ) + : ElevatedButton.icon(onPressed: onTap, icon: Icon(icon), label: Text(label)); + } +} diff --git a/lib/src/view/play/ongoing_games_screen.dart b/lib/src/view/play/ongoing_games_screen.dart index 2ebda0b091..97a0471701 100644 --- a/lib/src/view/play/ongoing_games_screen.dart +++ b/lib/src/view/play/ongoing_games_screen.dart @@ -4,40 +4,31 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; class OngoingGamesScreen extends ConsumerWidget { const OngoingGamesScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); + return ConsumerPlatformWidget(ref: ref, androidBuilder: _buildAndroid, iosBuilder: _buildIos); } Widget _buildIos(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } Widget _buildAndroid(BuildContext context, WidgetRef ref) { final ongoingGames = ref.watch(ongoingGamesProvider); return Scaffold( appBar: ongoingGames.maybeWhen( - data: (data) => - AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), + data: (data) => AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), orElse: () => AppBar(title: const SizedBox.shrink()), ), body: _Body(), @@ -50,12 +41,13 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ongoingGames = ref.watch(ongoingGamesProvider); return ongoingGames.maybeWhen( - data: (data) => ListView( - children: [ - const SizedBox(height: 8.0), - ...data.map((game) => OngoingGamePreview(game: game)), - ], - ), + data: + (data) => ListView( + children: [ + const SizedBox(height: 8.0), + ...data.map((game) => OngoingGamePreview(game: game)), + ], + ), orElse: () => const SizedBox.shrink(), ); } @@ -69,8 +61,8 @@ class OngoingGamePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SmallBoardPreview( - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, + orientation: game.orientation, + lastMove: game.lastMove, fen: game.fen, description: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -85,20 +77,13 @@ class OngoingGamePreview extends ConsumerWidget { Icon( game.perf.icon, size: 34, - color: DefaultTextStyle.of(context).style.color?.withOpacity(0.6), + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), ), if (game.secondsLeft != null && game.secondsLeft! > 0) - Text( - game.isMyTurn - ? context.l10n.yourTurn - : context.l10n.waitingForOpponent, - ), + Text(game.isMyTurn ? context.l10n.yourTurn : context.l10n.waitingForOpponent), if (game.isMyTurn && game.secondsLeft != null) Text( - timeago.format( - DateTime.now().add(Duration(seconds: game.secondsLeft!)), - allowFromNow: true, - ), + relativeDate(context.l10n, DateTime.now().add(Duration(seconds: game.secondsLeft!))), ), ], ), @@ -106,12 +91,13 @@ class OngoingGamePreview extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => GameScreen( - initialGameId: game.fullId, - loadingFen: game.fen, - loadingOrientation: game.orientation, - loadingLastMove: game.lastMove, - ), + builder: + (context) => GameScreen( + initialGameId: game.fullId, + loadingFen: game.fen, + loadingOrientation: game.orientation, + loadingLastMove: game.lastMove, + ), ).then((_) { if (context.mounted) { ref.invalidate(ongoingGamesProvider); diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 0dfd06a20c..3961253ee8 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -4,73 +4,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/play/challenge_screen.dart'; +import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:linkify/linkify.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:url_launcher/url_launcher.dart'; -part 'online_bots_screen.g.dart'; - // TODO(#796): remove when Leela featured bots special challenges are ready // https://github.com/lichess-org/mobile/issues/796 -const _disabledBots = { - 'leelaknightodds', - 'leelaqueenodds', - 'leelaqueenforknight', - 'leelarookodds', -}; +const _disabledBots = {'leelaknightodds', 'leelaqueenodds', 'leelaqueenforknight', 'leelarookodds'}; -@riverpod -Future> _onlineBots(_OnlineBotsRef ref) async { +final _onlineBotsProvider = FutureProvider.autoDispose>((ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getOnlineBots().then( - (bots) => bots - .whereNot( - (bot) => _disabledBots.contains(bot.id.value.toLowerCase()), - ) - .toIList(), - ), + (bots) => + bots.whereNot((bot) => _disabledBots.contains(bot.id.value.toLowerCase())).toIList(), + ), const Duration(hours: 5), ); -} +}); class OnlineBotsScreen extends StatelessWidget { const OnlineBotsScreen(); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.onlineBots), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.onlineBots)), body: _Body(), ); } @@ -82,198 +54,162 @@ class _Body extends ConsumerWidget { final onlineBots = ref.watch(_onlineBotsProvider); return onlineBots.when( - data: (data) => ListView.separated( - itemCount: data.length, - separatorBuilder: (context, index) => - Theme.of(context).platform == TargetPlatform.iOS - ? Divider( - height: 0, - thickness: 0, - // equals to _kNotchedPaddingWithoutLeading constant - // See: https://github.com/flutter/flutter/blob/89ea49204b37523a16daec53b5e6fae70995929d/packages/flutter/lib/src/cupertino/list_tile.dart#L24 - indent: 28, - color: CupertinoDynamicColor.resolve( - CupertinoColors.separator, - context, - ), - ) - : const SizedBox.shrink(), - itemBuilder: (context, index) { - final bot = data[index]; - return PlatformListTile( - isThreeLine: true, - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? Row( - children: [ - if (bot.verified == true) ...[ - const Icon(Icons.verified_outlined), - const SizedBox(width: 5), - ], - const CupertinoListTileChevron(), - ], - ) - : bot.verified == true - ? const Icon(Icons.verified_outlined) - : null, - title: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: UserFullNameWidget( - user: bot.lightUser, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - subtitle: Column( - children: [ - Row( - children: - [Perf.blitz, Perf.rapid, Perf.classical].map((perf) { - final rating = bot.perfs[perf]?.rating; - final nbGames = bot.perfs[perf]?.games ?? 0; - return Padding( - padding: const EdgeInsets.only( - right: 16.0, - top: 4.0, - bottom: 4.0, - ), - child: Row( - children: [ - Icon(perf.icon, size: 16), - const SizedBox(width: 4.0), - if (rating != null && nbGames > 0) - Text( - '$rating', - style: const TextStyle( - color: Colors.grey, - ), - ) - else - const Text(' - '), - ], - ), - ); - }).toList(), + data: + (data) => ListView.separated( + itemCount: data.length, + separatorBuilder: + (context, index) => + Theme.of(context).platform == TargetPlatform.iOS + ? Divider( + height: 0, + thickness: 0, + // equals to _kNotchedPaddingWithoutLeading constant + // See: https://github.com/flutter/flutter/blob/89ea49204b37523a16daec53b5e6fae70995929d/packages/flutter/lib/src/cupertino/list_tile.dart#L24 + indent: 28, + color: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), + ) + : const SizedBox.shrink(), + itemBuilder: (context, index) { + final bot = data[index]; + return PlatformListTile( + isThreeLine: true, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? Row( + children: [ + if (bot.verified == true) ...[ + const Icon(Icons.verified_outlined), + const SizedBox(width: 5), + ], + const CupertinoListTileChevron(), + ], + ) + : bot.verified == true + ? const Icon(Icons.verified_outlined) + : null, + title: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: UserFullNameWidget( + user: bot.lightUser, + style: const TextStyle(fontWeight: FontWeight.w600), + ), ), - Text( - bot.profile?.bio ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, + subtitle: Column( + children: [ + Row( + children: + [Perf.blitz, Perf.rapid, Perf.classical].map((perf) { + final rating = bot.perfs[perf]?.rating; + final nbGames = bot.perfs[perf]?.games ?? 0; + return Padding( + padding: const EdgeInsets.only(right: 16.0, top: 4.0, bottom: 4.0), + child: Row( + children: [ + Icon(perf.icon, size: 16), + const SizedBox(width: 4.0), + if (rating != null && nbGames > 0) + Text('$rating', style: const TextStyle(color: Colors.grey)) + else + const Text(' - '), + ], + ), + ); + }).toList(), + ), + Text(bot.profile?.bio ?? '', maxLines: 2, overflow: TextOverflow.ellipsis), + ], ), - ], - ), - onTap: () { - final session = ref.read(authSessionProvider); - if (session == null) { - showPlatformSnackbar( - context, - context.l10n.challengeRegisterToSendChallenges, - type: SnackBarType.error, - ); - return; - } - pushPlatformRoute( - context, - title: context.l10n.challengeChallengesX(bot.lightUser.name), - builder: (context) => ChallengeScreen(bot.lightUser), - ); - }, - onLongPress: () { - showAdaptiveBottomSheet( - context: context, - useRootNavigator: true, - isDismissible: true, - isScrollControlled: true, - builder: (context) => _ContextMenu(bot: bot), + onTap: () { + final session = ref.read(authSessionProvider); + if (session == null) { + showPlatformSnackbar( + context, + context.l10n.challengeRegisterToSendChallenges, + type: SnackBarType.error, + ); + return; + } + pushPlatformRoute( + context, + title: context.l10n.challengeChallengesX(bot.lightUser.name), + builder: (context) => CreateChallengeScreen(bot.lightUser), + ); + }, + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => _ContextMenu(bot: bot), + ); + }, ); }, - ); - }, - ), + ), loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { debugPrint('Could not load bots: $e'); - return FullScreenRetryRequest( - onRetry: () => ref.refresh(_onlineBotsProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.refresh(_onlineBotsProvider)); }, ); } } class _ContextMenu extends ConsumerWidget { - const _ContextMenu({ - required this.bot, - }); + const _ContextMenu({required this.bot}); final User bot; @override Widget build(BuildContext context, WidgetRef ref) { - return DraggableScrollableSheet( - initialChildSize: .6, - expand: false, - snap: true, - snapSizes: const [.6, .8], - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - Padding( - padding: Styles.bodyPadding.add(const EdgeInsets.only(top: 8.0)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - UserFullNameWidget( - user: bot.lightUser, - style: Styles.title, - ), - const SizedBox(height: 8.0), - if (bot.profile?.bio != null) - Linkify( - onOpen: (link) async { - if (link.originText.startsWith('@')) { - final username = link.originText.substring(1); - pushPlatformRoute( - context, - builder: (ctx) => UserScreen( - user: LightUser( - id: UserId.fromUserName(username), - name: username, + return BottomSheetScrollableContainer( + children: [ + Padding( + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 16.0)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + UserFullNameWidget(user: bot.lightUser, style: Styles.title), + const SizedBox(height: 8.0), + if (bot.profile?.bio != null) + Linkify( + onOpen: (link) async { + if (link.originText.startsWith('@')) { + final username = link.originText.substring(1); + pushPlatformRoute( + context, + builder: + (ctx) => UserScreen( + user: LightUser(id: UserId.fromUserName(username), name: username), ), - ), - ); - } else { - launchUrl(Uri.parse(link.url)); - } - }, - linkifiers: const [ - UrlLinkifier(), - EmailLinkifier(), - UserTagLinkifier(), - ], - text: bot.profile!.bio!, - maxLines: 20, - overflow: TextOverflow.ellipsis, - linkStyle: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.none, - ), + ); + } else { + launchUrl(Uri.parse(link.url)); + } + }, + linkifiers: const [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()], + text: bot.profile!.bio!, + maxLines: 20, + overflow: TextOverflow.ellipsis, + linkStyle: const TextStyle( + color: Colors.blueAccent, + decoration: TextDecoration.none, ), - ], - ), - ), - BottomSheetContextMenuAction( - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: bot.lightUser, ), - ); - }, - icon: Icons.person, - child: Text(context.l10n.profile), + ], ), - ], - ), + ), + const PlatformDivider(), + BottomSheetContextMenuAction( + onPressed: () { + pushPlatformRoute(context, builder: (context) => UserScreen(user: bot.lightUser)); + }, + icon: Icons.person, + child: Text(context.l10n.profile), + ), + ], ); } } diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 8523537850..d206c3052c 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -2,50 +2,26 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/view/home/create_game_options.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +import 'create_game_options.dart'; class PlayScreen extends StatelessWidget { const PlayScreen(); @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.play), - ), - body: const _Body(), - ); - } -} - -class _Body extends StatelessWidget { - const _Body(); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameButton(), - ), - const CreateGameOptions(), - ], + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.play)), + body: const SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding(padding: Styles.horizontalBodyPadding, child: QuickGameButton()), + CreateGameOptions(), + ], + ), ), ); } diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index db28d5905d..7c20db1ecf 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -20,6 +21,7 @@ class QuickGameButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final playPrefs = ref.watch(gameSetupPreferencesProvider); final session = ref.watch(authSessionProvider); + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return Row( children: [ @@ -48,9 +50,7 @@ class QuickGameButton extends ConsumerWidget { context: context, isScrollControlled: true, showDragHandle: true, - constraints: BoxConstraints( - maxHeight: screenHeight - (screenHeight / 10), - ), + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), builder: (BuildContext context) { return TimeControlModal( value: playPrefs.quickPairingTimeIncrement, @@ -65,48 +65,53 @@ class QuickGameButton extends ConsumerWidget { }, ), ), - if (Theme.of(context).platform == TargetPlatform.android) - const SizedBox(width: 8.0), + if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(width: 8.0), Expanded( flex: kFlexGoldenRatio, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.filled( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 16.0, - ), - onPressed: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - }, - child: Text(context.l10n.play, style: Styles.bold), - ) - : FilledButton( - onPressed: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton.tinted( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), + onPressed: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, child: Text(context.l10n.play, style: Styles.bold), + ) + : FilledButton( + onPressed: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(context.l10n.play, style: Styles.bold), + ), ), - ), ), ], ); diff --git a/lib/src/view/play/quick_game_matrix.dart b/lib/src/view/play/quick_game_matrix.dart index 970485e734..d7aaff84ab 100644 --- a/lib/src/view/play/quick_game_matrix.dart +++ b/lib/src/view/play/quick_game_matrix.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -20,9 +21,8 @@ class QuickGameMatrix extends StatelessWidget { @override Widget build(BuildContext context) { final brightness = Theme.of(context).brightness; - final logoColor = brightness == Brightness.light - ? const Color(0x0F000000) - : const Color(0x80FFFFFF); + final logoColor = + brightness == Brightness.light ? const Color(0x0F000000) : const Color(0x80FFFFFF); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -40,35 +40,18 @@ class QuickGameMatrix extends StatelessWidget { child: const Column( children: [ _SectionChoices( - choices: [ - TimeIncrement(60, 0), - TimeIncrement(120, 1), - TimeIncrement(180, 0), - ], + choices: [TimeIncrement(60, 0), TimeIncrement(120, 1), TimeIncrement(180, 0)], ), SizedBox(height: _kMatrixSpacing), _SectionChoices( - choices: [ - TimeIncrement(180, 2), - TimeIncrement(300, 0), - TimeIncrement(300, 3), - ], + choices: [TimeIncrement(180, 2), TimeIncrement(300, 0), TimeIncrement(300, 3)], ), SizedBox(height: _kMatrixSpacing), _SectionChoices( - choices: [ - TimeIncrement(600, 0), - TimeIncrement(600, 5), - TimeIncrement(900, 10), - ], + choices: [TimeIncrement(600, 0), TimeIncrement(600, 5), TimeIncrement(900, 10)], ), SizedBox(height: _kMatrixSpacing), - _SectionChoices( - choices: [ - TimeIncrement(1800, 0), - TimeIncrement(1800, 20), - ], - ), + _SectionChoices(choices: [TimeIncrement(1800, 0), TimeIncrement(1800, 20)]), ], ), ), @@ -85,37 +68,37 @@ class _SectionChoices extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - final choiceWidgets = choices - .mapIndexed((index, choice) { - return [ - Expanded( - child: _ChoiceChip( - key: ValueKey(choice), - label: Text( - choice.display, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 20.0, + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final choiceWidgets = + choices + .mapIndexed((index, choice) { + return [ + Expanded( + child: _ChoiceChip( + key: ValueKey(choice), + label: Text( + choice.display, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0), + ), + speed: choice.speed, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen(seek: GameSeek.fastPairing(choice, session)), + ); + } + : null, ), ), - speed: choice.speed, - onSelected: (bool selected) { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing(choice, session), - ), - ); - }, - ), - ), - if (index < choices.length - 1) - const SizedBox(width: _kMatrixSpacing), - ]; - }) - .flattened - .toList(); + if (index < choices.length - 1) const SizedBox(width: _kMatrixSpacing), + ]; + }) + .flattened + .toList(); return IntrinsicHeight( child: Row( @@ -127,12 +110,15 @@ class _SectionChoices extends ConsumerWidget { Expanded( child: _ChoiceChip( label: Text(context.l10n.custom), - onSelected: (bool selected) { - pushPlatformRoute( - context, - builder: (_) => const CreateCustomGameScreen(), - ); - }, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, ), ), ], @@ -143,16 +129,11 @@ class _SectionChoices extends ConsumerWidget { } class _ChoiceChip extends StatefulWidget { - const _ChoiceChip({ - required this.label, - this.speed, - required this.onSelected, - super.key, - }); + const _ChoiceChip({required this.label, this.speed, required this.onTap, super.key}); final Widget label; final Speed? speed; - final void Function(bool selected) onSelected; + final void Function()? onTap; @override State<_ChoiceChip> createState() => _ChoiceChipState(); @@ -161,22 +142,26 @@ class _ChoiceChip extends StatefulWidget { class _ChoiceChipState extends State<_ChoiceChip> { @override Widget build(BuildContext context) { - final cardColor = Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context).withOpacity(0.7) - : Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.7); + final cardColor = + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context).withValues(alpha: 0.7) + : Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.7); - return Container( - decoration: BoxDecoration( - color: cardColor, - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - ), - child: AdaptiveInkWell( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - onTap: () => widget.onSelected(true), - splashColor: Theme.of(context).primaryColor.withOpacity(0.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Center(child: widget.label), + return Opacity( + opacity: widget.onTap != null ? 1.0 : 0.5, + child: Container( + decoration: BoxDecoration( + color: cardColor, + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + ), + child: AdaptiveInkWell( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + onTap: widget.onTap, + splashColor: Theme.of(context).primaryColor.withValues(alpha: 0.2), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: widget.label), + ), ), ), ); diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index 8871621e1f..5ff39f8fa2 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -3,25 +3,27 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; class TimeControlModal extends ConsumerWidget { - final ValueSetter onSelected; - final TimeIncrement value; - final bool excludeUltraBullet; - const TimeControlModal({ - required this.onSelected, required this.value, + required this.onSelected, this.excludeUltraBullet = false, super.key, }); + final TimeIncrement value; + final ValueSetter onSelected; + + final bool excludeUltraBullet; + @override Widget build(BuildContext context, WidgetRef ref) { void onSelected(TimeIncrement choice) { @@ -29,184 +31,144 @@ class TimeControlModal extends ConsumerWidget { this.onSelected(choice); } - return SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: ListView( - shrinkWrap: true, - children: [ - Text( - context.l10n.timeControl, - style: Styles.title, - ), - const SizedBox(height: 26.0), - _SectionChoices( - value, - choices: [ - if (!excludeUltraBullet) const TimeIncrement(0, 1), - const TimeIncrement(60, 0), - const TimeIncrement(60, 1), - const TimeIncrement(120, 1), - ], - title: const _SectionTitle( - title: 'Bullet', - icon: LichessIcons.bullet, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(180, 0), - TimeIncrement(180, 2), - TimeIncrement(300, 0), - TimeIncrement(300, 3), - ], - title: const _SectionTitle( - title: 'Blitz', - icon: LichessIcons.blitz, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(600, 0), - TimeIncrement(600, 5), - TimeIncrement(900, 0), - TimeIncrement(900, 10), - ], - title: const _SectionTitle( - title: 'Rapid', - icon: LichessIcons.rapid, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(1500, 0), - TimeIncrement(1800, 0), - TimeIncrement(1800, 20), - TimeIncrement(3600, 0), - ], - title: const _SectionTitle( - title: 'Classical', - icon: LichessIcons.classical, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - Theme( - data: - Theme.of(context).copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - title: _SectionTitle( - title: context.l10n.custom, - icon: Icons.tune, - ), - tilePadding: EdgeInsets.zero, - minTileHeight: 0, - children: [ - Builder( - builder: (context) { - TimeIncrement custom = value; - return StatefulBuilder( - builder: (context, setState) { - return Column( + return BottomSheetScrollableContainer( + padding: Styles.bodyPadding, + children: [ + Text(context.l10n.timeControl, style: Styles.title), + const SizedBox(height: 26.0), + _SectionChoices( + value, + choices: [ + if (!excludeUltraBullet) const TimeIncrement(0, 1), + const TimeIncrement(60, 0), + const TimeIncrement(60, 1), + const TimeIncrement(120, 1), + ], + title: const _SectionTitle(title: 'Bullet', icon: LichessIcons.bullet), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(180, 0), + TimeIncrement(180, 2), + TimeIncrement(300, 0), + TimeIncrement(300, 3), + ], + title: const _SectionTitle(title: 'Blitz', icon: LichessIcons.blitz), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(600, 0), + TimeIncrement(600, 5), + TimeIncrement(900, 0), + TimeIncrement(900, 10), + ], + title: const _SectionTitle(title: 'Rapid', icon: LichessIcons.rapid), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(1500, 0), + TimeIncrement(1800, 0), + TimeIncrement(1800, 20), + TimeIncrement(3600, 0), + ], + title: const _SectionTitle(title: 'Classical', icon: LichessIcons.classical), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: _SectionTitle(title: context.l10n.custom, icon: Icons.tune), + tilePadding: EdgeInsets.zero, + minTileHeight: 0, + children: [ + Builder( + builder: (context) { + TimeIncrement custom = value; + return StatefulBuilder( + builder: (context, setState) { + return Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, children: [ - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: NonLinearSlider( - value: custom.time, - values: kAvailableTimesInSeconds, - labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == - TargetPlatform.iOS + Expanded( + child: NonLinearSlider( + value: custom.time, + values: kAvailableTimesInSeconds, + labelBuilder: clockLabelInMinutes, + onChange: + Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); - }); - } + setState(() { + custom = TimeIncrement( + value.toInt(), + custom.increment, + ); + }); + } : null, - onChangeEnd: (num value) { - setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); - }); - }, - ), - ), - SizedBox( - width: 80, - child: Center( - child: Text( - custom.display, - style: Styles.timeControl - .merge(Styles.bold), - ), - ), + onChangeEnd: (num value) { + setState(() { + custom = TimeIncrement(value.toInt(), custom.increment); + }); + }, + ), + ), + SizedBox( + width: 80, + child: Center( + child: Text( + custom.display, + style: Styles.timeControl.merge(Styles.bold), ), - Expanded( - child: NonLinearSlider( - value: custom.increment, - values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == - TargetPlatform.iOS + ), + ), + Expanded( + child: NonLinearSlider( + value: custom.increment, + values: kAvailableIncrementsInSeconds, + onChange: + Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); - }); - } + setState(() { + custom = TimeIncrement(custom.time, value.toInt()); + }); + } : null, - onChangeEnd: (num value) { - setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); - }); - }, - ), - ), - ], - ), - SecondaryButton( - onPressed: custom.isInfinite - ? null - : () => onSelected(custom), - semanticsLabel: 'OK', - child: Text( - context.l10n.mobileOkButton, - style: Styles.bold, + onChangeEnd: (num value) { + setState(() { + custom = TimeIncrement(custom.time, value.toInt()); + }); + }, ), ), ], - ); - }, + ), + SecondaryButton( + onPressed: custom.isInfinite ? null : () => onSelected(custom), + semanticsLabel: 'OK', + child: Text(context.l10n.mobileOkButton, style: Styles.bold), + ), + ], ); }, - ), - ], + ); + }, ), - ), - const SizedBox(height: 40.0), - ], + ], + ), ), - ), + ], ); } } @@ -226,46 +188,38 @@ class _SectionChoices extends StatelessWidget { @override Widget build(BuildContext context) { - final choiceWidgets = choices - .mapIndexed((index, choice) { - return [ - Expanded( - child: _ChoiceChip( - key: ValueKey(choice), - label: Text(choice.display, style: Styles.bold), - selected: selected == choice, - onSelected: (bool selected) { - if (selected) onSelected(choice); - }, - ), - ), - if (index < choices.length - 1) const SizedBox(width: 10), - ]; - }) - .flattened - .toList(); + final choiceWidgets = + choices + .mapIndexed((index, choice) { + return [ + Expanded( + child: _ChoiceChip( + key: ValueKey(choice), + label: Text(choice.display, style: Styles.bold), + selected: selected == choice, + onSelected: (bool selected) { + if (selected) onSelected(choice); + }, + ), + ), + if (index < choices.length - 1) const SizedBox(width: 10), + ]; + }) + .flattened + .toList(); if (choices.length < 4) { final placeHolders = [ const [SizedBox(width: 10)], for (int i = choices.length; i < 4; i++) - [ - const Expanded(child: SizedBox(width: 10)), - if (i < 3) const SizedBox(width: 10), - ], + [const Expanded(child: SizedBox(width: 10)), if (i < 3) const SizedBox(width: 10)], ]; choiceWidgets.addAll(placeHolders.flattened); } return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 10), - Row( - children: choiceWidgets, - ), - ], + children: [title, const SizedBox(height: 10), Row(children: choiceWidgets)], ); } } @@ -286,36 +240,24 @@ class _ChoiceChip extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondarySystemGroupedBackground - .resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainerHighest, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondarySystemGroupedBackground.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.all(Radius.circular(5.0)), - border: selected - ? Border.fromBorderSide( - BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0, - ), - ) - : const Border.fromBorderSide( - BorderSide( - color: Colors.transparent, - width: 2.0, - ), - ), + border: + selected + ? Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ) + : const Border.fromBorderSide(BorderSide(color: Colors.transparent, width: 2.0)), ), child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(5.0)), onTap: () => onSelected(true), child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Center( - child: DefaultTextStyle.merge( - style: Styles.timeControl, - child: label, - ), - ), + child: Center(child: DefaultTextStyle.merge(style: Styles.timeControl, child: label)), ), ), ); @@ -331,17 +273,12 @@ class _SectionTitle extends StatelessWidget { @override Widget build(BuildContext context) { return IconTheme( - data: CupertinoIconThemeData( - color: CupertinoColors.systemGrey.resolveFrom(context), - ), + data: CupertinoIconThemeData(color: CupertinoColors.systemGrey.resolveFrom(context)), child: Row( children: [ Icon(icon, size: 20.0), const SizedBox(width: 10), - Text( - title, - style: _titleStyle, - ), + Text(title, style: _titleStyle), ], ), ); diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index 147ee226a0..49a09e9067 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -1,13 +1,11 @@ import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show ClientException; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -15,6 +13,7 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; @@ -25,21 +24,10 @@ class PuzzleDashboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: SizedBox.shrink(), - trailing: DaysSelector(), - ), - child: _Body(), - ) - : Scaffold( - body: const _Body(), - appBar: AppBar( - title: const SizedBox.shrink(), - actions: const [DaysSelector()], - ), - ); + return const PlatformScaffold( + body: _Body(), + appBar: PlatformAppBar(title: SizedBox.shrink(), actions: [DaysSelector()]), + ); } } @@ -48,30 +36,21 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PuzzleDashboardWidget(), - ], - ), - ); + return ListView(children: [PuzzleDashboardWidget()]); } } class PuzzleDashboardWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final puzzleDashboard = - ref.watch(puzzleDashboardProvider(ref.watch(daysProvider).days)); + final puzzleDashboard = ref.watch(puzzleDashboardProvider(ref.watch(daysProvider).days)); return puzzleDashboard.when( data: (dashboard) { if (dashboard == null) { return const SizedBox.shrink(); } - final chartData = - dashboard.themes.take(9).sortedBy((e) => e.theme.name).toList(); + final chartData = dashboard.themes.take(9).sortedBy((e) => e.theme.name).toList(); return ListSection( header: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -79,9 +58,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { Text(context.l10n.puzzlePuzzleDashboard), Text( context.l10n.puzzlePuzzleDashboardDescription, - style: Styles.subtitle.copyWith( - color: textShade(context, Styles.subtitleOpacity), - ), + style: Styles.subtitle.copyWith(color: textShade(context, Styles.subtitleOpacity)), ), ], ), @@ -89,14 +66,12 @@ class PuzzleDashboardWidget extends ConsumerWidget { cupertinoAdditionalDividerMargin: -14, children: [ Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : Styles.horizontalBodyPadding, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : Styles.horizontalBodyPadding, child: StatCardRow([ - StatCard( - context.l10n.performance, - value: dashboard.global.performance.toString(), - ), + StatCard(context.l10n.performance, value: dashboard.global.performance.toString()), StatCard( context.l10n .puzzleNbPlayed(dashboard.global.nb) @@ -107,8 +82,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { ), StatCard( context.l10n.puzzleSolved.capitalize(), - value: - '${((dashboard.global.firstWins / dashboard.global.nb) * 100).round()}%', + value: '${((dashboard.global.firstWins / dashboard.global.nb) * 100).round()}%', ), ]), ), @@ -116,10 +90,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { Padding( padding: const EdgeInsets.all(10.0), child: AspectRatio( - aspectRatio: - MediaQuery.sizeOf(context).width > FormFactor.desktop - ? 2.8 - : 1.2, + aspectRatio: MediaQuery.sizeOf(context).width > FormFactor.desktop ? 2.8 : 1.2, child: PuzzleChart(chartData), ), ), @@ -127,18 +98,13 @@ class PuzzleDashboardWidget extends ConsumerWidget { ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleDashboardWidget] could not load puzzle dashboard; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleDashboardWidget] could not load puzzle dashboard; $e\n$s'); return Padding( padding: Styles.bodySectionPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - context.l10n.puzzlePuzzleDashboard, - style: Styles.sectionTitle, - ), + Text(context.l10n.puzzlePuzzleDashboard, style: Styles.sectionTitle), if (e is ClientException && e.message.contains('404')) Text(context.l10n.puzzleNoPuzzlesToShow) else @@ -197,7 +163,7 @@ class PuzzleChart extends StatelessWidget { @override Widget build(BuildContext context) { - final radarColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.5); + final radarColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; return RadarChart( RadarChartData( @@ -207,16 +173,15 @@ class PuzzleChart extends StatelessWidget { radarShape: RadarShape.polygon, dataSets: [ RadarDataSet( - fillColor: chartColor.withOpacity(0.2), + fillColor: chartColor.withValues(alpha: 0.2), borderColor: chartColor, - dataEntries: puzzleData - .map((theme) => RadarEntry(value: theme.performance.toDouble())) - .toList(), + dataEntries: + puzzleData.map((theme) => RadarEntry(value: theme.performance.toDouble())).toList(), ), ], - getTitle: (index, angle) => RadarChartTitle( - text: puzzleThemeL10n(context, puzzleData[index].theme).name, - ), + getTitle: + (index, angle) => + RadarChartTitle(text: puzzleData[index].theme.l10n(context.l10n).name), titleTextStyle: const TextStyle(fontSize: 10), titlePositionPercentageOffset: 0.09, tickCount: 3, @@ -235,17 +200,18 @@ class DaysSelector extends ConsumerWidget { final day = ref.watch(daysProvider); return session != null ? AppBarTextButton( - onPressed: () => showChoicePicker( - context, - choices: Days.values, - selectedItem: day, - labelBuilder: (t) => Text(_daysL10n(context, t)), - onSelectedItemChanged: (newDay) { - ref.read(daysProvider.notifier).state = newDay; - }, - ), - child: Text(_daysL10n(context, day)), - ) + onPressed: + () => showChoicePicker( + context, + choices: Days.values, + selectedItem: day, + labelBuilder: (t) => Text(_daysL10n(context, t)), + onSelectedItemChanged: (newDay) { + ref.read(daysProvider.notifier).state = newDay; + }, + ), + child: Text(_daysL10n(context, day)), + ) : const SizedBox.shrink(); } } diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 81d9a3ae28..725614b11d 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -5,61 +5,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'puzzle_screen.dart'; -part 'opening_screen.g.dart'; - -@riverpod -Future<(bool, IMap, IList?)> _openings( - _OpeningsRef ref, -) async { - final connectivity = await ref.watch(connectivityChangesProvider.future); - final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); - IList? onlineOpenings; - try { - onlineOpenings = await ref.watch(puzzleOpeningsProvider.future); - } catch (e) { - onlineOpenings = null; - } - return (connectivity.isOnline, savedOpenings, onlineOpenings); -} +final _openingsProvider = + FutureProvider.autoDispose<(bool, IMap, IList?)>((ref) async { + final connectivity = await ref.watch(connectivityChangesProvider.future); + final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); + IList? onlineOpenings; + try { + onlineOpenings = await ref.watch(puzzleOpeningsProvider.future); + } catch (e) { + onlineOpenings = null; + } + return (connectivity.isOnline, savedOpenings, onlineOpenings); + }); class OpeningThemeScreen extends StatelessWidget { const OpeningThemeScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.puzzlePuzzlesByOpenings), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.puzzlePuzzlesByOpenings)), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzlePuzzlesByOpenings), - ), - child: const _Body(), - ); - } } class _Body extends ConsumerWidget { @@ -67,56 +44,50 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final titleStyle = Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - color: CupertinoTheme.of(context).textTheme.textStyle.color, - ) - : null; + final titleStyle = + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle(color: CupertinoTheme.of(context).textTheme.textStyle.color) + : null; final openings = ref.watch(_openingsProvider); - return SafeArea( - child: openings.when( - data: (data) { - final (isOnline, savedOpenings, onlineOpenings) = data; - if (isOnline && onlineOpenings != null) { - return ListView( - children: [ - for (final openingFamily in onlineOpenings) - _OpeningFamily( - openingFamily: openingFamily, - titleStyle: titleStyle, - ), - ], - ); - } else { - return ListSection( - children: [ - for (final openingKey in savedOpenings.keys) - _OpeningTile( - name: openingKey.replaceAll('_', ' '), - openingKey: openingKey, - count: savedOpenings[openingKey]!, - titleStyle: titleStyle, - ), - ], - ); - } - }, - error: (error, stack) { - return const Center(child: Text('Could not load openings.')); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - ), + return openings.when( + data: (data) { + final (isOnline, savedOpenings, onlineOpenings) = data; + if (isOnline && onlineOpenings != null) { + return ListView( + children: [ + for (final openingFamily in onlineOpenings) + _OpeningFamily(openingFamily: openingFamily, titleStyle: titleStyle), + ], + ); + } else { + return ListView( + children: [ + ListSection( + children: [ + for (final openingKey in savedOpenings.keys) + _OpeningTile( + name: openingKey.replaceAll('_', ' '), + openingKey: openingKey, + count: savedOpenings[openingKey]!, + titleStyle: titleStyle, + ), + ], + ), + ], + ); + } + }, + error: (error, stack) { + return const Center(child: Text('Could not load openings.')); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), ); } } class _OpeningFamily extends ConsumerWidget { - const _OpeningFamily({ - required this.openingFamily, - required this.titleStyle, - }); + const _OpeningFamily({required this.openingFamily, required this.titleStyle}); final PuzzleOpeningFamily openingFamily; final TextStyle? titleStyle; @@ -125,62 +96,49 @@ class _OpeningFamily extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), - child: openingFamily.openings.isNotEmpty - ? ExpansionTile( - title: Text( - openingFamily.name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - subtitle: Text( - '${openingFamily.count}', - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), + child: + openingFamily.openings.isNotEmpty + ? ExpansionTile( + title: Text(openingFamily.name, overflow: TextOverflow.ellipsis, style: titleStyle), + subtitle: Text( + '${openingFamily.count}', + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), ), - ), - children: [ - ListSection( - children: [ - _OpeningTile( - name: openingFamily.name, - openingKey: openingFamily.key, - count: openingFamily.count, - titleStyle: titleStyle, - ), - ...openingFamily.openings.map( - (opening) => _OpeningTile( - name: opening.name, - openingKey: opening.key, - count: opening.count, + children: [ + ListSection( + children: [ + _OpeningTile( + name: openingFamily.name, + openingKey: openingFamily.key, + count: openingFamily.count, titleStyle: titleStyle, ), - ), - ], - ), - ], - ) - : ListTile( - title: Text( - openingFamily.name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - subtitle: Text( - '${openingFamily.count}', - style: TextStyle( - color: textShade(context, 0.5), + ...openingFamily.openings.map( + (opening) => _OpeningTile( + name: opening.name, + openingKey: opening.key, + count: opening.count, + titleStyle: titleStyle, + ), + ), + ], + ), + ], + ) + : ListTile( + title: Text(openingFamily.name, overflow: TextOverflow.ellipsis, style: titleStyle), + subtitle: Text( + '${openingFamily.count}', + style: TextStyle(color: textShade(context, 0.5)), ), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: PuzzleOpening(openingFamily.key)), + ); + }, ), - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleOpening(openingFamily.key), - ), - ); - }, - ), ); } } @@ -201,27 +159,14 @@ class _OpeningTile extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - leading: Theme.of(context).platform == TargetPlatform.iOS - ? null - : const SizedBox.shrink(), - title: Text( - name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - trailing: Text( - '$count', - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ), + leading: Theme.of(context).platform == TargetPlatform.iOS ? null : const SizedBox.shrink(), + title: Text(name, overflow: TextOverflow.ellipsis, style: titleStyle), + trailing: Text('$count', style: TextStyle(color: textShade(context, Styles.subtitleOpacity))), onTap: () { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleOpening(openingKey), - ), + builder: (context) => PuzzleScreen(angle: PuzzleOpening(openingKey)), ); }, ); diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 87576cb802..02729e5ea0 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,11 +11,7 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; class PuzzleFeedbackWidget extends ConsumerWidget { - const PuzzleFeedbackWidget({ - required this.puzzle, - required this.state, - required this.onStreak, - }); + const PuzzleFeedbackWidget({required this.puzzle, required this.state, required this.onStreak}); final Puzzle puzzle; final PuzzleState state; @@ -24,65 +19,63 @@ class PuzzleFeedbackWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pieceSet = - ref.watch(boardPreferencesProvider.select((value) => value.pieceSet)); - final boardTheme = - ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); + final pieceSet = ref.watch(boardPreferencesProvider.select((value) => value.pieceSet)); + final boardTheme = ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final brightness = ref.watch(currentBrightnessProvider); - final piece = state.pov == Side.white - ? cg.PieceKind.whiteKing - : cg.PieceKind.blackKing; + final piece = state.pov == Side.white ? PieceKind.whiteKing : PieceKind.blackKing; final asset = pieceSet.assets[piece]!; switch (state.mode) { case PuzzleMode.view: - final puzzleRating = - context.l10n.puzzleRatingX(puzzle.puzzle.rating.toString()); - final playedXTimes = context.l10n - .puzzlePlayedXTimes(puzzle.puzzle.plays) - .localizeNumbers(); + final puzzleRating = context.l10n.puzzleRatingX(puzzle.puzzle.rating.toString()); + final playedXTimes = context.l10n.puzzlePlayedXTimes(puzzle.puzzle.plays).localizeNumbers(); return _FeedbackTile( - leading: state.result == PuzzleResult.win - ? Icon(Icons.check, size: 36, color: context.lichessColors.good) - : null, - title: onStreak && state.result == PuzzleResult.lose - ? const Text( - 'GAME OVER', - style: TextStyle( - fontSize: 24, - letterSpacing: 2.0, + leading: + state.result == PuzzleResult.win + ? Icon(Icons.check, size: 36, color: context.lichessColors.good) + : null, + title: + onStreak && state.result == PuzzleResult.lose + ? const Text( + 'GAME OVER', + style: TextStyle(fontSize: 24, letterSpacing: 2.0), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ) + : Text( + state.result == PuzzleResult.win + ? context.l10n.puzzlePuzzleSuccess + : context.l10n.puzzlePuzzleComplete, + overflow: TextOverflow.ellipsis, + ), + subtitle: + onStreak && state.result == PuzzleResult.lose + ? null + : RatingPrefAware( + orElse: Text('$playedXTimes.', overflow: TextOverflow.ellipsis, maxLines: 2), + child: Text( + '$puzzleRating. $playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ), - textAlign: TextAlign.center, - ) - : Text( - state.result == PuzzleResult.win - ? context.l10n.puzzlePuzzleSuccess - : context.l10n.puzzlePuzzleComplete, - ), - subtitle: onStreak && state.result == PuzzleResult.lose - ? null - : RatingPrefAware( - orElse: Text('$playedXTimes.'), - child: Text('$puzzleRating. $playedXTimes.'), - ), ); case PuzzleMode.load: case PuzzleMode.play: if (state.feedback == PuzzleFeedback.bad) { return _FeedbackTile( - leading: Icon( - Icons.close, - size: 36, - color: context.lichessColors.error, + leading: Icon(Icons.close, size: 36, color: context.lichessColors.error), + title: Text(context.l10n.puzzleNotTheMove, overflow: TextOverflow.ellipsis), + subtitle: Text( + context.l10n.puzzleTrySomethingElse, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), - title: Text(context.l10n.puzzleNotTheMove), - subtitle: Text(context.l10n.puzzleTrySomethingElse), ); } else if (state.feedback == PuzzleFeedback.good) { return _FeedbackTile( - leading: - Icon(Icons.check, size: 36, color: context.lichessColors.good), + leading: Icon(Icons.check, size: 36, color: context.lichessColors.good), title: Text(context.l10n.puzzleBestMove), subtitle: Text(context.l10n.puzzleKeepGoing), ); @@ -91,9 +84,10 @@ class PuzzleFeedbackWidget extends ConsumerWidget { leading: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), - color: brightness == Brightness.light - ? boardTheme.colors.lightSquare - : boardTheme.colors.darkSquare, + color: + brightness == Brightness.light + ? boardTheme.colors.lightSquare + : boardTheme.colors.darkSquare, ), child: Padding( padding: const EdgeInsets.all(2.0), @@ -106,11 +100,13 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ), ), ), - title: Text(context.l10n.yourTurn), + title: Text(context.l10n.yourTurn, overflow: TextOverflow.ellipsis), subtitle: Text( state.pov == Side.white ? context.l10n.puzzleFindTheBestMoveForWhite : context.l10n.puzzleFindTheBestMoveForBlack, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ); } @@ -119,11 +115,7 @@ class PuzzleFeedbackWidget extends ConsumerWidget { } class _FeedbackTile extends StatelessWidget { - const _FeedbackTile({ - this.leading, - required this.title, - this.subtitle, - }); + const _FeedbackTile({this.leading, required this.title, this.subtitle}); final Widget? leading; final Widget title; @@ -135,25 +127,23 @@ class _FeedbackTile extends StatelessWidget { return Row( children: [ - if (leading != null) ...[ - leading!, - const SizedBox(width: 18), - ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - DefaultTextStyle.merge( - style: TextStyle( - fontSize: - defaultFontSize != null ? defaultFontSize * 1.2 : null, - fontWeight: FontWeight.bold, + if (leading != null) ...[leading!, const SizedBox(width: 16.0)], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultTextStyle.merge( + style: TextStyle( + fontSize: defaultFontSize != null ? defaultFontSize * 1.2 : null, + fontWeight: FontWeight.bold, + ), + child: title, ), - child: title, - ), - if (subtitle != null) subtitle!, - ], + if (subtitle != null) subtitle!, + ], + ), ), ], ); diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 23ba4d699c..4fcf7eb8cd 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_layout_grid/flutter_layout_grid.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,41 +9,16 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:timeago/timeago.dart' as timeago; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); - -class PuzzleHistoryScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzleHistory), - ), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.puzzleHistory)), - body: _Body(), - ); - } -} +final _dateFormatter = DateFormat.yMMMd(); /// Shows a short preview of the puzzle history. class PuzzleHistoryPreview extends ConsumerWidget { @@ -58,8 +32,7 @@ class PuzzleHistoryPreview extends ConsumerWidget { return _PreviewBoardsGrid( rowGap: 16, builder: (crossAxisCount, boardWidth) { - final cappedHistory = - maxRows != null ? history.take(crossAxisCount * maxRows!) : history; + final cappedHistory = maxRows != null ? history.take(crossAxisCount * maxRows!) : history; return cappedHistory.map((e) { final (fen, side, lastMove) = e.preview; @@ -69,22 +42,17 @@ class PuzzleHistoryPreview extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: e.id, - ), + builder: + (_) => + PuzzleScreen(angle: const PuzzleTheme(PuzzleThemeKey.mix), puzzleId: e.id), ); }, - orientation: side.cg, + orientation: side, fen: fen, - lastMove: lastMove.cg, + lastMove: lastMove, footer: Padding( padding: const EdgeInsets.only(top: 2.0), - child: Row( - children: [ - _PuzzleResult(e), - ], - ), + child: Row(children: [_PuzzleResult(e)]), ), ); }).toList(); @@ -93,6 +61,19 @@ class PuzzleHistoryPreview extends ConsumerWidget { } } +/// A screen that displays the full puzzle history. +class PuzzleHistoryScreen extends StatelessWidget { + const PuzzleHistoryScreen(); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.puzzleHistory)), + body: _Body(), + ); + } +} + class _Body extends ConsumerStatefulWidget { @override ConsumerState<_Body> createState() => _BodyState(); @@ -102,8 +83,6 @@ const _kPuzzlePadding = 10.0; class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); - bool _hasMore = true; - bool _isLoading = false; @override void initState() { @@ -119,9 +98,9 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { - if (_hasMore && !_isLoading) { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { + final currentState = ref.read(puzzleActivityProvider).valueOrNull; + if (currentState != null && !currentState.isLoading) { ref.read(puzzleActivityProvider.notifier).getNext(); } } @@ -133,20 +112,12 @@ class _BodyState extends ConsumerState<_Body> { return historyState.when( data: (state) { - _hasMore = state.hasMore; - _isLoading = state.isLoading; if (state.hasError) { - showPlatformSnackbar( - context, - 'Error loading history', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Error loading history', type: SnackBarType.error); } - final crossAxisCount = - MediaQuery.sizeOf(context).width > FormFactor.tablet ? 4 : 2; + final crossAxisCount = MediaQuery.sizeOf(context).width > FormFactor.tablet ? 4 : 2; final columnsGap = _kPuzzlePadding * crossAxisCount + _kPuzzlePadding; - final boardWidth = - (MediaQuery.sizeOf(context).width - columnsGap) / crossAxisCount; + final boardWidth = (MediaQuery.sizeOf(context).width - columnsGap) / crossAxisCount; // List prepared for the ListView.builder. // It includes the date headers, and puzzles are sliced into rows of `crossAxisCount` length. @@ -174,27 +145,22 @@ class _BodyState extends ConsumerState<_Body> { return Padding( padding: const EdgeInsets.only(right: _kPuzzlePadding), child: Row( - children: element - .map( - (e) => - _HistoryBoard(e as PuzzleHistoryEntry, boardWidth), - ) - .toList(), + children: + element + .map((e) => PuzzleHistoryBoard(e as PuzzleHistoryEntry, boardWidth)) + .toList(), ), ); } else if (element is DateTime) { - final title = DateTime.now().difference(element).inDays >= 15 - ? _dateFormatter.format(element) - : timeago.format(element); + final title = + DateTime.now().difference(element).inDays >= 15 + ? _dateFormatter.format(element) + : relativeDate(context.l10n, element); return Padding( - padding: const EdgeInsets.only(left: _kPuzzlePadding) - .add(Styles.sectionTopPadding), + padding: const EdgeInsets.only(left: _kPuzzlePadding).add(Styles.sectionTopPadding), child: Text( title, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18), ), ); } else { @@ -204,9 +170,7 @@ class _BodyState extends ConsumerState<_Body> { ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleHistoryScreen] could not load puzzle history', - ); + debugPrint('SEVERE: [PuzzleHistoryScreen] could not load puzzle history'); return const Center(child: Text('Could not load Puzzle History')); }, loading: () => const CenterLoadingIndicator(), @@ -214,8 +178,8 @@ class _BodyState extends ConsumerState<_Body> { } } -class _HistoryBoard extends ConsumerWidget { - const _HistoryBoard(this.puzzle, this.boardWidth); +class PuzzleHistoryBoard extends ConsumerWidget { + const PuzzleHistoryBoard(this.puzzle, this.boardWidth); final PuzzleHistoryEntry puzzle; final double boardWidth; @@ -235,19 +199,15 @@ class _HistoryBoard extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (ctx) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.id, - ), + builder: + (ctx) => + PuzzleScreen(angle: const PuzzleTheme(PuzzleThemeKey.mix), puzzleId: puzzle.id), ); }, - orientation: turn.cg, + orientation: turn, fen: fen, - lastMove: lastMove.cg, - footer: Padding( - padding: const EdgeInsets.only(top: 2), - child: _PuzzleResult(puzzle), - ), + lastMove: lastMove, + footer: Padding(padding: const EdgeInsets.only(top: 2), child: _PuzzleResult(puzzle)), ), ); } @@ -261,14 +221,12 @@ class _PuzzleResult extends StatelessWidget { @override Widget build(BuildContext context) { return ColoredBox( - color: entry.win - ? context.lichessColors.good.withOpacity(0.7) - : context.lichessColors.error.withOpacity(0.7), + color: + entry.win + ? context.lichessColors.good.withValues(alpha: 0.7) + : context.lichessColors.error.withValues(alpha: 0.7), child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 1, - horizontal: 3, - ), + padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 3), child: Row( children: [ Text( @@ -288,24 +246,13 @@ class _PuzzleResult extends StatelessWidget { Text( '${entry.solvingTime!.inSeconds}s', overflow: TextOverflow.fade, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - height: 1.0, - ), + style: const TextStyle(color: Colors.white, fontSize: 10, height: 1.0), ) else Text( - (entry.win - ? context.l10n.puzzleSolved - : context.l10n.puzzleFailed) - .toUpperCase(), + (entry.win ? context.l10n.puzzleSolved : context.l10n.puzzleFailed).toUpperCase(), overflow: TextOverflow.fade, - style: const TextStyle( - fontSize: 10, - color: Colors.white, - height: 1.0, - ), + style: const TextStyle(fontSize: 10, color: Colors.white, height: 1.0), ), ], ), @@ -318,32 +265,26 @@ class _PreviewBoardsGrid extends StatelessWidget { final List Function(int, double) builder; final double rowGap; - const _PreviewBoardsGrid({ - required this.builder, - required this.rowGap, - }); + const _PreviewBoardsGrid({required this.builder, required this.rowGap}); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final crossAxisCount = constraints.maxWidth > 600 - ? 4 - : constraints.maxWidth > 450 + final crossAxisCount = + constraints.maxWidth > 600 + ? 4 + : constraints.maxWidth > 450 ? 3 : 2; const columnGap = 12.0; final boardWidth = - (constraints.maxWidth - (columnGap * crossAxisCount - columnGap)) / - crossAxisCount; + (constraints.maxWidth - (columnGap * crossAxisCount - columnGap)) / crossAxisCount; final boards = builder(crossAxisCount, boardWidth); return LayoutGrid( columnSizes: List.generate(crossAxisCount, (_) => 1.fr), - rowSizes: List.generate( - (boards.length / crossAxisCount).ceil(), - (_) => auto, - ), + rowSizes: List.generate((boards.length / crossAxisCount).ceil(), (_) => auto), rowGap: rowGap, columnGap: columnGap, children: boards, diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index d845ad745f..f53a633433 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; @@ -22,26 +21,26 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_settings_screen.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'puzzle_feedback_widget.dart'; import 'puzzle_session_widget.dart'; @@ -50,11 +49,7 @@ class PuzzleScreen extends ConsumerStatefulWidget { /// Creates a new puzzle screen. /// /// If [puzzleId] is provided, the screen will load the puzzle with that id. Otherwise, it will load the next puzzle from the queue. - const PuzzleScreen({ - required this.angle, - this.puzzleId, - super.key, - }); + const PuzzleScreen({required this.angle, this.puzzleId, super.key}); final PuzzleAngle angle; final PuzzleId? puzzleId; @@ -84,72 +79,40 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { super.didPop(); if (mounted) { ref.invalidate(nextPuzzleProvider(widget.angle)); - ref.invalidate(puzzleRecentActivityProvider); } } @override Widget build(BuildContext context) { return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: const [ - _PuzzleSettingsButton(), - ], - title: _Title(angle: widget.angle), - ), - body: widget.puzzleId != null - ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) - : _LoadNextPuzzle(angle: widget.angle), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(angle: widget.angle), - trailing: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - _PuzzleSettingsButton(), - ], + child: PlatformScaffold( + appBar: PlatformAppBar( + actions: const [ToggleSoundButton(), _PuzzleSettingsButton()], + title: _Title(angle: widget.angle), ), + body: + widget.puzzleId != null + ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) + : _LoadNextPuzzle(angle: widget.angle), ), - child: widget.puzzleId != null - ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) - : _LoadNextPuzzle(angle: widget.angle), ); } } class _Title extends ConsumerWidget { - const _Title({ - required this.angle, - }); + const _Title({required this.angle}); final PuzzleAngle angle; @override Widget build(BuildContext context, WidgetRef ref) { return switch (angle) { - PuzzleTheme(themeKey: final key) => key == PuzzleThemeKey.mix - ? Text(context.l10n.puzzleDesc) - : Text(puzzleThemeL10n(context, key).name), + PuzzleTheme(themeKey: final key) => + key == PuzzleThemeKey.mix + ? Text(context.l10n.puzzleDesc) + : Text(key.l10n(context.l10n).name), PuzzleOpening(key: final key) => ref - .watch( - puzzleOpeningNameProvider(key), - ) + .watch(puzzleOpeningNameProvider(key)) .when( data: (data) => Text(data), loading: () => const SizedBox.shrink(), @@ -173,38 +136,20 @@ class _LoadNextPuzzle extends ConsumerWidget { if (data == null) { return const Center( child: BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, - boardData: cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + fen: kEmptyFen, + orientation: Side.white, errorMessage: 'No more puzzles. Go online to get more.', ), ); } else { - return _Body( - initialPuzzleContext: data, - ); + return _Body(initialPuzzleContext: data); } }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s'); return Center( - child: BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), - errorMessage: e.toString(), - ), + child: BoardTable(fen: kEmptyFen, orientation: Side.white, errorMessage: e.toString()), ); }, ); @@ -232,42 +177,28 @@ class _LoadPuzzleFromId extends ConsumerWidget { ), ); }, - loading: () => const Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: BoardTable( - boardData: cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, + loading: + () => const Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: BoardTable.empty(showEngineGaugePlaceholder: true), ), - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, ), - ), + BottomBar.empty(), + ], ), - SizedBox(height: kBottomBarHeight), - ], - ), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s'); return Column( children: [ Expanded( child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ), @@ -281,9 +212,7 @@ class _LoadPuzzleFromId extends ConsumerWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.initialPuzzleContext, - }); + const _Body({required this.initialPuzzleContext}); final PuzzleContext initialPuzzleContext; @@ -294,11 +223,8 @@ class _Body extends ConsumerWidget { final boardPreferences = ref.watch(boardPreferencesProvider); - final currentEvalBest = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMove), - ); - final evalBestMove = - (currentEvalBest ?? puzzleState.node.eval?.bestMove)?.cg; + final currentEvalBest = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMove)); + final evalBestMove = (currentEvalBest ?? puzzleState.node.eval?.bestMove) as NormalMove?; return Column( children: [ @@ -306,45 +232,48 @@ class _Body extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!); - }, - boardData: cg.BoardData( - orientation: puzzleState.pov.cg, - interactableSide: puzzleState.mode == PuzzleMode.load || - puzzleState.position.isGameOver - ? cg.InteractableSide.none - : puzzleState.mode == PuzzleMode.view - ? cg.InteractableSide.both + orientation: puzzleState.pov, + fen: puzzleState.fen, + lastMove: puzzleState.lastMove as NormalMove?, + gameData: GameData( + playerSide: + puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver + ? PlayerSide.none + : puzzleState.mode == PuzzleMode.view + ? PlayerSide.both : puzzleState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: puzzleState.fen, - isCheck: boardPreferences.boardHighlights && - puzzleState.position.isCheck, - lastMove: puzzleState.lastMove?.cg, - sideToMove: puzzleState.position.turn.cg, + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPreferences.boardHighlights && puzzleState.position.isCheck, + sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, - shapes: puzzleState.isEngineEnabled && evalBestMove != null - ? ISet([ - cg.Arrow( + promotionMove: puzzleState.promotionMove, + onMove: (move, {isDrop}) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, + onPromotionSelection: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelection(role); + }, + ), + shapes: + puzzleState.isEngineEnabled && evalBestMove != null + ? ISet([ + Arrow( color: const Color(0x40003088), orig: evalBestMove.from, dest: evalBestMove.to, ), ]) - : null, - ), - engineGauge: puzzleState.isEngineEnabled - ? EngineGaugeParams( - orientation: puzzleState.pov, - isLocalEngineAvailable: true, - position: puzzleState.position, - savedEval: puzzleState.node.eval, - ) - : null, + : null, + engineGauge: + puzzleState.isEngineEnabled + ? ( + orientation: puzzleState.pov, + isLocalEngineAvailable: true, + position: puzzleState.position, + savedEval: puzzleState.node.eval, + ) + : null, showEngineGaugePlaceholder: true, topTable: Center( child: PuzzleFeedbackWidget( @@ -359,9 +288,7 @@ class _Body extends ConsumerWidget { if (puzzleState.glicko != null) RatingPrefAware( child: Padding( - padding: const EdgeInsets.only( - top: 10.0, - ), + padding: const EdgeInsets.only(top: 10.0), child: Row( children: [ Text(context.l10n.rating), @@ -369,7 +296,8 @@ class _Body extends ConsumerWidget { TweenAnimationBuilder( tween: Tween( begin: puzzleState.glicko!.rating, - end: puzzleState.nextContext?.glicko?.rating ?? + end: + puzzleState.nextContext?.glicko?.rating ?? puzzleState.glicko!.rating, ), duration: const Duration(milliseconds: 500), @@ -396,20 +324,14 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - initialPuzzleContext: initialPuzzleContext, - ctrlProvider: ctrlProvider, - ), + _BottomBar(initialPuzzleContext: initialPuzzleContext, ctrlProvider: ctrlProvider), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const _BottomBar({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @@ -425,105 +347,77 @@ class _BottomBar extends ConsumerWidget { final puzzleState = ref.watch(ctrlProvider); final isDailyPuzzle = puzzleState.puzzle.isDailyPuzzle == true; - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (initialPuzzleContext.userId != null && - !isDailyPuzzle && - puzzleState.mode != PuzzleMode.view) - _DifficultySelector( - initialPuzzleContext: initialPuzzleContext, - ctrlProvider: ctrlProvider, - ), - if (puzzleState.mode != PuzzleMode.view) - BottomBarButton( - icon: Icons.help, - label: context.l10n.viewTheSolution, - showLabel: true, - onTap: puzzleState.canViewSolution - ? () => ref.read(ctrlProvider.notifier).viewSolution() - : null, - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showPuzzleMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - onTap: () { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - }, - label: context.l10n.toggleLocalEvaluation, - icon: CupertinoIcons.gauge, - highlighted: puzzleState.isLocalEvalEnabled, - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: RepeatButton( - triggerDelays: _repeatTriggerDelays, - onLongPress: - puzzleState.canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - onTap: puzzleState.canGoBack - ? () => _moveBackward(ref) - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: RepeatButton( - triggerDelays: _repeatTriggerDelays, - onLongPress: - puzzleState.canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - onTap: puzzleState.canGoNext - ? () => _moveForward(ref) - : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, - blink: puzzleState.viewedSolutionRecently, - ), - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - onTap: puzzleState.mode == PuzzleMode.view && - puzzleState.nextContext != null - ? () => ref - .read(ctrlProvider.notifier) - .loadPuzzle(puzzleState.nextContext!) - : null, - highlighted: true, - label: context.l10n.puzzleContinueTraining, - icon: CupertinoIcons.play_arrow_solid, - ), - ), - ], + return BottomBar( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (initialPuzzleContext.userId != null && + !isDailyPuzzle && + puzzleState.mode != PuzzleMode.view) + _DifficultySelector( + initialPuzzleContext: initialPuzzleContext, + ctrlProvider: ctrlProvider, ), - ), - ), + if (puzzleState.mode != PuzzleMode.view) + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: + puzzleState.canViewSolution + ? () => ref.read(ctrlProvider.notifier).viewSolution() + : null, + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showPuzzleMenu(context, ref); + }, + icon: Icons.menu, + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + onTap: () { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + label: context.l10n.toggleLocalEvaluation, + icon: CupertinoIcons.gauge, + highlighted: puzzleState.isLocalEvalEnabled, + ), + if (puzzleState.mode == PuzzleMode.view) + RepeatButton( + triggerDelays: _repeatTriggerDelays, + onLongPress: puzzleState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: puzzleState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + if (puzzleState.mode == PuzzleMode.view) + RepeatButton( + triggerDelays: _repeatTriggerDelays, + onLongPress: puzzleState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + onTap: puzzleState.canGoNext ? () => _moveForward(ref) : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, + blink: puzzleState.viewedSolutionRecently, + ), + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + onTap: + puzzleState.mode == PuzzleMode.view && puzzleState.nextContext != null + ? () => ref.read(ctrlProvider.notifier).onLoadPuzzle(puzzleState.nextContext!) + : null, + highlighted: true, + label: context.l10n.puzzleContinueTraining, + icon: CupertinoIcons.play_arrow_solid, + ), + ], ); } @@ -537,8 +431,7 @@ class _BottomBar extends ConsumerWidget { onPressed: (context) { launchShareDialog( context, - text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}') - .toString(), + text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}').toString(), ); }, ), @@ -547,24 +440,24 @@ class _BottomBar extends ConsumerWidget { onPressed: (context) { pushPlatformRoute( context, - builder: (context) => AnalysisScreen( - title: context.l10n.analysis, - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, - initialMoveCursor: 0, - ), - ), + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: puzzleState.pov, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isComputerAnalysisAllowed: true, + variant: Variant.standard, + ), + initialMoveCursor: 0, + ), + ), ); }, ), BottomSheetAction( - makeLabel: (context) => Text( - context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value), - ), + makeLabel: + (context) => Text(context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value)), onPressed: (_) async { final game = await ref.read( archivedGameProvider(id: puzzleState.puzzle.game.id).future, @@ -572,11 +465,12 @@ class _BottomBar extends ConsumerWidget { if (context.mounted) { pushPlatformRoute( context, - builder: (context) => ArchivedGameScreen( - gameData: game.data, - orientation: puzzleState.pov, - initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, - ), + builder: + (context) => ArchivedGameScreen( + gameData: game.data, + orientation: puzzleState.pov, + initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, + ), ); } }, @@ -595,66 +489,57 @@ class _BottomBar extends ConsumerWidget { } class _DifficultySelector extends ConsumerWidget { - const _DifficultySelector({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const _DifficultySelector({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { - final difficulty = ref.watch( - puzzlePreferencesProvider(initialPuzzleContext.userId) - .select((state) => state.difficulty), - ); + final difficulty = ref.watch(puzzlePreferencesProvider.select((state) => state.difficulty)); final state = ref.watch(ctrlProvider); final connectivity = ref.watch(connectivityChangesProvider); return connectivity.when( - data: (data) => StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - PuzzleDifficulty selectedDifficulty = difficulty; - return BottomBarButton( - icon: Icons.tune, - label: puzzleDifficultyL10n(context, difficulty), - tooltip: context.l10n.puzzleDifficultyLevel, - showLabel: true, - onTap: !data.isOnline || state.isChangingDifficulty - ? null - : () { - showChoicePicker( - context, - choices: PuzzleDifficulty.values, - selectedItem: difficulty, - labelBuilder: (t) => - Text(puzzleDifficultyL10n(context, t)), - onSelectedItemChanged: (PuzzleDifficulty? d) { - if (d != null) { - setState(() { - selectedDifficulty = d; + data: + (data) => StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + PuzzleDifficulty selectedDifficulty = difficulty; + return BottomBarButton( + icon: Icons.tune, + label: puzzleDifficultyL10n(context, difficulty), + tooltip: context.l10n.puzzleDifficultyLevel, + showLabel: true, + onTap: + !data.isOnline || state.isChangingDifficulty + ? null + : () { + showChoicePicker( + context, + choices: PuzzleDifficulty.values, + selectedItem: difficulty, + labelBuilder: (t) => Text(puzzleDifficultyL10n(context, t)), + onSelectedItemChanged: (PuzzleDifficulty? d) { + if (d != null) { + setState(() { + selectedDifficulty = d; + }); + } + }, + ).then((_) async { + if (selectedDifficulty == difficulty) { + return; + } + final nextContext = await ref + .read(ctrlProvider.notifier) + .changeDifficulty(selectedDifficulty); + if (context.mounted && nextContext != null) { + ref.read(ctrlProvider.notifier).onLoadPuzzle(nextContext); + } }); - } - }, - ).then( - (_) async { - if (selectedDifficulty == difficulty) { - return; - } - final nextContext = await ref - .read(ctrlProvider.notifier) - .changeDifficulty(selectedDifficulty); - if (context.mounted && nextContext != null) { - ref - .read(ctrlProvider.notifier) - .loadPuzzle(nextContext); - } - }, - ); - }, - ); - }, - ), + }, + ); + }, + ), loading: () => const ButtonLoadingIndicator(), error: (_, __) => const SizedBox.shrink(), ); @@ -667,13 +552,15 @@ class _PuzzleSettingsButton extends StatelessWidget { @override Widget build(BuildContext context) { return AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => const PuzzleSettingsScreen(), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + builder: (_) => const PuzzleSettingsScreen(), + ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ); diff --git a/lib/src/view/puzzle/puzzle_session_widget.dart b/lib/src/view/puzzle/puzzle_session_widget.dart index a6f01e57b2..de00ea2404 100644 --- a/lib/src/view/puzzle/puzzle_session_widget.dart +++ b/lib/src/view/puzzle/puzzle_session_widget.dart @@ -14,17 +14,13 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; class PuzzleSessionWidget extends ConsumerStatefulWidget { - const PuzzleSessionWidget({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const PuzzleSessionWidget({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @override - ConsumerState createState() => - PuzzleSessionWidgetState(); + ConsumerState createState() => PuzzleSessionWidgetState(); } class PuzzleSessionWidgetState extends ConsumerState { @@ -36,9 +32,7 @@ class PuzzleSessionWidgetState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (lastAttemptKey.currentContext != null) { - Scrollable.ensureVisible( - lastAttemptKey.currentContext!, - ); + Scrollable.ensureVisible(lastAttemptKey.currentContext!); } }); } @@ -60,10 +54,7 @@ class PuzzleSessionWidgetState extends ConsumerState { @override Widget build(BuildContext context) { final session = ref.watch( - puzzleSessionProvider( - widget.initialPuzzleContext.userId, - widget.initialPuzzleContext.angle, - ), + puzzleSessionProvider(widget.initialPuzzleContext.userId, widget.initialPuzzleContext.angle), ); final puzzleState = ref.watch(widget.ctrlProvider); final brightness = ref.watch(currentBrightnessProvider); @@ -78,13 +69,13 @@ class PuzzleSessionWidgetState extends ConsumerState { final remainingSpace = estimateRemainingHeightLeftBoard(context); final estimatedTableHeight = remainingSpace / 2; const estimatedRatingWidgetHeight = 33.0; - final estimatedWidgetHeight = - estimatedTableHeight - estimatedRatingWidgetHeight; - final maxHeight = orientation == Orientation.portrait - ? estimatedWidgetHeight >= 60 - ? 60.0 - : 26.0 - : 60.0; + final estimatedWidgetHeight = estimatedTableHeight - estimatedRatingWidgetHeight; + final maxHeight = + orientation == Orientation.portrait + ? estimatedWidgetHeight >= 60 + ? 60.0 + : 26.0 + : 60.0; return ConstrainedBox( constraints: BoxConstraints(minHeight: 26.0, maxHeight: maxHeight), @@ -101,36 +92,33 @@ class PuzzleSessionWidgetState extends ConsumerState { isLoading: loadingPuzzleId == attempt.id, brightness: brightness, attempt: attempt, - onTap: puzzleState.puzzle.puzzle.id != attempt.id && - loadingPuzzleId == null - ? (id) async { - final provider = puzzleProvider(id); - setState(() { - loadingPuzzleId = id; - }); - try { - final puzzle = await ref.read(provider.future); - final nextContext = PuzzleContext( - userId: widget.initialPuzzleContext.userId, - angle: widget.initialPuzzleContext.angle, - puzzle: puzzle, - ); - - ref - .read(widget.ctrlProvider.notifier) - .loadPuzzle(nextContext); - } finally { - if (mounted) { - setState(() { - loadingPuzzleId = null; - }); + onTap: + puzzleState.puzzle.puzzle.id != attempt.id && loadingPuzzleId == null + ? (id) async { + final provider = puzzleProvider(id); + setState(() { + loadingPuzzleId = id; + }); + try { + final puzzle = await ref.read(provider.future); + final nextContext = PuzzleContext( + userId: widget.initialPuzzleContext.userId, + angle: widget.initialPuzzleContext.angle, + puzzle: puzzle, + ); + + ref.read(widget.ctrlProvider.notifier).onLoadPuzzle(nextContext); + } finally { + if (mounted) { + setState(() { + loadingPuzzleId = null; + }); + } } } - } - : null, + : null, ), - if (puzzleState.mode == PuzzleMode.view || - currentAttempt == null) + if (puzzleState.mode == PuzzleMode.view || currentAttempt == null) _SessionItem( isCurrent: currentAttempt == null, isLoading: false, @@ -163,19 +151,21 @@ class _SessionItem extends StatelessWidget { final Brightness brightness; final void Function(PuzzleId id)? onTap; - Color get good => brightness == Brightness.light - ? LichessColors.good.shade300 - : defaultTargetPlatform == TargetPlatform.iOS + Color get good => + brightness == Brightness.light + ? LichessColors.good.shade300 + : defaultTargetPlatform == TargetPlatform.iOS ? LichessColors.good.shade600 : LichessColors.good.shade400; - Color get error => brightness == Brightness.light - ? LichessColors.error.shade300 - : defaultTargetPlatform == TargetPlatform.iOS + Color get error => + brightness == Brightness.light + ? LichessColors.error.shade300 + : defaultTargetPlatform == TargetPlatform.iOS ? LichessColors.error.shade600 : LichessColors.error.shade400; - Color get next => Colors.grey.withOpacity(0.5); + Color get next => Colors.grey.withValues(alpha: 0.5); @override Widget build(BuildContext context) { @@ -188,55 +178,28 @@ class _SessionItem extends StatelessWidget { height: 26, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: isCurrent - ? Colors.grey - : attempt != null + color: + isCurrent + ? Colors.grey + : attempt != null ? attempt!.win ? good.harmonizeWith(colorScheme.primary) : error.harmonizeWith(colorScheme.primary) : next, borderRadius: const BorderRadius.all(Radius.circular(5)), ), - child: isLoading - ? const Padding( - padding: EdgeInsets.all(2.0), - child: FittedBox( - fit: BoxFit.cover, - child: CircularProgressIndicator.adaptive( - backgroundColor: Colors.white, + child: + isLoading + ? const Padding( + padding: EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.cover, + child: CircularProgressIndicator.adaptive(backgroundColor: Colors.white), ), - ), - ) - : attempt?.ratingDiff != null && attempt!.ratingDiff != 0 + ) + : attempt?.ratingDiff != null && attempt!.ratingDiff != 0 ? RatingPrefAware( - orElse: Icon( - attempt != null - ? attempt!.win - ? Icons.check - : Icons.close - : null, - color: Colors.white, - size: 18, - ), - child: Padding( - padding: const EdgeInsets.all(2.0), - child: FittedBox( - fit: BoxFit.cover, - child: Text( - attempt!.ratingDiffString!, - maxLines: 1, - style: const TextStyle( - color: Colors.white, - height: 1, - fontFeatures: [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ) - : Icon( + orElse: Icon( attempt != null ? attempt!.win ? Icons.check @@ -245,6 +208,31 @@ class _SessionItem extends StatelessWidget { color: Colors.white, size: 18, ), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.fitHeight, + child: Text( + attempt!.ratingDiffString!, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + height: 1, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + ), + ) + : Icon( + attempt != null + ? attempt!.win + ? Icons.check + : Icons.close + : null, + color: Colors.white, + size: 18, + ), ), ); } diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 24660c765c..14a57c25df 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -1,12 +1,10 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -15,74 +13,24 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authSessionProvider)?.user.id; - - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select((pref) => pref.isSoundEnabled), - ); - final autoNext = ref.watch( - PuzzlePreferencesProvider(userId).select((value) => value.autoNext), - ); - final boardPrefs = ref.watch(boardPreferencesProvider); - - return DraggableScrollableSheet( - initialChildSize: .6, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), - value: autoNext, - onChanged: (value) { - ref - .read(puzzlePreferencesProvider(userId).notifier) - .setAutoNext(value); - }, - ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); - }, - ), - ], - ), + final autoNext = ref.watch(puzzlePreferencesProvider.select((value) => value.autoNext)); + return BottomSheetScrollableContainer( + children: [ + SwitchSettingTile( + title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), + value: autoNext, + onChanged: (value) { + ref.read(puzzlePreferencesProvider.notifier).setAutoNext(value); + }, + ), + PlatformListTile( + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute(context, fullscreenDialog: true, screen: const BoardSettingsScreen()); + }, + ), + ], ); } } diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 7482c3d211..a870b218b9 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -1,20 +1,23 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -25,7 +28,6 @@ import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'puzzle_screen.dart'; @@ -36,164 +38,314 @@ import 'streak_screen.dart'; const _kNumberOfHistoryItemsOnHandset = 8; const _kNumberOfHistoryItemsOnTablet = 16; -class PuzzleTabScreen extends ConsumerStatefulWidget { +class PuzzleTabScreen extends ConsumerWidget { const PuzzleTabScreen({super.key}); @override - ConsumerState createState() => _PuzzleTabScreenState(); + Widget build(BuildContext context, WidgetRef ref) { + final savedBatches = ref.watch(savedBatchesProvider).valueOrNull; + + if (savedBatches == null) { + return const Center(child: CircularProgressIndicator()); + } + + if (Theme.of(context).platform == TargetPlatform.iOS) { + return _CupertinoTabBody(savedBatches); + } else { + return _MaterialTabBody(savedBatches); + } + } +} + +Widget _buildMainListItem( + BuildContext context, + int index, + Animation animation, + PuzzleAngle Function(int index) getAngle, +) { + switch (index) { + case 0: + return const _PuzzleMenu(); + case 1: + return Padding( + padding: Styles.horizontalBodyPadding.add( + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionTopPadding + : EdgeInsets.zero, + ), + child: Text(context.l10n.puzzleDesc, style: Styles.sectionTitle), + ); + case 2: + return const DailyPuzzle(); + case 3: + return PuzzleAnglePreview( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const PuzzleScreen(angle: PuzzleTheme(PuzzleThemeKey.mix)), + ); + }, + ); + default: + final angle = getAngle(index); + return PuzzleAnglePreview( + angle: angle, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: angle), + ); + }, + ); + } } -class _PuzzleTabScreenState extends ConsumerState { - final _androidRefreshKey = GlobalKey(); +Widget _buildMainListRemovedItem( + PuzzleAngle angle, + BuildContext context, + Animation animation, +) { + return SizeTransition(sizeFactor: animation, child: PuzzleAnglePreview(angle: angle)); +} + +// display the main body list for cupertino devices, as a workaround +// for missing type to handle both [SliverAnimatedList] and [AnimatedList]. +class _CupertinoTabBody extends ConsumerStatefulWidget { + const _CupertinoTabBody(this.savedBatches); + + final IList<(PuzzleAngle, int)> savedBatches; @override - Widget build(BuildContext context) { - final session = ref.watch(authSessionProvider); - return PlatformWidget( - androidBuilder: (context) => _androidBuilder(context, session), - iosBuilder: (context) => _iosBuilder(context, session), + ConsumerState<_CupertinoTabBody> createState() => _CupertinoTabBodyState(); +} + +class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { + final GlobalKey _listKey = GlobalKey(); + late SliverAnimatedListModel _angles; + + @override + void initState() { + super.initState(); + _angles = SliverAnimatedListModel( + listKey: _listKey, + removedItemBuilder: _buildMainListRemovedItem, + initialItems: widget.savedBatches.map((e) => e.$1), + itemsOffset: 4, ); } - Widget _androidBuilder(BuildContext context, AuthSessionState? userSession) { - final body = Column( - children: [ - const ConnectivityBanner(), - Expanded( - child: _Body(userSession), - ), - ], - ); + @override + void didUpdateWidget(covariant _CupertinoTabBody oldWidget) { + super.didUpdateWidget(oldWidget); + final oldKeys = ISet(oldWidget.savedBatches.map((e) => e.$1)); + final newKeys = ISet(widget.savedBatches.map((e) => e.$1)); + + if (oldKeys != newKeys) { + final missings = oldKeys.difference(newKeys); + if (missings.isNotEmpty) { + for (final missing in missings) { + final index = _angles.indexOf(missing); + if (index != -1) { + _angles.removeAt(index); + } + } + } - return PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, _) { - if (!didPop) { - ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; + final additions = newKeys.difference(oldKeys); + if (additions.isNotEmpty) { + for (final addition in additions) { + _angles.prepend(addition); } - }, - child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.puzzles), - actions: const [ - _DashboardButton(), - ], - ), - body: userSession != null - ? RefreshIndicator( - key: _androidRefreshKey, - onRefresh: _refreshData, - child: body, - ) - : body, - ), - ); + } + } } - Widget _iosBuilder(BuildContext context, AuthSessionState? userSession) { + @override + Widget build(BuildContext context) { + final isTablet = isTabletOrLarger(context); + + Widget buildItem(BuildContext context, int index, Animation animation) => + _buildMainListItem(context, index, animation, (index) => _angles[index]); + + if (isTablet) { + return Row( + children: [ + Expanded( + child: CupertinoPageScaffold( + child: CustomScrollView( + controller: puzzlesScrollController, + slivers: [ + CupertinoSliverNavigationBar( + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), + largeTitle: Text(context.l10n.puzzles), + trailing: const Row( + mainAxisSize: MainAxisSize.min, + children: [_DashboardButton()], + ), + ), + const SliverToBoxAdapter(child: ConnectivityBanner()), + SliverSafeArea( + top: false, + sliver: SliverAnimatedList( + key: _listKey, + initialItemCount: _angles.length, + itemBuilder: buildItem, + ), + ), + ], + ), + ), + ), + VerticalDivider( + width: 1.0, + thickness: 1.0, + color: CupertinoColors.opaqueSeparator.resolveFrom(context), + ), + Expanded( + child: CupertinoPageScaffold( + backgroundColor: CupertinoColors.systemBackground.resolveFrom(context), + navigationBar: CupertinoNavigationBar( + transitionBetweenRoutes: false, + middle: Text(context.l10n.puzzleHistory), + trailing: const _HistoryButton(), + ), + child: ListView(children: const [PuzzleHistoryWidget(showHeader: false)]), + ), + ), + ], + ); + } + return CupertinoPageScaffold( child: CustomScrollView( controller: puzzlesScrollController, slivers: [ CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), largeTitle: Text(context.l10n.puzzles), trailing: const Row( mainAxisSize: MainAxisSize.min, - children: [ - _DashboardButton(), - ], + children: [_DashboardButton(), SizedBox(width: 6.0), _HistoryButton()], ), ), - if (userSession != null) - CupertinoSliverRefreshControl( - onRefresh: _refreshData, - ), const SliverToBoxAdapter(child: ConnectivityBanner()), SliverSafeArea( top: false, - sliver: _Body(userSession), + sliver: SliverAnimatedList( + key: _listKey, + initialItemCount: _angles.length, + itemBuilder: buildItem, + ), ), ], ), ); } +} - Future _refreshData() { - return Future.wait([ - ref.refresh(puzzleRecentActivityProvider.future), - ]); - } +class _MaterialTabBody extends ConsumerStatefulWidget { + const _MaterialTabBody(this.savedBatches); + + final IList<(PuzzleAngle, int)> savedBatches; + + @override + ConsumerState<_MaterialTabBody> createState() => _MaterialTabBodyState(); } -class _Body extends ConsumerWidget { - const _Body(this.session); +class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { + final GlobalKey _listKey = GlobalKey(); + late AnimatedListModel _angles; - final AuthSessionState? session; + @override + void initState() { + super.initState(); + _angles = AnimatedListModel( + listKey: _listKey, + removedItemBuilder: _buildMainListRemovedItem, + initialItems: widget.savedBatches.map((e) => e.$1), + itemsOffset: 4, + ); + } @override - Widget build(BuildContext context, WidgetRef ref) { - final connectivity = ref.watch(connectivityChangesProvider); + void didUpdateWidget(covariant _MaterialTabBody oldWidget) { + super.didUpdateWidget(oldWidget); + final oldKeys = ISet(oldWidget.savedBatches.map((e) => e.$1)); + final newKeys = ISet(widget.savedBatches.map((e) => e.$1)); + + if (oldKeys != newKeys) { + final missings = oldKeys.difference(newKeys); + if (missings.isNotEmpty) { + for (final missing in missings) { + final index = _angles.indexOf(missing); + if (index != -1) { + _angles.removeAt(index); + } + } + } + final additions = newKeys.difference(oldKeys); + if (additions.isNotEmpty) { + for (final addition in additions) { + _angles.prepend(addition); + } + } + } + } + + @override + Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); - final handsetChildren = [ - connectivity.when( - data: (data) => data.isOnline - ? const _DailyPuzzle() - : const _OfflinePuzzlePreview(), - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ), - if (Theme.of(context).platform == TargetPlatform.android) - const SizedBox(height: 8.0), - _PuzzleMenu(connectivity: connectivity), - PuzzleHistoryWidget(), - ]; - - final tabletChildren = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - connectivity.when( - data: (data) => data.isOnline - ? const _DailyPuzzle() - : const _OfflinePuzzlePreview(), - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), + Widget buildItem(BuildContext context, int index, Animation animation) => + _buildMainListItem(context, index, animation, (index) => _angles[index]); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) { + if (!didPop) { + ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(context.l10n.puzzles), + actions: const [_DashboardButton(), _HistoryButton()], + ), + body: + isTablet + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AnimatedList( + key: _listKey, + initialItemCount: _angles.length, + controller: puzzlesScrollController, + itemBuilder: buildItem, + ), + ), + Expanded(child: ListView(children: const [PuzzleHistoryWidget()])), + ], + ) + : Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: AnimatedList( + key: _listKey, + controller: puzzlesScrollController, + initialItemCount: _angles.length, + itemBuilder: buildItem, + ), + ), + ], ), - _PuzzleMenu(connectivity: connectivity), - ], - ), - ), - Expanded( - child: Column( - children: [ - PuzzleHistoryWidget(), - ], - ), - ), - ], ), - ]; - - final children = isTablet ? tabletChildren : handsetChildren; - - return Theme.of(context).platform == TargetPlatform.iOS - ? SliverList(delegate: SliverChildListDelegate.fixed(children)) - : ListView( - controller: puzzlesScrollController, - children: children, - ); + ); } } @@ -213,53 +365,40 @@ class _PuzzleMenuListTile extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) - : null, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) + : null, leading: Icon( icon, size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, ), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: onTap, ); } } -class _PuzzleMenu extends StatelessWidget { - const _PuzzleMenu({required this.connectivity}); - - final AsyncValue connectivity; +class _PuzzleMenu extends ConsumerWidget { + const _PuzzleMenu(); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final connectivity = ref.watch(connectivityChangesProvider); final bool isOnline = connectivity.value?.isOnline ?? false; return ListSection( hasLeading: true, children: [ - _PuzzleMenuListTile( - icon: PuzzleIcons.mix, - title: context.l10n.puzzlePuzzles, - subtitle: context.l10n.puzzleDesc, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.puzzleDesc, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ); - }, - ), _PuzzleMenuListTile( icon: PuzzleIcons.opening, title: context.l10n.puzzlePuzzleThemes, @@ -268,7 +407,6 @@ class _PuzzleMenu extends StatelessWidget { pushPlatformRoute( context, title: context.l10n.puzzlePuzzleThemes, - rootNavigator: true, builder: (context) => const PuzzleThemesScreen(), ); }, @@ -278,19 +416,21 @@ class _PuzzleMenu extends StatelessWidget { child: _PuzzleMenuListTile( icon: LichessIcons.streak, title: 'Puzzle Streak', - subtitle: context.l10n.puzzleStreakDescription.characters + subtitle: + context.l10n.puzzleStreakDescription.characters .takeWhile((c) => c != '.') .toString() + (context.l10n.puzzleStreakDescription.contains('.') ? '.' : ''), - onTap: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const StreakScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StreakScreen(), + ); + } + : null, ), ), Opacity( @@ -299,15 +439,16 @@ class _PuzzleMenu extends StatelessWidget { icon: LichessIcons.storm, title: 'Puzzle Storm', subtitle: context.l10n.mobilePuzzleStormSubtitle, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const StormScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StormScreen(), + ); + } + : null, ), ), ], @@ -316,6 +457,10 @@ class _PuzzleMenu extends StatelessWidget { } class PuzzleHistoryWidget extends ConsumerWidget { + const PuzzleHistoryWidget({this.showHeader = true}); + + final bool showHeader; + @override Widget build(BuildContext context, WidgetRef ref) { final asyncData = ref.watch(puzzleRecentActivityProvider); @@ -327,14 +472,11 @@ class PuzzleHistoryWidget extends ConsumerWidget { } if (recentActivity.isEmpty) { return ListSection( - header: Text(context.l10n.puzzleHistory), + header: showHeader ? Text(context.l10n.puzzleHistory) : null, children: [ Center( child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 8.0, - ), + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), child: Text(context.l10n.puzzleNoPuzzlesToShow), ), ), @@ -342,55 +484,49 @@ class PuzzleHistoryWidget extends ConsumerWidget { ); } - final maxItems = isTablet - ? _kNumberOfHistoryItemsOnTablet - : _kNumberOfHistoryItemsOnHandset; + final maxItems = + isTablet ? _kNumberOfHistoryItemsOnTablet : _kNumberOfHistoryItemsOnHandset; return ListSection( - cupertinoBackgroundColor: - CupertinoTheme.of(context).scaffoldBackgroundColor, + cupertinoBackgroundColor: CupertinoPageScaffoldBackgroundColor.maybeOf(context), cupertinoClipBehavior: Clip.none, - header: Text(context.l10n.puzzleHistory), - headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => PuzzleHistoryScreen(), - ), - child: Text( - context.l10n.more, - ), - ), + header: showHeader ? Text(context.l10n.puzzleHistory) : null, + headerTrailing: + showHeader + ? NoPaddingTextButton( + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => const PuzzleHistoryScreen(), + ), + child: Text(context.l10n.more), + ) + : null, children: [ Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : Styles.horizontalBodyPadding, - child: PuzzleHistoryPreview( - recentActivity.take(maxItems).toIList(), - maxRows: 5, - ), + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : Styles.horizontalBodyPadding, + child: PuzzleHistoryPreview(recentActivity.take(maxItems).toIList(), maxRows: 5), ), ], ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleHistoryWidget] could not load puzzle history', - ); - return Padding( + debugPrint('SEVERE: [PuzzleHistoryWidget] could not load puzzle history'); + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load Puzzle history.'), + child: Text('Could not load Puzzle history.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 5, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 5, header: true), + ), ), - ), - ), ); } } @@ -401,40 +537,84 @@ class _DashboardButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - if (session != null) { - return AppBarIconButton( - icon: const Icon(Icons.history), - semanticsLabel: context.l10n.puzzlePuzzleDashboard, - onPressed: () { - ref.invalidate(puzzleDashboardProvider); - _showDashboard(context, session); - }, - ); + if (session == null) { + return const SizedBox.shrink(); } - return const SizedBox.shrink(); + final onPressed = ref + .watch(connectivityChangesProvider) + .whenIs( + online: + () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzlePuzzleDashboard, + builder: (_) => const PuzzleDashboardScreen(), + ); + }, + offline: () => null, + ); + + return AppBarIconButton( + icon: const Icon(Icons.assessment_outlined), + semanticsLabel: context.l10n.puzzlePuzzleDashboard, + onPressed: onPressed, + ); } +} - void _showDashboard(BuildContext context, AuthSessionState session) => - pushPlatformRoute( - context, - title: context.l10n.puzzlePuzzleDashboard, - builder: (_) => const PuzzleDashboardScreen(), - ); +class _HistoryButton extends ConsumerWidget { + const _HistoryButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(authSessionProvider); + if (session == null) { + return const SizedBox.shrink(); + } + final onPressed = ref + .watch(connectivityChangesProvider) + .whenIs( + online: + () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzleHistory, + builder: (_) => const PuzzleHistoryScreen(), + ); + }, + offline: () => null, + ); + return AppBarIconButton( + icon: const Icon(Icons.history_outlined), + semanticsLabel: context.l10n.puzzleHistory, + onPressed: onPressed, + ); + } } -class _DailyPuzzle extends ConsumerWidget { - const _DailyPuzzle(); +TextStyle _puzzlePreviewSubtitleStyle(BuildContext context) { + return TextStyle( + fontSize: 14.0, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ); +} + +/// A widget that displays the daily puzzle. +class DailyPuzzle extends ConsumerWidget { + const DailyPuzzle({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final puzzle = ref.watch(dailyPuzzleProvider); + return puzzle.when( data: (data) { final preview = PuzzlePreview.fromPuzzle(data); return SmallBoardPreview( - orientation: preview.orientation.cg, + orientation: preview.orientation, fen: preview.initialFen, - lastMove: preview.initialMove.cg, + lastMove: preview.initialMove, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -442,25 +622,20 @@ class _DailyPuzzle extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(context.l10n.puzzlePuzzleOfTheDay, style: Styles.boardPreviewTitle), Text( - context.l10n.puzzlePuzzleOfTheDay, - style: Styles.boardPreviewTitle, - ), - Text( - context.l10n - .puzzlePlayedXTimes(data.puzzle.plays) - .localizeNumbers(), + context.l10n.puzzlePlayedXTimes(data.puzzle.plays).localizeNumbers(), + style: _puzzlePreviewSubtitleStyle(context), ), ], ), Icon( Icons.today, size: 34, - color: - DefaultTextStyle.of(context).style.color?.withOpacity(0.6), + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), ), Text( - data.puzzle.initialPly.isOdd + data.puzzle.sideToMove == Side.white ? context.l10n.whitePlays : context.l10n.blackPlays, ), @@ -471,90 +646,154 @@ class _DailyPuzzle extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: data.puzzle.id, - ), + builder: + (context) => PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: data.puzzle.id, + ), ); }, ); }, - loading: () => SmallBoardPreview( - orientation: Side.white.cg, - fen: kEmptyFen, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - context.l10n.puzzlePuzzleOfTheDay, - style: Styles.boardPreviewTitle, - ), - const Text(''), - ], - ), - ), - error: (error, stack) => Padding( - padding: Styles.bodySectionPadding, - child: const Text('Could not load the daily puzzle.'), - ), + loading: + () => + isOnline + ? const Shimmer( + child: ShimmerLoading(isLoading: true, child: SmallBoardPreview.loading()), + ) + : const SizedBox.shrink(), + error: (error, _) { + return isOnline + ? const Padding( + padding: Styles.bodySectionPadding, + child: Text('Could not load the daily puzzle.'), + ) + : const SizedBox.shrink(); + }, ); } } -class _OfflinePuzzlePreview extends ConsumerWidget { - const _OfflinePuzzlePreview(); +/// A widget that displays a preview of a puzzle angle batch. +class PuzzleAnglePreview extends ConsumerWidget { + const PuzzleAnglePreview({required this.angle, this.onTap, super.key}); + + final PuzzleAngle angle; + final VoidCallback? onTap; @override Widget build(BuildContext context, WidgetRef ref) { - final puzzle = - ref.watch(nextPuzzleProvider(const PuzzleTheme(PuzzleThemeKey.mix))); - return puzzle.maybeWhen( - data: (data) { - final preview = - data != null ? PuzzlePreview.fromPuzzle(data.puzzle) : null; - return SmallBoardPreview( - orientation: preview?.orientation.cg ?? Side.white.cg, - fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove.cg, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - context.l10n.puzzleDesc, - style: Styles.boardPreviewTitle, - ), - Text( - context.l10n - .puzzlePlayedXTimes(data?.puzzle.puzzle.plays ?? 0) - .localizeNumbers(), - ), - ], - ), - onTap: data != null - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ).then((_) { + final puzzle = ref.watch(nextPuzzleProvider(angle)); + final flatOpenings = ref.watch(flatOpeningsListProvider); + + Widget buildPuzzlePreview(Puzzle? puzzle, {bool loading = false}) { + final preview = puzzle != null ? PuzzlePreview.fromPuzzle(puzzle) : null; + + return loading + ? const Shimmer( + child: ShimmerLoading(isLoading: true, child: SmallBoardPreview.loading()), + ) + : Slidable( + dragStartBehavior: DragStartBehavior.start, + enabled: angle != const PuzzleTheme(PuzzleThemeKey.mix), + endActionPane: ActionPane( + motion: const StretchMotion(), + children: [ + SlidableAction( + icon: Icons.delete, + onPressed: (context) async { + final service = await ref.read(puzzleServiceProvider.future); if (context.mounted) { - ref.invalidate( - nextPuzzleProvider( - const PuzzleTheme(PuzzleThemeKey.mix), - ), + service.deleteBatch( + userId: ref.read(authSessionProvider)?.user.id, + angle: angle, ); } - }); - } - : null, - ); - }, - orElse: () => const SizedBox.shrink(), + }, + spacing: 8.0, + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + label: context.l10n.delete, + ), + ], + ), + child: SmallBoardPreview( + orientation: preview?.orientation ?? Side.white, + fen: preview?.initialFen ?? kEmptyFen, + lastMove: preview?.initialMove, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: switch (angle) { + PuzzleTheme(themeKey: final themeKey) => [ + Text( + themeKey.l10n(context.l10n).name, + style: Styles.boardPreviewTitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text( + themeKey.l10n(context.l10n).description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ), + ), + ], + PuzzleOpening(key: final openingKey) => [ + Text( + flatOpenings.valueOrNull + ?.firstWhere( + (o) => o.key == openingKey, + orElse: + () => ( + key: openingKey, + name: openingKey.replaceAll('_', ''), + count: 0, + ), + ) + .name ?? + openingKey.replaceAll('_', ' '), + style: Styles.boardPreviewTitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + }, + ), + Icon( + switch (angle) { + PuzzleTheme(themeKey: final themeKey) => themeKey.icon, + PuzzleOpening() => PuzzleIcons.opening, + }, + size: 34, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ), + if (puzzle != null) + Text( + puzzle.puzzle.sideToMove == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ) + else + const Text('No puzzles available, please go online to fetch them.'), + ], + ), + onTap: puzzle != null ? onTap : null, + ), + ); + } + + return puzzle.maybeWhen( + data: (data) => buildPuzzlePreview(data?.puzzle), + orElse: () => buildPuzzlePreview(null, loading: true), ); } } diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index 3887cc76e8..9d90db4ab6 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -5,29 +5,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/puzzle/opening_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'puzzle_screen.dart'; -part 'puzzle_themes_screen.g.dart'; - @riverpod -Future< - ( - bool, - IMap, - IMap?, - bool, - )> _themes( - _ThemesRef ref, -) async { +final _themesProvider = FutureProvider.autoDispose< + (bool, IMap, IMap?, bool) +>((ref) async { final connectivity = await ref.watch(connectivityChangesProvider.future); final savedThemes = await ref.watch(savedThemeBatchesProvider.future); IMap? onlineThemes; @@ -37,42 +29,19 @@ Future< onlineThemes = null; } final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); - return ( - connectivity.isOnline, - savedThemes, - onlineThemes, - savedOpenings.isNotEmpty - ); -} + return (connectivity.isOnline, savedThemes, onlineThemes, savedOpenings.isNotEmpty); +}); class PuzzleThemesScreen extends StatelessWidget { const PuzzleThemesScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.puzzlePuzzleThemes), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.puzzlePuzzleThemes)), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzlePuzzleThemes), - ), - child: const _Body(), - ); - } } class _Body extends ConsumerWidget { @@ -83,55 +52,51 @@ class _Body extends ConsumerWidget { // skip recommended category since we display it on the puzzle tab screen final list = ref.watch(puzzleThemeCategoriesProvider).skip(1).toList(); final themes = ref.watch(_themesProvider); - final expansionTileColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondaryLabel.resolveFrom(context) - : null; - - return SafeArea( - child: themes.when( - data: (data) { - final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = - data; - - final openingsAvailable = hasConnectivity || hasSavedOpenings; - return ListView( - children: [ - Theme( - data: Theme.of(context) - .copyWith(dividerColor: Colors.transparent), - child: Opacity( - opacity: openingsAvailable ? 1 : 0.5, - child: ExpansionTile( - iconColor: expansionTileColor, - collapsedIconColor: expansionTileColor, - title: Text(context.l10n.puzzleByOpenings), - trailing: const Icon(Icons.keyboard_arrow_right), - onExpansionChanged: openingsAvailable - ? (expanded) { + final expansionTileColor = + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondaryLabel.resolveFrom(context) + : null; + + return themes.when( + data: (data) { + final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = data; + + final openingsAvailable = hasConnectivity || hasSavedOpenings; + return ListView( + children: [ + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: Opacity( + opacity: openingsAvailable ? 1 : 0.5, + child: ExpansionTile( + iconColor: expansionTileColor, + collapsedIconColor: expansionTileColor, + title: Text(context.l10n.puzzleByOpenings), + trailing: const Icon(Icons.keyboard_arrow_right), + onExpansionChanged: + openingsAvailable + ? (expanded) { pushPlatformRoute( context, builder: (ctx) => const OpeningThemeScreen(), ); } - : null, - ), + : null, ), ), - for (final category in list) - _Category( - hasConnectivity: hasConnectivity, - category: category, - onlineThemes: onlineThemes, - savedThemes: savedThemes, - ), - ], - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stack) => - const Center(child: Text('Could not load themes.')), - ), + ), + for (final category in list) + _Category( + hasConnectivity: hasConnectivity, + category: category, + onlineThemes: onlineThemes, + savedThemes: savedThemes, + ), + ], + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (error, stack) => const Center(child: Text('Could not load themes.')), ); } } @@ -153,10 +118,7 @@ class _Category extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final themeCountStyle = TextStyle( fontSize: 12, - color: textShade( - context, - Styles.subtitleOpacity, - ), + color: textShade(context, Styles.subtitleOpacity), ); final (categoryName, themes) = category; @@ -168,79 +130,67 @@ class _Category extends ConsumerWidget { children: [ ListSection( hasLeading: true, - children: themes.map( - (theme) { - final isThemeAvailable = - hasConnectivity || savedThemes.containsKey(theme); - - return Opacity( - opacity: isThemeAvailable ? 1 : 0.5, - child: PlatformListTile( - leading: Icon(puzzleThemeIcon(theme)), - trailing: hasConnectivity && - onlineThemes?.containsKey(theme) == true - ? Padding( - padding: const EdgeInsets.only(left: 6.0), - child: Text( - '${onlineThemes![theme]!.count}', - style: themeCountStyle, - ), - ) - : savedThemes.containsKey(theme) - ? Padding( + children: + themes.map((theme) { + final isThemeAvailable = hasConnectivity || savedThemes.containsKey(theme); + + return Opacity( + opacity: isThemeAvailable ? 1 : 0.5, + child: PlatformListTile( + leading: Icon(theme.icon), + trailing: + hasConnectivity && onlineThemes?.containsKey(theme) == true + ? Padding( padding: const EdgeInsets.only(left: 6.0), child: Text( - '${savedThemes[theme]!}', + '${onlineThemes![theme]!.count}', style: themeCountStyle, ), ) - : null, - title: Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.only(top: 6.0) - : EdgeInsets.zero, - child: Text( - puzzleThemeL10n(context, theme).name, - style: Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - color: CupertinoTheme.of(context) - .textTheme - .textStyle - .color, + : savedThemes.containsKey(theme) + ? Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Text('${savedThemes[theme]!}', style: themeCountStyle), ) - : null, + : null, + title: Padding( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.only(top: 6.0) + : EdgeInsets.zero, + child: Text( + theme.l10n(context.l10n).name, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle( + color: CupertinoTheme.of(context).textTheme.textStyle.color, + ) + : null, + ), ), - ), - subtitle: Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Text( - puzzleThemeL10n(context, theme).description, - maxLines: 10, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), + subtitle: Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Text( + theme.l10n(context.l10n).description, + maxLines: 10, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), ), ), + isThreeLine: true, + onTap: + isThemeAvailable + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: PuzzleTheme(theme)), + ); + } + : null, ), - isThreeLine: true, - onTap: isThemeAvailable - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleTheme(theme), - ), - ); - } - : null, - ), - ); - }, - ).toList(), + ); + }).toList(), ), ], ), diff --git a/lib/src/view/puzzle/storm_clock.dart b/lib/src/view/puzzle/storm_clock.dart index c02d586663..be4dbd0e7f 100644 --- a/lib/src/view/puzzle/storm_clock.dart +++ b/lib/src/view/puzzle/storm_clock.dart @@ -16,23 +16,20 @@ class StormClockWidget extends StatefulWidget { _ClockState createState() => _ClockState(); } -class _ClockState extends State - with SingleTickerProviderStateMixin { +class _ClockState extends State with SingleTickerProviderStateMixin { // ignore: avoid-late-keyword late AnimationController _controller; // ignore: avoid-late-keyword - late final Animation _bonusFadeAnimation = - Tween(begin: 1.0, end: 0.0).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOut), - ); + late final Animation _bonusFadeAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); // ignore: avoid-late-keyword late final Animation _bonusSlideAnimation = Tween( begin: const Offset(0.7, 0.0), end: const Offset(0.7, -1.0), - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOut), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); StreamSubscription<(Duration, int?)>? streamSubscription; @@ -46,10 +43,8 @@ class _ClockState extends State // declaring as late final causes an error because the widget is being disposed // after the clock start - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - )..addStatusListener((status) { + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500)) + ..addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() { currentBonusSeconds = null; @@ -98,9 +93,8 @@ class _ClockState extends State @override Widget build(BuildContext build) { final brightness = Theme.of(context).brightness; - final clockStyle = brightness == Brightness.dark - ? _ClockStyle.darkThemeStyle - : _ClockStyle.lightThemeStyle; + final clockStyle = + brightness == Brightness.dark ? _ClockStyle.darkThemeStyle : _ClockStyle.lightThemeStyle; final minutes = time.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = time.inSeconds.remainder(60).toString().padLeft(2, '0'); @@ -120,9 +114,10 @@ class _ClockState extends State child: Text( '${currentBonusSeconds! > 0 ? '+' : ''}$currentBonusSeconds', style: TextStyle( - color: currentBonusSeconds! < 0 - ? context.lichessColors.error - : context.lichessColors.good, + color: + currentBonusSeconds! < 0 + ? context.lichessColors.error + : context.lichessColors.good, fontWeight: FontWeight.bold, fontSize: 20, fontFeatures: const [FontFeature.tabularFigures()], @@ -139,20 +134,17 @@ class _ClockState extends State ), duration: const Duration(milliseconds: 500), builder: (context, Duration value, _) { - final minutes = - value.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = - value.inSeconds.remainder(60).toString().padLeft(2, '0'); + final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); return Text( '$minutes:$seconds', style: TextStyle( - color: currentBonusSeconds! < 0 - ? context.lichessColors.error - : context.lichessColors.good, + color: + currentBonusSeconds! < 0 + ? context.lichessColors.error + : context.lichessColors.good, fontSize: _kClockFontSize, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], + fontFeatures: const [FontFeature.tabularFigures()], ), ); }, @@ -161,13 +153,9 @@ class _ClockState extends State Text( '$minutes:$seconds', style: TextStyle( - color: isActive - ? clockStyle.activeTextColor - : clockStyle.textColor, + color: isActive ? clockStyle.activeTextColor : clockStyle.textColor, fontSize: _kClockFontSize, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], + fontFeatures: const [FontFeature.tabularFigures()], ), ), ], @@ -178,19 +166,10 @@ class _ClockState extends State } enum _ClockStyle { - darkThemeStyle( - textColor: Colors.grey, - activeTextColor: Colors.white, - ), - lightThemeStyle( - textColor: Colors.grey, - activeTextColor: Colors.black, - ); - - const _ClockStyle({ - required this.textColor, - required this.activeTextColor, - }); + darkThemeStyle(textColor: Colors.grey, activeTextColor: Colors.white), + lightThemeStyle(textColor: Colors.grey, activeTextColor: Colors.black); + + const _ClockStyle({required this.textColor, required this.activeTextColor}); final Color textColor; final Color activeTextColor; diff --git a/lib/src/view/puzzle/storm_dashboard.dart b/lib/src/view/puzzle/storm_dashboard.dart index 5ae03b1a81..25ebd5cfa1 100644 --- a/lib/src/view/puzzle/storm_dashboard.dart +++ b/lib/src/view/puzzle/storm_dashboard.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -9,6 +8,7 @@ import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; @@ -19,32 +19,19 @@ class StormDashboardModal extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(LichessIcons.storm, size: 20), - const SizedBox(width: 8.0), - Text(context.l10n.stormHighscores), - ], - ), - ), - child: _Body(user: user), - ) - : Scaffold( - body: _Body(user: user), - appBar: AppBar( - title: Row( - children: [ - const Icon(LichessIcons.storm, size: 20), - const SizedBox(width: 8.0), - Text(context.l10n.stormHighscores), - ], - ), - ), - ); + return PlatformScaffold( + body: _Body(user: user), + appBar: PlatformAppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(LichessIcons.storm, size: 20), + const SizedBox(width: 8.0), + Text(context.l10n.stormHighscores), + ], + ), + ), + ); } } @@ -67,44 +54,23 @@ class _Body extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: - Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), - child: StatCardRow( - [ - StatCard( - context.l10n.stormAllTime, - value: data.highScore.allTime.toString(), - ), - StatCard( - context.l10n.stormThisMonth, - value: data.highScore.month.toString(), - ), - ], - ), + padding: Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), + child: StatCardRow([ + StatCard(context.l10n.stormAllTime, value: data.highScore.allTime.toString()), + StatCard(context.l10n.stormThisMonth, value: data.highScore.month.toString()), + ]), ), Padding( - padding: - Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), - child: StatCardRow( - [ - StatCard( - context.l10n.stormThisWeek, - value: data.highScore.week.toString(), - ), - StatCard( - context.l10n.today, - value: data.highScore.day.toString(), - ), - ], - ), + padding: Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), + child: StatCardRow([ + StatCard(context.l10n.stormThisWeek, value: data.highScore.week.toString()), + StatCard(context.l10n.today, value: data.highScore.day.toString()), + ]), ), if (data.dayHighscores.isNotEmpty) ...[ Padding( padding: Styles.bodySectionPadding, - child: Text( - context.l10n.stormBestRunOfDay, - style: Styles.sectionTitle, - ), + child: Text(context.l10n.stormBestRunOfDay, style: Styles.sectionTitle), ), Padding( padding: Styles.horizontalBodyPadding, @@ -113,22 +79,10 @@ class _Body extends ConsumerWidget { children: [ TableRow( children: [ - Text( - textAlign: TextAlign.center, - context.l10n.stormScore, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormTime, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormHighestSolved, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormRuns, - ), + Text(textAlign: TextAlign.center, context.l10n.stormScore), + Text(textAlign: TextAlign.center, context.l10n.stormTime), + Text(textAlign: TextAlign.center, context.l10n.stormHighestSolved), + Text(textAlign: TextAlign.center, context.l10n.stormRuns), ], ), ], @@ -142,14 +96,12 @@ class _Body extends ConsumerWidget { // Date row final entryIndex = index ~/ 2; return ColoredBox( - color: LichessColors.grey.withOpacity(0.23), + color: LichessColors.grey.withValues(alpha: 0.23), child: Padding( padding: Styles.horizontalBodyPadding, child: Text( - dateFormat - .format(data.dayHighscores[entryIndex].day), - style: - const TextStyle(fontWeight: FontWeight.w600), + dateFormat.format(data.dayHighscores[entryIndex].day), + style: const TextStyle(fontWeight: FontWeight.w600), ), ), ); @@ -157,20 +109,15 @@ class _Body extends ConsumerWidget { // Data row final entryIndex = (index - 1) ~/ 2; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 10, - ), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ TableRow( children: [ Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].score - .toString(), + data.dayHighscores[entryIndex].score.toString(), style: TextStyle( color: context.lichessColors.brag, fontWeight: FontWeight.bold, @@ -182,13 +129,11 @@ class _Body extends ConsumerWidget { ), Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].highest - .toString(), + data.dayHighscores[entryIndex].highest.toString(), ), Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].runs - .toString(), + data.dayHighscores[entryIndex].runs.toString(), ), ], ), @@ -200,17 +145,13 @@ class _Body extends ConsumerWidget { ), ), ] else - Center( - child: Text(context.l10n.mobilePuzzleStormNothingToShow), - ), + Center(child: Text(context.l10n.mobilePuzzleStormNothingToShow)), ], ), ); }, error: (e, s) { - debugPrint( - 'SEVERE: [StormDashboardModel] could not load storm dashboard; $e\n$s', - ); + debugPrint('SEVERE: [StormDashboardModel] could not load storm dashboard; $e\n$s'); return const SafeArea(child: Text('Could not load dashboard')); }, loading: () => _Loading(), diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index a0395b05f9..2f4b7867e1 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; @@ -15,7 +15,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/gestures_exclusion.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -25,11 +24,13 @@ import 'package:lichess_mobile/src/view/puzzle/storm_clock.dart'; import 'package:lichess_mobile/src/view/puzzle/storm_dashboard.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; class StormScreen extends ConsumerStatefulWidget { @@ -45,37 +46,13 @@ class _StormScreenState extends ConsumerState { @override Widget build(BuildContext context) { return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: [_StormDashboardButton(), ToggleSoundButton()], - title: const Text('Puzzle Storm'), - ), - body: _Load(_boardKey), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: const Text('Puzzle Storm'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [_StormDashboardButton(), ToggleSoundButton()], + child: PlatformScaffold( + appBar: PlatformAppBar( + actions: [_StormDashboardButton(), const ToggleSoundButton()], + title: const Text('Puzzle Storm'), ), + body: _Load(_boardKey), ), - child: _Load(_boardKey), ); } } @@ -94,18 +71,13 @@ class _Load extends ConsumerWidget { }, loading: () => const CenterLoadingIndicator(), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleStormScreen] could not load streak; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleStormScreen] could not load streak; $e\n$s'); return Center( child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ); @@ -150,14 +122,15 @@ class _Body extends ConsumerWidget { final NavigatorState navigator = Navigator.of(context); final shouldPop = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: Text(context.l10n.mobilePuzzleStormConfirmEndRun), - onYes: () { - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: Text(context.l10n.mobilePuzzleStormConfirmEndRun), + onYes: () { + return Navigator.of(context).pop(true); + }, + onNo: () => Navigator.of(context).pop(false), + ), ); if (shouldPop ?? false) { navigator.pop(); @@ -171,27 +144,27 @@ class _Body extends ConsumerWidget { bottom: false, child: BoardTable( boardKey: boardKey, - onMove: (move, {isDrop, isPremove}) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - onPremove: (move) => - ref.read(ctrlProvider.notifier).setPremove(move), - boardData: cg.BoardData( - orientation: stormState.pov.cg, - interactableSide: !stormState.firstMovePlayed || - stormState.mode == StormMode.ended || - stormState.position.isGameOver - ? cg.InteractableSide.none - : stormState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: stormState.position.fen, - isCheck: boardPreferences.boardHighlights && - stormState.position.isCheck, - lastMove: stormState.lastMove?.cg, - sideToMove: stormState.position.turn.cg, + orientation: stormState.pov, + lastMove: stormState.lastMove as NormalMove?, + fen: stormState.position.fen, + gameData: GameData( + playerSide: + !stormState.firstMovePlayed || + stormState.mode == StormMode.ended || + stormState.position.isGameOver + ? PlayerSide.none + : stormState.pov == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPreferences.boardHighlights && stormState.position.isCheck, + sideToMove: stormState.position.turn, validMoves: stormState.validMoves, - premove: stormState.premove, + promotionMove: stormState.promotionMove, + onMove: + (move, {isDrop, captured}) => + ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelection: + (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), topTable: _TopTable(data), bottomTable: _Combo(stormState.combo), @@ -206,14 +179,12 @@ class _Body extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.android ? AndroidGesturesExclusionWidget( - boardKey: boardKey, - shouldExcludeGesturesOnFocusGained: () => - stormState.mode == StormMode.initial || - stormState.mode == StormMode.running, - shouldSetImmersiveMode: - boardPreferences.immersiveModeWhilePlaying ?? false, - child: content, - ) + boardKey: boardKey, + shouldExcludeGesturesOnFocusGained: + () => stormState.mode == StormMode.initial || stormState.mode == StormMode.running, + shouldSetImmersiveMode: boardPreferences.immersiveModeWhilePlaying ?? false, + child: content, + ) : content; } } @@ -227,57 +198,31 @@ Future _stormInfoDialogBuilder(BuildContext context) { text: TextSpan( style: DefaultTextStyle.of(context).style, children: const [ - TextSpan( - text: '\n', - ), + TextSpan(text: '\n'), TextSpan( text: 'Each puzzle grants one point. The goal is to get as many points as you can before the time runs out.', ), - TextSpan( - text: '\n\n', - ), - TextSpan( - text: 'Combo bar\n', - style: TextStyle(fontSize: 18), - ), + TextSpan(text: '\n\n'), + TextSpan(text: 'Combo bar\n', style: TextStyle(fontSize: 18)), TextSpan( text: 'Each correct ', children: [ - TextSpan( - text: 'move', - style: TextStyle(fontWeight: FontWeight.bold), - ), + TextSpan(text: 'move', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan( text: ' fills the combo bar. When the bar is full, you get a time bonus, and you increase the value of the next bonus.', ), ], ), - TextSpan( - text: '\n\n', - ), - TextSpan( - text: 'Bonus values:\n', - ), - TextSpan( - text: '• 5 moves: +3s\n', - ), - TextSpan( - text: '• 12 moves: +5s\n', - ), - TextSpan( - text: '• 20 moves: +7s\n', - ), - TextSpan( - text: '• 30 moves: +10s\n', - ), - TextSpan( - text: '• Then +10s every 10 other moves.\n', - ), - TextSpan( - text: '\n', - ), + TextSpan(text: '\n\n'), + TextSpan(text: 'Bonus values:\n'), + TextSpan(text: '• 5 moves: +3s\n'), + TextSpan(text: '• 12 moves: +5s\n'), + TextSpan(text: '• 20 moves: +7s\n'), + TextSpan(text: '• 30 moves: +10s\n'), + TextSpan(text: '• Then +10s every 10 other moves.\n'), + TextSpan(text: '\n'), TextSpan( text: 'When you play a wrong move, the combo bar is depleted, and you lose 10 seconds.', @@ -286,27 +231,17 @@ Future _stormInfoDialogBuilder(BuildContext context) { ), ), ); - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoAlertDialog( - title: Text(context.l10n.aboutX('Puzzle Storm')), - content: content, - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ) - : AlertDialog( - title: Text(context.l10n.aboutX('Puzzle Storm')), - content: content, - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ); + + return PlatformAlertDialog( + title: Text(context.l10n.aboutX('Puzzle Storm')), + content: content, + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ); }, ); } @@ -327,8 +262,7 @@ class _TopTable extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final stormState = - ref.watch(stormControllerProvider(data.puzzles, data.timestamp)); + final stormState = ref.watch(stormControllerProvider(data.puzzles, data.timestamp)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( @@ -347,10 +281,7 @@ class _TopTable extends ConsumerWidget { context.l10n.stormMoveToStart, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( stormState.pov == Side.white @@ -358,20 +289,14 @@ class _TopTable extends ConsumerWidget { : context.l10n.stormYouPlayTheBlackPiecesInAllPuzzles, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 12, - ), + style: const TextStyle(fontSize: 12), ), ], ), ), ) else ...[ - Icon( - LichessIcons.storm, - size: 50.0, - color: context.lichessColors.brag, - ), + Icon(LichessIcons.storm, size: 50.0, color: context.lichessColors.brag), const SizedBox(width: 8), Text( stormState.numSolved.toString(), @@ -399,8 +324,7 @@ class _Combo extends ConsumerStatefulWidget { ConsumerState<_Combo> createState() => _ComboState(); } -class _ComboState extends ConsumerState<_Combo> - with SingleTickerProviderStateMixin { +class _ComboState extends ConsumerState<_Combo> with SingleTickerProviderStateMixin { late AnimationController _controller; @override @@ -424,14 +348,12 @@ class _ComboState extends ConsumerState<_Combo> if (ref.read(boardPreferencesProvider).hapticFeedback) { HapticFeedback.heavyImpact(); } - _controller.animateTo(1.0, curve: Curves.easeInOut).then( - (_) async { - await Future.delayed(const Duration(milliseconds: 300)); - if (mounted) { - _controller.value = 0; - } - }, - ); + _controller.animateTo(1.0, curve: Curves.easeInOut).then((_) async { + await Future.delayed(const Duration(milliseconds: 300)); + if (mounted) { + _controller.value = 0; + } + }); return; } _controller.animateTo(newVal, curve: Curves.easeIn); @@ -455,145 +377,139 @@ class _ComboState extends ConsumerState<_Combo> ); return AnimatedBuilder( animation: _controller, - builder: (context, child) => LayoutBuilder( - builder: (context, constraints) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.combo.current.toString(), - style: TextStyle( - fontSize: 26, - height: 1.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context) - .textTheme - .textStyle - .color - : null, - ), - ), - Text( - context.l10n.stormCombo, - style: TextStyle( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context) - .textTheme - .textStyle - .color - : null, - ), - ), - ], - ), - ), - SizedBox( - width: constraints.maxWidth * 0.65, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 25, - child: Container( - decoration: BoxDecoration( - boxShadow: _controller.value == 1.0 - ? [ - BoxShadow( - color: indicatorColor.withOpacity(0.3), - blurRadius: 10.0, - spreadRadius: 2.0, - ), - ] - : [], + builder: + (context, child) => LayoutBuilder( + builder: (context, constraints) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.combo.current.toString(), + style: TextStyle( + fontSize: 26, + height: 1.0, + fontWeight: FontWeight.bold, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.color + : null, + ), ), - child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(3.0)), - child: LinearProgressIndicator( - value: _controller.value, - valueColor: - AlwaysStoppedAnimation(indicatorColor), + Text( + context.l10n.stormCombo, + style: TextStyle( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.color + : null, ), ), - ), + ], ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: - StormCombo.levelBonus.mapIndexed((index, level) { - final isCurrentLevel = index < lvl; - return AnimatedContainer( - alignment: Alignment.center, - curve: Curves.easeIn, - duration: const Duration(milliseconds: 1000), - width: 28 * - MediaQuery.textScalerOf(context).scale(14) / - 14, - height: 24 * - MediaQuery.textScalerOf(context).scale(14) / - 14, - decoration: isCurrentLevel - ? BoxDecoration( - color: comboShades[index], - borderRadius: const BorderRadius.all( - Radius.circular(3.0), - ), - ) - : null, - child: Text( - '${level}s', - style: TextStyle( - color: isCurrentLevel - ? Theme.of(context).colorScheme.onSecondary - : null, + ), + SizedBox( + width: constraints.maxWidth * 0.65, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 25, + child: Container( + decoration: BoxDecoration( + boxShadow: + _controller.value == 1.0 + ? [ + BoxShadow( + color: indicatorColor.withValues(alpha: 0.3), + blurRadius: 10.0, + spreadRadius: 2.0, + ), + ] + : [], + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(3.0)), + child: LinearProgressIndicator( + value: _controller.value, + valueColor: AlwaysStoppedAnimation(indicatorColor), + ), ), ), - ); - }).toList(), + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: + StormCombo.levelBonus.mapIndexed((index, level) { + final isCurrentLevel = index < lvl; + return AnimatedContainer( + alignment: Alignment.center, + curve: Curves.easeIn, + duration: const Duration(milliseconds: 1000), + width: 28 * MediaQuery.textScalerOf(context).scale(14) / 14, + height: 24 * MediaQuery.textScalerOf(context).scale(14) / 14, + decoration: + isCurrentLevel + ? BoxDecoration( + color: comboShades[index], + borderRadius: const BorderRadius.all( + Radius.circular(3.0), + ), + ) + : null, + child: Text( + '${level}s', + style: TextStyle( + color: + isCurrentLevel + ? Theme.of(context).colorScheme.onSecondary + : null, + ), + ), + ); + }).toList(), + ), + ], ), - ], - ), - ), - const SizedBox(width: 10.0), - ], - ); - }, - ), + ), + const SizedBox(width: 10.0), + ], + ); + }, + ), ); } List generateShades(Color baseColor, bool light) { final shades = []; - final int r = baseColor.red; - final int g = baseColor.green; - final int b = baseColor.blue; + final double r = baseColor.r; + final double g = baseColor.g; + final double b = baseColor.b; const int step = 20; // Generate darker shades for (int i = 4; i >= 2; i = i - 2) { - final int newR = (r - i * step).clamp(0, 255); - final int newG = (g - i * step).clamp(0, 255); - final int newB = (b - i * step).clamp(0, 255); - shades.add(Color.fromARGB(baseColor.alpha, newR, newG, newB)); + final double newR = (r - i * step).clamp(0, 255); + final double newG = (g - i * step).clamp(0, 255); + final double newB = (b - i * step).clamp(0, 255); + shades.add(Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB)); } // Generate lighter shades for (int i = 2; i <= 3; i++) { - final int newR = (r + i * step).clamp(0, 255); - final int newG = (g + i * step).clamp(0, 255); - final int newB = (b + i * step).clamp(0, 255); - shades.add(Color.fromARGB(baseColor.alpha, newR, newG, newB)); + final double newR = (r + i * step).clamp(0, 255); + final double newG = (g + i * step).clamp(0, 255); + final double newB = (b + i * step).clamp(0, 255); + shades.add(Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB)); } if (light) { @@ -612,58 +528,46 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final stormState = ref.watch(ctrl); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (stormState.mode == StormMode.initial) - BottomBarButton( - icon: Icons.info_outline, - label: context.l10n.aboutX('Storm'), - showLabel: true, - onTap: () => _stormInfoDialogBuilder(context), - ), - BottomBarButton( - icon: Icons.delete, - label: context.l10n.stormNewRun.split('(').first.trimRight(), - showLabel: true, - onTap: () { - stormState.clock.reset(); - ref.invalidate(stormProvider); - }, - ), - if (stormState.mode == StormMode.running) - BottomBarButton( - icon: LichessIcons.flag, - label: context.l10n.stormEndRun.split('(').first.trimRight(), - showLabel: true, - onTap: stormState.puzzleIndex >= 1 - ? () { - if (stormState.clock.startAt != null) { - stormState.clock.sendEnd(); - } - } - : null, - ), - if (stormState.mode == StormMode.ended && - stormState.stats != null) - BottomBarButton( - icon: Icons.open_in_new, - label: 'Result', - showLabel: true, - onTap: () => _showStats(context, stormState.stats!), - ), - ], + return BottomBar( + children: [ + if (stormState.mode == StormMode.initial) + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Storm'), + showLabel: true, + onTap: () => _stormInfoDialogBuilder(context), ), + BottomBarButton( + icon: Icons.delete, + label: context.l10n.stormNewRun.split('(').first.trimRight(), + showLabel: true, + onTap: () { + stormState.clock.reset(); + ref.invalidate(stormProvider); + }, ), - ), + if (stormState.mode == StormMode.running) + BottomBarButton( + icon: LichessIcons.flag, + label: context.l10n.stormEndRun.split('(').first.trimRight(), + showLabel: true, + onTap: + stormState.puzzleIndex >= 1 + ? () { + if (stormState.clock.startAt != null) { + stormState.clock.sendEnd(); + } + } + : null, + ), + if (stormState.mode == StormMode.ended && stormState.stats != null) + BottomBarButton( + icon: Icons.open_in_new, + label: 'Result', + showLabel: true, + onTap: () => _showStats(context, stormState.stats!), + ), + ], ); } } @@ -674,28 +578,16 @@ class _RunStats extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - leading: CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(context.l10n.close), - ), - ), - child: _RunStatsPopup(stats), - ) - : Scaffold( - body: _RunStatsPopup(stats), - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ), - ); + return PlatformScaffold( + body: _RunStatsPopup(stats), + appBar: PlatformAppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + title: const SizedBox.shrink(), + ), + ); } } @@ -713,33 +605,24 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { @override Widget build(BuildContext context) { final puzzleList = widget.stats.historyFilter(filter); - final highScoreWidgets = widget.stats.newHigh != null - ? [ - const SizedBox(height: 16), - ListTile( - leading: Icon( - LichessIcons.storm, - size: 46, - color: context.lichessColors.brag, - ), - title: Text( - newHighTitle(context, widget.stats.newHigh!), - style: Styles.sectionTitle.copyWith( - color: context.lichessColors.brag, - ), - ), - subtitle: Text( - context.l10n.stormPreviousHighscoreWasX( - widget.stats.newHigh!.prev.toString(), + final highScoreWidgets = + widget.stats.newHigh != null + ? [ + const SizedBox(height: 16), + ListTile( + leading: Icon(LichessIcons.storm, size: 46, color: context.lichessColors.brag), + title: Text( + newHighTitle(context, widget.stats.newHigh!), + style: Styles.sectionTitle.copyWith(color: context.lichessColors.brag), ), - style: TextStyle( - color: context.lichessColors.brag, + subtitle: Text( + context.l10n.stormPreviousHighscoreWasX(widget.stats.newHigh!.prev.toString()), + style: TextStyle(color: context.lichessColors.brag), ), ), - ), - const SizedBox(height: 10), - ] - : null; + const SizedBox(height: 10), + ] + : null; return SafeArea( child: ListView( @@ -747,34 +630,20 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { if (highScoreWidgets != null) ...highScoreWidgets, ListSection( cupertinoAdditionalDividerMargin: 6, - header: Text( - '${widget.stats.score} ${context.l10n.stormPuzzlesSolved}', - ), + header: Text('${widget.stats.score} ${context.l10n.stormPuzzlesSolved}'), children: [ - _StatsRow( - context.l10n.stormMoves, - widget.stats.moves.toString(), - ), + _StatsRow(context.l10n.stormMoves, widget.stats.moves.toString()), _StatsRow( context.l10n.accuracy, '${(((widget.stats.moves - widget.stats.errors) / widget.stats.moves) * 100).toStringAsFixed(2)}%', ), - _StatsRow( - context.l10n.stormCombo, - widget.stats.comboBest.toString(), - ), - _StatsRow( - context.l10n.stormTime, - '${widget.stats.time.inSeconds}s', - ), + _StatsRow(context.l10n.stormCombo, widget.stats.comboBest.toString()), + _StatsRow(context.l10n.stormTime, '${widget.stats.time.inSeconds}s'), _StatsRow( context.l10n.stormTimePerMove, '${widget.stats.timePerMove.toStringAsFixed(1)}s', ), - _StatsRow( - context.l10n.stormHighestSolved, - widget.stats.highest.toString(), - ), + _StatsRow(context.l10n.stormHighestSolved, widget.stats.highest.toString()), ], ), const SizedBox(height: 10.0), @@ -797,23 +666,19 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { children: [ Row( children: [ - Text( - context.l10n.stormPuzzlesPlayed, - style: Styles.sectionTitle, - ), + Text(context.l10n.stormPuzzlesPlayed, style: Styles.sectionTitle), const Spacer(), Tooltip( excludeFromSemantics: true, message: context.l10n.stormFailedPuzzles, child: PlatformIconButton( semanticsLabel: context.l10n.stormFailedPuzzles, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.clear_fill - : Icons.close, - onTap: () => setState( - () => - filter = filter.copyWith(failed: !filter.failed), - ), + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.clear_fill + : Icons.close, + onTap: + () => setState(() => filter = filter.copyWith(failed: !filter.failed)), highlighted: filter.failed, ), ), @@ -822,12 +687,11 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { excludeFromSemantics: true, child: PlatformIconButton( semanticsLabel: context.l10n.stormSlowPuzzles, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.hourglass - : Icons.hourglass_bottom, - onTap: () => setState( - () => filter = filter.copyWith(slow: !filter.slow), - ), + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.hourglass + : Icons.hourglass_bottom, + onTap: () => setState(() => filter = filter.copyWith(slow: !filter.slow)), highlighted: filter.slow, ), ), @@ -837,10 +701,7 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { if (puzzleList.isNotEmpty) PuzzleHistoryPreview(puzzleList) else - Center( - child: - Text(context.l10n.mobilePuzzleStormFilterNothingToShow), - ), + Center(child: Text(context.l10n.mobilePuzzleStormFilterNothingToShow)), ], ), ), @@ -875,10 +736,7 @@ class _StatsRow extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - if (value != null) Text(value!), - ], + children: [Text(label), if (value != null) Text(value!)], ), ); } @@ -911,11 +769,10 @@ class _StormDashboardButton extends ConsumerWidget { return const SizedBox.shrink(); } - void _showDashboard(BuildContext context, AuthSessionState session) => - pushPlatformRoute( - context, - rootNavigator: true, - fullscreenDialog: true, - builder: (_) => StormDashboardModal(user: session.user), - ); + void _showDashboard(BuildContext context, AuthSessionState session) => pushPlatformRoute( + context, + rootNavigator: true, + fullscreenDialog: true, + builder: (_) => StormDashboardModal(user: session.user), + ); } diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 0851d0f223..3618d0ef64 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -7,16 +7,15 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_controller.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -24,8 +23,10 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -36,36 +37,13 @@ class StreakScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + return const WakelockWidget( + child: PlatformScaffold( + appBar: PlatformAppBar(actions: [ToggleSoundButton()], title: Text('Puzzle Streak')), + body: _Load(), ), ); } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: [ToggleSoundButton()], - title: const Text('Puzzle Streak'), - ), - body: const _Load(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: const Text('Puzzle Streak'), - trailing: ToggleSoundButton(), - ), - child: const _Load(), - ); - } } class _Load extends ConsumerWidget { @@ -84,29 +62,18 @@ class _Load extends ConsumerWidget { angle: const PuzzleTheme(PuzzleThemeKey.mix), userId: session?.user.id, ), - streak: PuzzleStreak( - streak: data.streak, - index: 0, - hasSkipped: false, - finished: false, - timestamp: data.timestamp, - ), + streak: data.streak, ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { - debugPrint( - 'SEVERE: [StreakScreen] could not load streak; $e\n$s', - ); + debugPrint('SEVERE: [StreakScreen] could not load streak; $e\n$s'); return Center( child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ); @@ -116,29 +83,28 @@ class _Load extends ConsumerWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.initialPuzzleContext, - required this.streak, - }); + const _Body({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final puzzleState = ref.watch(ctrlProvider); - ref.listen(ctrlProvider.select((s) => s.nextPuzzleStreakFetchError), - (_, shouldShowDialog) { + ref.listen(ctrlProvider.select((s) => s.nextPuzzleStreakFetchError), ( + _, + shouldShowDialog, + ) { if (shouldShowDialog) { showAdaptiveDialog( context: context, - builder: (context) => _RetryFetchPuzzleDialog( - initialPuzzleContext: initialPuzzleContext, - streak: streak, - ), + builder: + (context) => _RetryFetchPuzzleDialog( + initialPuzzleContext: initialPuzzleContext, + streak: streak, + ), ); } }); @@ -149,32 +115,32 @@ class _Body extends ConsumerWidget { child: Center( child: SafeArea( child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!); - }, - boardData: cg.BoardData( - orientation: puzzleState.pov.cg, - interactableSide: puzzleState.mode == PuzzleMode.load || - puzzleState.position.isGameOver - ? cg.InteractableSide.none - : puzzleState.mode == PuzzleMode.view - ? cg.InteractableSide.both + orientation: puzzleState.pov, + fen: puzzleState.fen, + lastMove: puzzleState.lastMove as NormalMove?, + gameData: GameData( + playerSide: + puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver + ? PlayerSide.none + : puzzleState.mode == PuzzleMode.view + ? PlayerSide.both : puzzleState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: puzzleState.fen, + ? PlayerSide.white + : PlayerSide.black, isCheck: puzzleState.position.isCheck, - lastMove: puzzleState.lastMove?.cg, - sideToMove: puzzleState.position.turn.cg, + sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, + promotionMove: puzzleState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, + onPromotionSelection: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelection(role); + }, ), topTable: Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - ), + padding: const EdgeInsets.symmetric(horizontal: 10.0), child: PuzzleFeedbackWidget( puzzle: puzzleState.puzzle, state: puzzleState, @@ -183,21 +149,13 @@ class _Body extends ConsumerWidget { ), ), bottomTable: Padding( - padding: const EdgeInsets.only( - top: 10.0, - left: 10.0, - right: 10.0, - ), + padding: const EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( children: [ - Icon( - LichessIcons.streak, - size: 40.0, - color: context.lichessColors.brag, - ), + Icon(LichessIcons.streak, size: 40.0, color: context.lichessColors.brag), const SizedBox(width: 8.0), Text( puzzleState.streak!.index.toString(), @@ -209,11 +167,7 @@ class _Body extends ConsumerWidget { ), ], ), - Text( - context.l10n.puzzleRatingX( - puzzleState.puzzle.puzzle.rating.toString(), - ), - ), + Text(context.l10n.puzzleRatingX(puzzleState.puzzle.puzzle.rating.toString())), ], ), ), @@ -221,10 +175,7 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - initialPuzzleContext: initialPuzzleContext, - streak: streak, - ), + _BottomBar(initialPuzzleContext: initialPuzzleContext, streak: streak), ], ); @@ -237,15 +188,13 @@ class _Body extends ConsumerWidget { final NavigatorState navigator = Navigator.of(context); final shouldPop = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: Text(context.l10n.mobilePuzzleStreakAbortWarning), - onYes: () { - ref.read(ctrlProvider.notifier).sendStreakResult(); - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: const Text('No worries, your score will be saved locally.'), + onYes: () => Navigator.of(context).pop(true), + onNo: () => Navigator.of(context).pop(false), + ), ); if (shouldPop ?? false) { navigator.pop(); @@ -257,161 +206,118 @@ class _Body extends ConsumerWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.initialPuzzleContext, - required this.streak, - }); + const _BottomBar({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final puzzleState = ref.watch(ctrlProvider); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (!puzzleState.streak!.finished) - BottomBarButton( - icon: Icons.info_outline, - label: context.l10n.aboutX('Streak'), - showLabel: true, - onTap: () => _streakInfoDialogBuilder(context), - ), - if (!puzzleState.streak!.finished) - BottomBarButton( - icon: Icons.skip_next, - label: context.l10n.skipThisMove, - showLabel: true, - onTap: puzzleState.streak!.hasSkipped || - puzzleState.mode == PuzzleMode.view - ? null - : () => ref.read(ctrlProvider.notifier).skipMove(), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: () { - launchShareDialog( - context, - text: lichessUri( - '/training/${puzzleState.puzzle.puzzle.id}', - ).toString(), - ); - }, - label: 'Share this puzzle', - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.analysis, - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, - initialMoveCursor: 0, - ), + return BottomBar( + children: [ + if (!puzzleState.streak!.finished) + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Streak'), + showLabel: true, + onTap: () => _streakInfoDialogBuilder(context), + ), + if (!puzzleState.streak!.finished) + BottomBarButton( + icon: Icons.skip_next, + label: context.l10n.skipThisMove, + showLabel: true, + onTap: + puzzleState.streak!.hasSkipped || puzzleState.mode == PuzzleMode.view + ? null + : () => ref.read(ctrlProvider.notifier).skipMove(), + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: () { + launchShareDialog( + context, + text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}').toString(), + ); + }, + label: 'Share this puzzle', + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: puzzleState.pov, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isComputerAnalysisAllowed: true, + variant: Variant.standard, ), - ); - }, - label: context.l10n.analysis, - icon: Icons.biotech, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: puzzleState.canGoBack - ? () => ref.read(ctrlProvider.notifier).userPrevious() - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: puzzleState.canGoNext - ? () => ref.read(ctrlProvider.notifier).userNext() - : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: ref.read(streakProvider).isLoading == false - ? () => ref.invalidate(streakProvider) - : null, - highlighted: true, - label: context.l10n.puzzleNewStreak, - icon: CupertinoIcons.play_arrow_solid, - ), - ), - ], + initialMoveCursor: 0, + ), + ), + ); + }, + label: context.l10n.analysis, + icon: Icons.biotech, ), - ), - ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: + puzzleState.canGoBack ? () => ref.read(ctrlProvider.notifier).userPrevious() : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: puzzleState.canGoNext ? () => ref.read(ctrlProvider.notifier).userNext() : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: + ref.read(streakProvider).isLoading == false + ? () => ref.invalidate(streakProvider) + : null, + highlighted: true, + label: context.l10n.puzzleNewStreak, + icon: CupertinoIcons.play_arrow_solid, + ), + ], ); } Future _streakInfoDialogBuilder(BuildContext context) { return showAdaptiveDialog( context: context, - builder: (context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoAlertDialog( - title: Text(context.l10n.aboutX('Puzzle Streak')), - content: Text(context.l10n.puzzleStreakDescription), - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ) - : AlertDialog( - title: Text(context.l10n.aboutX('Puzzle Streak')), - content: Text(context.l10n.puzzleStreakDescription), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ); - }, + builder: + (context) => PlatformAlertDialog( + title: Text(context.l10n.aboutX('Puzzle Streak')), + content: Text(context.l10n.puzzleStreakDescription), + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ), ); } } class _RetryFetchPuzzleDialog extends ConsumerWidget { - const _RetryFetchPuzzleDialog({ - required this.initialPuzzleContext, - required this.streak, - }); + const _RetryFetchPuzzleDialog({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @@ -421,8 +327,7 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final state = ref.watch(ctrlProvider); Future retryStreakNext() async { @@ -435,51 +340,34 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { Navigator.of(context).pop(); } if (data != null) { - ref.read(ctrlProvider.notifier).loadPuzzle( + ref + .read(ctrlProvider.notifier) + .onLoadPuzzle( data, - nextStreak: - state.streak!.copyWith(index: state.streak!.index + 1), + nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); } }, ); } - final canRetry = state.nextPuzzleStreakFetchError && - !state.nextPuzzleStreakFetchIsRetrying; + final canRetry = state.nextPuzzleStreakFetchError && !state.nextPuzzleStreakFetchIsRetrying; - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - title: const Text(title), - content: const Text(content), - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - isDestructiveAction: true, - child: Text(context.l10n.cancel), - ), - CupertinoDialogAction( - onPressed: canRetry ? retryStreakNext : null, - isDefaultAction: true, - child: Text(context.l10n.retry), - ), - ], - ); - } else { - return AlertDialog( - title: const Text(title), - content: const Text(content), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.cancel), - ), - TextButton( - onPressed: canRetry ? retryStreakNext : null, - child: Text(context.l10n.retry), - ), - ], - ); - } + return PlatformAlertDialog( + title: const Text(title), + content: const Text(content), + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + cupertinoIsDestructiveAction: true, + child: Text(context.l10n.cancel), + ), + PlatformDialogAction( + onPressed: canRetry ? retryStreakNext : null, + cupertinoIsDefaultAction: true, + child: Text(context.l10n.retry), + ), + ], + ); } } diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index 746e66c181..8504b58e28 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -1,53 +1,37 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository_providers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'following_screen.g.dart'; - -@riverpod -Future<(IList, IList)> _getFollowingAndOnlines( - _GetFollowingAndOnlinesRef ref, -) async { - final following = await ref.watch(followingProvider.future); - final onlines = await ref.watch(onlineFriendsProvider.future); - return (following, onlines); -} +final _getFollowingAndOnlinesProvider = FutureProvider.autoDispose<(IList, IList)>( + (ref) async { + final following = await ref.watch(followingProvider.future); + final onlines = await ref.watch(onlineFriendsProvider.future); + return (following, onlines); + }, +); class FollowingScreen extends StatelessWidget { const FollowingScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.friends), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.friends)), body: const _Body(), ); } @@ -58,9 +42,7 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final followingAndOnlines = ref.watch( - _getFollowingAndOnlinesProvider, - ); + final followingAndOnlines = ref.watch(_getFollowingAndOnlinesProvider); return followingAndOnlines.when( data: (data) { @@ -68,40 +50,36 @@ class _Body extends ConsumerWidget { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { if (following.isEmpty) { - return Center( - child: Text(context.l10n.mobileNotFollowingAnyUser), - ); + return Center(child: Text(context.l10n.mobileNotFollowingAnyUser)); } return SafeArea( child: ColoredBox( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemBackground.resolveFrom(context) - : Colors.transparent, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemBackground.resolveFrom(context) + : Colors.transparent, child: ListView.separated( itemCount: following.length, - separatorBuilder: (context, index) => const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ), + separatorBuilder: + (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final user = following[index]; return Slidable( + dragStartBehavior: DragStartBehavior.start, endActionPane: ActionPane( - motion: const ScrollMotion(), + motion: const StretchMotion(), extentRatio: 0.3, children: [ SlidableAction( onPressed: (BuildContext context) async { final oldState = following; setState(() { - following = following.removeWhere( - (v) => v.id == user.id, - ); + following = following.removeWhere((v) => v.id == user.id); }); try { await ref.withClient( - (client) => RelationRepository(client) - .unfollow(user.id), + (client) => RelationRepository(client).unfollow(user.id), ); } catch (_) { setState(() { @@ -120,14 +98,13 @@ class _Body extends ConsumerWidget { child: UserListTile.fromUser( user, _isOnline(user, data.$2), - onTap: () => { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: user.lightUser, - ), - ), - }, + onTap: + () => { + pushPlatformRoute( + context, + builder: (context) => UserScreen(user: user.lightUser), + ), + }, ), ); }, @@ -138,12 +115,8 @@ class _Body extends ConsumerWidget { ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [FollowingScreen] could not load following users; $error\n$stackTrace', - ); - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(followingProvider), - ); + debugPrint('SEVERE: [FollowingScreen] could not load following users; $error\n$stackTrace'); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(followingProvider)); }, loading: () => const CenterLoadingIndicator(), ); diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 154b118107..10031c8be6 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -7,18 +7,17 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AccountPreferencesScreen extends ConsumerStatefulWidget { const AccountPreferencesScreen({super.key}); @override - ConsumerState createState() => - _AccountPreferencesScreenState(); + ConsumerState createState() => _AccountPreferencesScreenState(); } -class _AccountPreferencesScreenState - extends ConsumerState { +class _AccountPreferencesScreenState extends ConsumerState { bool isLoading = false; Future _setPref(Future Function() f) async { @@ -41,402 +40,363 @@ class _AccountPreferencesScreenState final content = accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } - return SafeArea( - child: ListView( - children: [ - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesDisplay), - hasLeading: false, - children: [ - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesZenMode, - ), - settingsValue: data.zenMode.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Zen.values, - selectedItem: data.zenMode, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Zen? value) { + return ListView( + children: [ + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesDisplay), + hasLeading: false, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesZenMode), + settingsValue: data.zenMode.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Zen.values, + selectedItem: data.zenMode, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (Zen? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) + .read(accountPreferencesProvider.notifier) .setZen(value ?? data.zenMode), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesZenMode, - builder: (context) => const ZenSettingsScreen(), - ); - } - }, - ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPgnPieceNotation, - ), - settingsValue: data.pieceNotation.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: PieceNotation.values, - selectedItem: data.pieceNotation, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (PieceNotation? value) { + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesZenMode, + builder: (context) => const ZenSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesPgnPieceNotation), + settingsValue: data.pieceNotation.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: PieceNotation.values, + selectedItem: data.pieceNotation, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (PieceNotation? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setPieceNotation( - value ?? data.pieceNotation, - ), + .read(accountPreferencesProvider.notifier) + .setPieceNotation(value ?? data.pieceNotation), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context - .l10n.preferencesPromoteToQueenAutomatically, - builder: (context) => - const PieceNotationSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - title: Text(context.l10n.preferencesShowPlayerRatings), - subtitle: Text( - context.l10n.preferencesExplainShowPlayerRatings, - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: data.showRatings.value, - onChanged: isLoading - ? null - : (value) { + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesPgnPieceNotation, + builder: (context) => const PieceNotationSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesShowPlayerRatings), + subtitle: Text(context.l10n.preferencesExplainShowPlayerRatings, maxLines: 5), + value: data.showRatings.value, + onChanged: + isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) .setShowRatings(BooleanPref(value)), ); }, - ), - ], - ), - ListSection( - header: - SettingsSectionTitle(context.l10n.preferencesGameBehavior), - hasLeading: false, - children: [ - SwitchSettingTile( - title: Text( - context.l10n.preferencesPremovesPlayingDuringOpponentTurn, - ), - value: data.premove.value, - onChanged: isLoading - ? null - : (value) { + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), + hasLeading: false, + children: [ + SwitchSettingTile( + title: Text(context.l10n.preferencesPremovesPlayingDuringOpponentTurn), + value: data.premove.value, + onChanged: + isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) .setPremove(BooleanPref(value)), ); }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesConfirmResignationAndDrawOffers, - ), - value: data.confirmResign.value, - onChanged: isLoading - ? null - : (value) { + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesConfirmResignationAndDrawOffers), + value: data.confirmResign.value, + onChanged: + isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) .setConfirmResign(BooleanPref(value)), ); }, - ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesTakebacksWithOpponentApproval, - ), - settingsValue: data.takeback.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Takeback.values, - selectedItem: data.takeback, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Takeback? value) { + ), + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesTakebacksWithOpponentApproval), + settingsValue: data.takeback.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Takeback.values, + selectedItem: data.takeback, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (Takeback? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) + .read(accountPreferencesProvider.notifier) .setTakeback(value ?? data.takeback), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context - .l10n.preferencesTakebacksWithOpponentApproval, - builder: (context) => const TakebackSettingsScreen(), - ); - } - }, - ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPromoteToQueenAutomatically, - ), - settingsValue: data.autoQueen.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: AutoQueen.values, - selectedItem: data.autoQueen, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoQueen? value) { + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesTakebacksWithOpponentApproval, + builder: (context) => const TakebackSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesPromoteToQueenAutomatically), + settingsValue: data.autoQueen.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: AutoQueen.values, + selectedItem: data.autoQueen, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (AutoQueen? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) + .read(accountPreferencesProvider.notifier) .setAutoQueen(value ?? data.autoQueen), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context - .l10n.preferencesPromoteToQueenAutomatically, - builder: (context) => const AutoQueenSettingsScreen(), - ); - } - }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesPromoteToQueenAutomatically, + builder: (context) => const AutoQueenSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesClaimDrawOnThreefoldRepetitionAutomatically, ), - SettingsListTile( - settingsLabel: Text( - context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, - ), - settingsValue: data.autoThreefold.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: AutoThreefold.values, - selectedItem: data.autoThreefold, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoThreefold? value) { + settingsValue: data.autoThreefold.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: AutoThreefold.values, + selectedItem: data.autoThreefold, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (AutoThreefold? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setAutoThreefold( - value ?? data.autoThreefold, - ), + .read(accountPreferencesProvider.notifier) + .setAutoThreefold(value ?? data.autoThreefold), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, - builder: (context) => - const AutoThreefoldSettingsScreen(), + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesClaimDrawOnThreefoldRepetitionAutomatically, + builder: (context) => const AutoThreefoldSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesMoveConfirmation), + settingsValue: data.submitMove.label(context), + showCupertinoTrailingValue: false, + onTap: () { + showMultipleChoicesPicker( + context, + choices: SubmitMoveChoice.values, + selectedItems: data.submitMove.choices, + labelBuilder: (t) => Text(t.label(context)), + ).then((value) { + if (value != null) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setSubmitMove(SubmitMove(value)), ); } - }, - ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesMoveConfirmation, - ), - settingsValue: data.submitMove.label(context), - showCupertinoTrailingValue: false, - onTap: () { - showMultipleChoicesPicker( + }); + }, + explanation: context.l10n.preferencesExplainCanThenBeTemporarilyDisabled, + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesChessClock), + hasLeading: false, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesGiveMoreTime), + settingsValue: data.moretime.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( context, - choices: SubmitMoveChoice.values, - selectedItems: data.submitMove.choices, + choices: Moretime.values, + selectedItem: data.moretime, labelBuilder: (t) => Text(t.label(context)), - ).then((value) { - if (value != null) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setSubmitMove(SubmitMove(value)), - ); - } - }); - }, - explanation: context - .l10n.preferencesExplainCanThenBeTemporarilyDisabled, - ), - ], - ), - ListSection( - header: - SettingsSectionTitle(context.l10n.preferencesChessClock), - hasLeading: false, - children: [ - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesGiveMoreTime, - ), - settingsValue: data.moretime.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Moretime.values, - selectedItem: data.moretime, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Moretime? value) { + onSelectedItemChanged: + isLoading + ? null + : (Moretime? value) { _setPref( () => ref - .read( - accountPreferencesProvider.notifier, - ) + .read(accountPreferencesProvider.notifier) .setMoretime(value ?? data.moretime), ); }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesGiveMoreTime, - builder: (context) => const MoretimeSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - title: - Text(context.l10n.preferencesSoundWhenTimeGetsCritical), - value: data.clockSound.value, - onChanged: isLoading - ? null - : (value) { + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesGiveMoreTime, + builder: (context) => const MoretimeSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesSoundWhenTimeGetsCritical), + value: data.clockSound.value, + onChanged: + isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) .setClockSound(BooleanPref(value)), ); }, - ), - ], - ), - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesPrivacy), - hasLeading: false, - children: [ - SwitchSettingTile( - title: Text( - context.l10n.letOtherPlayersFollowYou, - ), - value: data.follow.value, - onChanged: isLoading - ? null - : (value) { + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesPrivacy), + hasLeading: false, + children: [ + SwitchSettingTile( + title: Text(context.l10n.letOtherPlayersFollowYou), + value: data.follow.value, + onChanged: + isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) .setFollow(BooleanPref(value)), ); }, - ), - ], - ), - ], - ), + ), + SettingsListTile( + settingsLabel: Text(context.l10n.letOtherPlayersChallengeYou), + settingsValue: data.challenge.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Challenge.values, + selectedItem: data.challenge, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (Challenge? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setChallenge(value ?? data.challenge), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.letOtherPlayersChallengeYou, + builder: (context) => const _ChallengeSettingsScreen(), + ); + } + }, + ), + ], + ), + ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountPreferencesProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(accountPreferencesProvider)); }, ); - return Theme.of(context).platform == TargetPlatform.android - ? Scaffold( - appBar: AppBar( - title: Text(context.l10n.preferencesPreferences), - actions: [ - if (isLoading) - const Padding( - padding: EdgeInsets.only(right: 16), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), - ), - ], - ), - body: content, - ) - : CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, - ), - child: content, - ); + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.preferencesPreferences), + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], + ), + body: content, + ); } } @@ -456,15 +416,12 @@ class _ZenSettingsScreenState extends ConsumerState { return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, + trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, ), child: SafeArea( child: ListView( @@ -473,22 +430,23 @@ class _ZenSettingsScreenState extends ConsumerState { choices: Zen.values, selectedItem: data.zenMode, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Zen? v) async { - setState(() { - isLoading = true; - }); - try { - await ref - .read(accountPreferencesProvider.notifier) - .setZen(v ?? data.zenMode); - } finally { + onSelectedItemChanged: + isLoading + ? null + : (Zen? v) async { setState(() { - isLoading = false; + isLoading = true; }); - } - }, + try { + await ref + .read(accountPreferencesProvider.notifier) + .setZen(v ?? data.zenMode); + } finally { + setState(() { + isLoading = false; + }); + } + }, ), ], ), @@ -505,12 +463,10 @@ class PieceNotationSettingsScreen extends ConsumerStatefulWidget { const PieceNotationSettingsScreen({super.key}); @override - ConsumerState createState() => - _PieceNotationSettingsScreenState(); + ConsumerState createState() => _PieceNotationSettingsScreenState(); } -class _PieceNotationSettingsScreenState - extends ConsumerState { +class _PieceNotationSettingsScreenState extends ConsumerState { Future? _pendingSetPieceNotation; @override @@ -519,9 +475,7 @@ class _PieceNotationSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -529,9 +483,10 @@ class _PieceNotationSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -540,17 +495,17 @@ class _PieceNotationSettingsScreenState choices: PieceNotation.values, selectedItem: data.pieceNotation, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (PieceNotation? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setPieceNotation(v ?? data.pieceNotation); - setState(() { - _pendingSetPieceNotation = future; - }); - }, + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (PieceNotation? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setPieceNotation(v ?? data.pieceNotation); + setState(() { + _pendingSetPieceNotation = future; + }); + }, ), ], ), @@ -569,12 +524,10 @@ class TakebackSettingsScreen extends ConsumerStatefulWidget { const TakebackSettingsScreen({super.key}); @override - ConsumerState createState() => - _TakebackSettingsScreenState(); + ConsumerState createState() => _TakebackSettingsScreenState(); } -class _TakebackSettingsScreenState - extends ConsumerState { +class _TakebackSettingsScreenState extends ConsumerState { bool isLoading = false; @override @@ -583,15 +536,12 @@ class _TakebackSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, + trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, ), child: SafeArea( child: ListView( @@ -600,22 +550,23 @@ class _TakebackSettingsScreenState choices: Takeback.values, selectedItem: data.takeback, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Takeback? v) async { - setState(() { - isLoading = true; - }); - try { - await ref - .read(accountPreferencesProvider.notifier) - .setTakeback(v ?? data.takeback); - } finally { + onSelectedItemChanged: + isLoading + ? null + : (Takeback? v) async { setState(() { - isLoading = false; + isLoading = true; }); - } - }, + try { + await ref + .read(accountPreferencesProvider.notifier) + .setTakeback(v ?? data.takeback); + } finally { + setState(() { + isLoading = false; + }); + } + }, ), ], ), @@ -632,12 +583,10 @@ class AutoQueenSettingsScreen extends ConsumerStatefulWidget { const AutoQueenSettingsScreen({super.key}); @override - ConsumerState createState() => - _AutoQueenSettingsScreenState(); + ConsumerState createState() => _AutoQueenSettingsScreenState(); } -class _AutoQueenSettingsScreenState - extends ConsumerState { +class _AutoQueenSettingsScreenState extends ConsumerState { Future? _pendingSetAutoQueen; @override @@ -646,9 +595,7 @@ class _AutoQueenSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -656,9 +603,10 @@ class _AutoQueenSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -671,13 +619,13 @@ class _AutoQueenSettingsScreenState snapshot.connectionState == ConnectionState.waiting ? null : (AutoQueen? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setAutoQueen(v ?? data.autoQueen); - setState(() { - _pendingSetAutoQueen = future; - }); - }, + final future = ref + .read(accountPreferencesProvider.notifier) + .setAutoQueen(v ?? data.autoQueen); + setState(() { + _pendingSetAutoQueen = future; + }); + }, ), ], ), @@ -696,12 +644,10 @@ class AutoThreefoldSettingsScreen extends ConsumerStatefulWidget { const AutoThreefoldSettingsScreen({super.key}); @override - ConsumerState createState() => - _AutoThreefoldSettingsScreenState(); + ConsumerState createState() => _AutoThreefoldSettingsScreenState(); } -class _AutoThreefoldSettingsScreenState - extends ConsumerState { +class _AutoThreefoldSettingsScreenState extends ConsumerState { Future? _pendingSetAutoThreefold; @override @@ -710,9 +656,7 @@ class _AutoThreefoldSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -720,9 +664,10 @@ class _AutoThreefoldSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -731,17 +676,17 @@ class _AutoThreefoldSettingsScreenState choices: AutoThreefold.values, selectedItem: data.autoThreefold, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (AutoThreefold? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setAutoThreefold(v ?? data.autoThreefold); - setState(() { - _pendingSetAutoThreefold = future; - }); - }, + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (AutoThreefold? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setAutoThreefold(v ?? data.autoThreefold); + setState(() { + _pendingSetAutoThreefold = future; + }); + }, ), ], ), @@ -760,12 +705,10 @@ class MoretimeSettingsScreen extends ConsumerStatefulWidget { const MoretimeSettingsScreen({super.key}); @override - ConsumerState createState() => - _MoretimeSettingsScreenState(); + ConsumerState createState() => _MoretimeSettingsScreenState(); } -class _MoretimeSettingsScreenState - extends ConsumerState { +class _MoretimeSettingsScreenState extends ConsumerState { Future? _pendingSetMoretime; @override @@ -774,9 +717,7 @@ class _MoretimeSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -784,9 +725,10 @@ class _MoretimeSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -795,16 +737,77 @@ class _MoretimeSettingsScreenState choices: Moretime.values, selectedItem: data.moretime, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (Moretime? v) { - setState(() { - _pendingSetMoretime = ref + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (Moretime? v) { + setState(() { + _pendingSetMoretime = ref + .read(accountPreferencesProvider.notifier) + .setMoretime(v ?? data.moretime); + }); + }, + ), + ], + ), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text(err.toString())), + ); + } +} + +class _ChallengeSettingsScreen extends ConsumerStatefulWidget { + const _ChallengeSettingsScreen(); + + @override + ConsumerState<_ChallengeSettingsScreen> createState() => _ChallengeSettingsScreenState(); +} + +class _ChallengeSettingsScreenState extends ConsumerState<_ChallengeSettingsScreen> { + Future? _pendingSetChallenge; + + @override + Widget build(BuildContext context) { + final accountPrefs = ref.watch(accountPreferencesProvider); + return accountPrefs.when( + data: (data) { + if (data == null) { + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); + } + + return FutureBuilder( + future: _pendingSetChallenge, + builder: (context, snapshot) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, + ), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: Challenge.values, + selectedItem: data.challenge, + titleBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (Challenge? v) { + final future = ref .read(accountPreferencesProvider.notifier) - .setMoretime(v ?? data.moretime); - }); - }, + .setChallenge(v ?? data.challenge); + setState(() { + _pendingSetChallenge = future; + }); + }, ), ], ), diff --git a/lib/src/view/settings/app_background_mode_screen.dart b/lib/src/view/settings/app_background_mode_screen.dart index dc551a2be7..60c96a8879 100644 --- a/lib/src/view/settings/app_background_mode_screen.dart +++ b/lib/src/view/settings/app_background_mode_screen.dart @@ -11,33 +11,24 @@ class AppBackgroundModeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.background)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.background)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } - static String themeTitle(BuildContext context, ThemeMode theme) { + static String themeTitle(BuildContext context, BackgroundThemeMode theme) { switch (theme) { - case ThemeMode.system: + case BackgroundThemeMode.system: return context.l10n.deviceTheme; - case ThemeMode.dark: + case BackgroundThemeMode.dark: return context.l10n.dark; - case ThemeMode.light: + case BackgroundThemeMode.light: return context.l10n.light; } } @@ -46,22 +37,19 @@ class AppBackgroundModeScreen extends StatelessWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch( - generalPreferencesProvider.select((state) => state.themeMode), - ); + final themeMode = ref.watch(generalPreferencesProvider.select((state) => state.themeMode)); - void onChanged(ThemeMode? value) => ref + void onChanged(BackgroundThemeMode? value) => ref .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? ThemeMode.system); + .setBackgroundThemeMode(value ?? BackgroundThemeMode.system); return SafeArea( child: ListView( children: [ ChoicePicker( - choices: ThemeMode.values, + choices: BackgroundThemeMode.values, selectedItem: themeMode, - titleBuilder: (t) => - Text(AppBackgroundModeScreen.themeTitle(context, t)), + titleBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), onSelectedItemChanged: onChanged, ), ], diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index eefd82f4f2..5df25681e1 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -2,12 +2,13 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/settings/piece_shift_method_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -18,24 +19,18 @@ class BoardSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(context.l10n.board)), + appBar: AppBar(title: Text(context.l10n.preferencesGameBehavior)), body: const _Body(), ); } Widget _iosBuilder(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); + return const CupertinoPageScaffold(navigationBar: CupertinoNavigationBar(), child: _Body()); } } @@ -48,157 +43,329 @@ class _Body extends ConsumerWidget { final androidVersionAsync = ref.watch(androidVersionProvider); - return SafeArea( - child: ListView( - children: [ - ListSection( - hasLeading: false, - showDivider: false, - children: [ - SettingsListTile( - settingsLabel: Text(context.l10n.preferencesHowDoYouMovePieces), - settingsValue: - pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: PieceShiftMethod.values, - selectedItem: boardPrefs.pieceShiftMethod, - labelBuilder: (t) => - Text(pieceShiftMethodl10n(context, t)), - onSelectedItemChanged: (PieceShiftMethod? value) { - ref - .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod( - value ?? PieceShiftMethod.either, - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesHowDoYouMovePieces, - builder: (context) => - const PieceShiftMethodSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers on game and puzzle boards.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleHapticFeedback(); - }, + return ListView( + children: [ + ListSection( + hasLeading: false, + showDivider: false, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesHowDoYouMovePieces), + settingsValue: pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: PieceShiftMethod.values, + selectedItem: boardPrefs.pieceShiftMethod, + labelBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), + onSelectedItemChanged: (PieceShiftMethod? value) { + ref + .read(boardPreferencesProvider.notifier) + .setPieceShiftMethod(value ?? PieceShiftMethod.either); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesHowDoYouMovePieces, + builder: (context) => const PieceShiftMethodSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), + value: boardPrefs.magnifyDraggedPiece, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleMagnifyDraggedPiece(); + }, + ), + SettingsListTile( + // TODO translate + settingsLabel: const Text('Drag target'), + explanation: + // TODO translate + 'How the target square is highlighted when dragging a piece.', + settingsValue: dragTargetKindLabel(boardPrefs.dragTargetKind), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: DragTargetKind.values, + selectedItem: boardPrefs.dragTargetKind, + labelBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: (DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind(value ?? DragTargetKind.circle); + }, + ); + } else { + pushPlatformRoute( + context, + title: 'Dragged piece target', + builder: (context) => const DragTargetKindSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + // TODO translate + title: const Text('Touch feedback'), + value: boardPrefs.hapticFeedback, + subtitle: const Text( + // TODO translate + 'Vibrate when moving pieces or capturing them.', + maxLines: 5, ), - if (Theme.of(context).platform == TargetPlatform.android && - !isTabletOrLarger(context)) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 29 - ? SwitchSettingTile( - title: Text(context.l10n.mobileSettingsImmersiveMode), - subtitle: Text( - context.l10n.mobileSettingsImmersiveModeSubtitle, - textAlign: TextAlign.justify, - maxLines: 5, - ), - value: boardPrefs.immersiveModeWhilePlaying ?? false, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleImmersiveModeWhilePlaying(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceDestinations, - ), - value: boardPrefs.showLegalMoves, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowLegalMoves(); - }, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesPieceAnimation), + value: boardPrefs.pieceAnimation, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); + }, + ), + if (Theme.of(context).platform == TargetPlatform.android && !isTabletOrLarger(context)) + androidVersionAsync.maybeWhen( + data: + (version) => + version != null && version.sdkInt >= 29 + ? SwitchSettingTile( + title: Text(context.l10n.mobileSettingsImmersiveMode), + subtitle: Text( + context.l10n.mobileSettingsImmersiveModeSubtitle, + maxLines: 5, + ), + value: boardPrefs.immersiveModeWhilePlaying ?? false, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleImmersiveModeWhilePlaying(); + }, + ) + : const SizedBox.shrink(), + orElse: () => const SizedBox.shrink(), ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesBoardHighlights, - ), - value: boardPrefs.boardHighlights, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleBoardHighlights(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesBoardCoordinates, - ), - value: boardPrefs.coordinates, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleCoordinates(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), - value: boardPrefs.magnifyDraggedPiece, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleMagnifyDraggedPiece(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, - ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, + SettingsListTile( + //TODO Add l10n + settingsLabel: const Text('Clock position'), + settingsValue: boardPrefs.clockPosition.label, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: + (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesPieceDestinations), + value: boardPrefs.showLegalMoves, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleShowLegalMoves(); + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesBoardHighlights), + value: boardPrefs.boardHighlights, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBoardHighlights(); + }, + ), + SettingsListTile( + settingsLabel: const Text('Material'), //TODO: l10n + settingsValue: boardPrefs.materialDifferenceFormat.l10n(AppLocalizations.of(context)), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: MaterialDifferenceFormat.values, + selectedItem: boardPrefs.materialDifferenceFormat, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: + (MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? MaterialDifferenceFormat.materialDifference, + ), + ); + } else { + pushPlatformRoute( + context, + title: 'Material', + builder: (context) => const MaterialDifferenceFormatScreen(), + ); + } + }, + ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, ), - ], + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleEnableShapeDrawings(); + }, + ), + ], + ), + ], + ); + } +} + +class PieceShiftMethodSettingsScreen extends ConsumerWidget { + const PieceShiftMethodSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pieceShiftMethod = ref.watch( + boardPreferencesProvider.select((state) => state.pieceShiftMethod), + ); + + void onChanged(PieceShiftMethod? value) { + ref + .read(boardPreferencesProvider.notifier) + .setPieceShiftMethod(value ?? PieceShiftMethod.either); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + notchedTile: true, + choices: PieceShiftMethod.values, + selectedItem: pieceShiftMethod, + titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +class BoardClockPositionScreen extends ConsumerWidget { + const BoardClockPositionScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clockPosition = ref.watch( + boardPreferencesProvider.select((state) => state.clockPosition), + ); + void onChanged(ClockPosition? value) => + ref.read(boardPreferencesProvider.notifier).setClockPosition(value ?? ClockPosition.right); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: ClockPosition.values, + selectedItem: clockPosition, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +class MaterialDifferenceFormatScreen extends ConsumerWidget { + const MaterialDifferenceFormatScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final materialDifferenceFormat = ref.watch( + boardPreferencesProvider.select((state) => state.materialDifferenceFormat), + ); + void onChanged(MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat(value ?? MaterialDifferenceFormat.materialDifference); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: ListView( + children: [ + ChoicePicker( + choices: MaterialDifferenceFormat.values, + selectedItem: materialDifferenceFormat, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, ), ], ), ); } } + +class DragTargetKindSettingsScreen extends ConsumerWidget { + const DragTargetKindSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dragTargetKind = ref.watch( + boardPreferencesProvider.select((state) => state.dragTargetKind), + ); + + void onChanged(DragTargetKind? value) { + ref.read(boardPreferencesProvider.notifier).setDragTargetKind(value ?? DragTargetKind.circle); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + Padding( + padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: const Text('How the target square is highlighted when dragging a piece.'), + ), + ChoicePicker( + notchedTile: true, + choices: DragTargetKind.values, + selectedItem: dragTargetKind, + titleBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +String pieceShiftMethodl10n(BuildContext context, PieceShiftMethod pieceShiftMethod) => + switch (pieceShiftMethod) { + // TODO add this to mobile translations + PieceShiftMethod.either => 'Either tap or drag', + PieceShiftMethod.drag => context.l10n.preferencesDragPiece, + PieceShiftMethod.tapTwoSquares => 'Tap two squares', + }; diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index c005919905..902f6e98a3 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -2,9 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -13,60 +12,38 @@ class BoardThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.board)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.board)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } } class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boardTheme = - ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); + final boardTheme = ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); - final hasSystemColors = - ref.watch(generalPreferencesProvider.select((p) => p.systemColors)); + final hasSystemColors = getCorePalette() != null; - final androidVersion = ref.watch(androidVersionProvider).whenOrNull( - data: (v) => v, - ); + final choices = + BoardTheme.values.where((t) => t != BoardTheme.system || hasSystemColors).toList(); - final choices = BoardTheme.values - .where( - (t) => - t != BoardTheme.system || - (hasSystemColors && - androidVersion != null && - androidVersion.sdkInt >= 31), - ) - .toList(); + void onChanged(BoardTheme? value) => + ref.read(boardPreferencesProvider.notifier).setBoardTheme(value ?? BoardTheme.brown); - void onChanged(BoardTheme? value) => ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(value ?? BoardTheme.brown); - - final checkedIcon = Theme.of(context).platform == TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoTheme.of(context).primaryColor, - ); + final checkedIcon = + Theme.of(context).platform == TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoTheme.of(context).primaryColor, + ); return SafeArea( child: ListView.separated( @@ -80,15 +57,13 @@ class _Body extends ConsumerWidget { onTap: () => onChanged(t), ); }, - separatorBuilder: (_, __) => PlatformDivider( - height: 1, - // on iOS: 14 (default indent) + 16 (padding) - indent: - Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Colors.transparent, - ), + separatorBuilder: + (_, __) => PlatformDivider( + height: 1, + // on iOS: 14 (default indent) + 16 (padding) + indent: Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, + color: Theme.of(context).platform == TargetPlatform.iOS ? null : Colors.transparent, + ), itemCount: choices.length, ), ); diff --git a/lib/src/view/settings/piece_set_screen.dart b/lib/src/view/settings/piece_set_screen.dart index efb1cf2234..a28a3163e9 100644 --- a/lib/src/view/settings/piece_set_screen.dart +++ b/lib/src/view/settings/piece_set_screen.dart @@ -1,106 +1,101 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class PieceSetScreen extends StatelessWidget { +class PieceSetScreen extends ConsumerStatefulWidget { const PieceSetScreen({super.key}); @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } + ConsumerState createState() => _PieceSetScreenState(); +} - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.pieceSet)), - body: _Body(), - ); +class _PieceSetScreenState extends ConsumerState { + bool isLoading = false; + + Future onChanged(PieceSet? value) async { + if (value != null) { + ref.read(boardPreferencesProvider.notifier).setPieceSet(value); + setState(() { + isLoading = true; + }); + try { + await precachePieceImages(value); + } finally { + setState(() { + isLoading = false; + }); + } + } } - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + List getPieceImages(PieceSet set) { + return [ + set.assets[PieceKind.whiteKing]!, + set.assets[PieceKind.blackQueen]!, + set.assets[PieceKind.whiteRook]!, + set.assets[PieceKind.blackBishop]!, + set.assets[PieceKind.whiteKnight]!, + set.assets[PieceKind.blackPawn]!, + ]; } -} -class _Body extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - List getPieceImages(PieceSet set) { - return [ - set.assets[PieceKind.whiteKing]!, - set.assets[PieceKind.blackQueen]!, - set.assets[PieceKind.whiteRook]!, - set.assets[PieceKind.blackBishop]!, - set.assets[PieceKind.whiteKnight]!, - set.assets[PieceKind.blackPawn]!, - ]; - } - - void onChanged(PieceSet? value) => ref - .read(boardPreferencesProvider.notifier) - .setPieceSet(value ?? PieceSet.cburnett); - - return SafeArea( - child: ListView.separated( - itemCount: PieceSet.values.length, - separatorBuilder: (_, __) => PlatformDivider( - height: 1, - // on iOS: 14 (default indent) + 16 (padding) - indent: - Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Colors.transparent, - ), - itemBuilder: (context, index) { - final set = PieceSet.values[index]; - return PlatformListTile( - trailing: boardPrefs.pieceSet == set - ? Theme.of(context).platform == TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoTheme.of(context).primaryColor, - ) - : null, - title: Text(set.label), - subtitle: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 264, + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.pieceSet), + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], + ), + body: SafeArea( + child: ListView.separated( + itemCount: PieceSet.values.length, + separatorBuilder: + (_, __) => PlatformDivider( + height: 1, + // on iOS: 14 (default indent) + 16 (padding) + indent: Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, + color: Theme.of(context).platform == TargetPlatform.iOS ? null : Colors.transparent, ), - child: Stack( - children: [ - boardPrefs.boardTheme.thumbnail, - Row( - children: getPieceImages(set) - .map( - (img) => Image( - image: img, - height: 44, - ), - ) - .toList(), - ), - ], + itemBuilder: (context, index) { + final pieceSet = PieceSet.values[index]; + return PlatformListTile( + trailing: + boardPrefs.pieceSet == pieceSet + ? Theme.of(context).platform == TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoTheme.of(context).primaryColor, + ) + : null, + title: Text(pieceSet.label), + subtitle: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 264), + child: Stack( + children: [ + boardPrefs.boardTheme.thumbnail, + Row( + children: [ + for (final img in getPieceImages(pieceSet)) Image(image: img, height: 44), + ], + ), + ], + ), ), - ), - onTap: () => onChanged(set), - selected: boardPrefs.pieceSet == set, - ); - }, + onTap: isLoading ? null : () => onChanged(pieceSet), + selected: boardPrefs.pieceSet == pieceSet, + ); + }, + ), ), ); } diff --git a/lib/src/view/settings/piece_shift_method_settings_screen.dart b/lib/src/view/settings/piece_shift_method_settings_screen.dart deleted file mode 100644 index 4edc23ba8d..0000000000 --- a/lib/src/view/settings/piece_shift_method_settings_screen.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:chessground/chessground.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; - -class PieceShiftMethodSettingsScreen extends StatelessWidget { - const PieceShiftMethodSettingsScreen({super.key}); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.preferencesHowDoYouMovePieces)), - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } -} - -String pieceShiftMethodl10n( - BuildContext context, - PieceShiftMethod pieceShiftMethod, -) => - switch (pieceShiftMethod) { - // This is called 'Either' in the Web UI, but in the app we might display this string - // without having the other values as context, so we need to be more explicit. - // TODO add this to mobile translations - PieceShiftMethod.either => 'Either click or drag', - PieceShiftMethod.drag => context.l10n.preferencesDragPiece, - // TODO This string uses 'click', we might want to use 'tap' instead in a mobile-specific translation - PieceShiftMethod.tapTwoSquares => context.l10n.preferencesClickTwoSquares, - }; - -class _Body extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final pieceShiftMethod = ref.watch( - boardPreferencesProvider.select( - (state) => state.pieceShiftMethod, - ), - ); - - void onChanged(PieceShiftMethod? value) { - ref - .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod(value ?? PieceShiftMethod.either); - } - - return SafeArea( - child: ListView( - children: [ - ChoicePicker( - notchedTile: true, - choices: PieceShiftMethod.values, - selectedItem: pieceShiftMethod, - titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), - ); - } -} diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 688f30a325..7fefc444b3 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -3,9 +3,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -13,8 +15,6 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/account/profile_screen.dart'; import 'package:lichess_mobile/src/view/settings/app_background_mode_screen.dart'; import 'package:lichess_mobile/src/view/settings/theme_screen.dart'; @@ -53,9 +53,7 @@ class SettingsTabScreen extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.settingsSettings), - ), + appBar: AppBar(title: Text(context.l10n.settingsSettings)), body: SafeArea(child: _Body()), ), ); @@ -65,13 +63,8 @@ class SettingsTabScreen extends ConsumerWidget { return CupertinoPageScaffold( child: CustomScrollView( slivers: [ - CupertinoSliverNavigationBar( - largeTitle: Text(context.l10n.settingsSettings), - ), - SliverSafeArea( - top: false, - sliver: _Body(), - ), + CupertinoSliverNavigationBar(largeTitle: Text(context.l10n.settingsSettings)), + SliverSafeArea(top: false, sliver: _Body()), ], ), ); @@ -81,51 +74,56 @@ class SettingsTabScreen extends ConsumerWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.listen(currentBottomTabProvider, (prev, current) { + if (prev != BottomTab.settings && current == BottomTab.settings) { + _refreshData(ref); + } + }); + final generalPrefs = ref.watch(generalPreferencesProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); - final packageInfo = ref.watch(packageInfoProvider); - - final androidVersionAsync = ref.watch(androidVersionProvider); + final packageInfo = ref.read(preloadedDataProvider).requireValue.packageInfo; + final dbSize = ref.watch(getDbSizeInBytesProvider); final Widget? donateButton = userSession == null || userSession.user.isPatron != true ? PlatformListTile( - leading: Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - color: context.lichessColors.brag, - ), - title: Text( - context.l10n.patronDonate, - style: TextStyle(color: context.lichessColors.brag), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () { - launchUrl(Uri.parse('https://lichess.org/patron')); - }, - ) + leading: Icon( + LichessIcons.patron, + semanticLabel: context.l10n.patronLichessPatron, + color: context.lichessColors.brag, + ), + title: Text( + context.l10n.patronDonate, + style: TextStyle(color: context.lichessColors.brag), + ), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () { + launchUrl(Uri.parse('https://lichess.org/patron')); + }, + ) : null; final List content = [ ListSection( - header: userSession != null - ? UserFullNameWidget(user: userSession.user) - : null, + header: userSession != null ? UserFullNameWidget(user: userSession.user) : null, hasLeading: true, showDivider: true, children: [ if (userSession != null) ...[ PlatformListTile( - leading: const Icon(Icons.person), + leading: const Icon(Icons.person_outline), title: Text(context.l10n.profile), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { + ref.invalidate(accountActivityProvider); pushPlatformRoute( context, title: context.l10n.profile, @@ -134,11 +132,12 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.manage_accounts), + leading: const Icon(Icons.manage_accounts_outlined), title: Text(context.l10n.preferencesPreferences), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -149,12 +148,12 @@ class _Body extends ConsumerWidget { ), if (authController.isLoading) const PlatformListTile( - leading: Icon(Icons.logout), + leading: Icon(Icons.logout_outlined), title: Center(child: ButtonLoadingIndicator()), ) else PlatformListTile( - leading: const Icon(Icons.logout), + leading: const Icon(Icons.logout_outlined), title: Text(context.l10n.logOut), onTap: () { _showSignOutConfirmDialog(context, ref); @@ -163,20 +162,19 @@ class _Body extends ConsumerWidget { ] else ...[ if (authController.isLoading) const PlatformListTile( - leading: Icon(Icons.login), + leading: Icon(Icons.login_outlined), title: Center(child: ButtonLoadingIndicator()), ) else PlatformListTile( - leading: const Icon(Icons.login), + leading: const Icon(Icons.login_outlined), title: Text(context.l10n.signIn), onTap: () { ref.read(authControllerProvider.notifier).signIn(); }, ), ], - if (Theme.of(context).platform == TargetPlatform.android && - donateButton != null) + if (Theme.of(context).platform == TargetPlatform.android && donateButton != null) donateButton, ], ), @@ -185,7 +183,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ SettingsListTile( - icon: const Icon(Icons.music_note), + icon: const Icon(Icons.music_note_outlined), settingsLabel: Text(context.l10n.sound), settingsValue: '${soundThemeL10n(context, generalPrefs.soundTheme)} (${volumeLabel(generalPrefs.masterVolume)})', @@ -197,40 +195,21 @@ class _Body extends ConsumerWidget { ); }, ), - if (Theme.of(context).platform == TargetPlatform.android) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), - ), SettingsListTile( - icon: const Icon(Icons.brightness_medium), + icon: const Icon(Icons.brightness_medium_outlined), settingsLabel: Text(context.l10n.background), - settingsValue: AppBackgroundModeScreen.themeTitle( - context, - generalPrefs.themeMode, - ), + settingsValue: AppBackgroundModeScreen.themeTitle(context, generalPrefs.themeMode), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( context, - choices: ThemeMode.values, + choices: BackgroundThemeMode.values, selectedItem: generalPrefs.themeMode, - labelBuilder: (t) => - Text(AppBackgroundModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (ThemeMode? value) => ref - .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? ThemeMode.system), + labelBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), + onSelectedItemChanged: + (BackgroundThemeMode? value) => ref + .read(generalPreferencesProvider.notifier) + .setBackgroundThemeMode(value ?? BackgroundThemeMode.system), ); } else { pushPlatformRoute( @@ -241,35 +220,34 @@ class _Body extends ConsumerWidget { } }, ), - SettingsListTile( - icon: const Icon(Icons.palette), - settingsLabel: const Text('Theme'), - settingsValue: - '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', + PlatformListTile( + leading: const Icon(Icons.palette_outlined), + title: Text(context.l10n.mobileTheme), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { - pushPlatformRoute( - context, - title: 'Theme', - builder: (context) => const ThemeScreen(), - ); + pushPlatformRoute(context, title: 'Theme', builder: (context) => const ThemeScreen()); }, ), PlatformListTile( leading: const Icon(LichessIcons.chess_board), - title: Text(context.l10n.board), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + title: Text(context.l10n.preferencesGameBehavior, overflow: TextOverflow.ellipsis), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, - title: context.l10n.board, + title: context.l10n.preferencesGameBehavior, builder: (context) => const BoardSettingsScreen(), ); }, ), SettingsListTile( - icon: const Icon(Icons.language), + icon: const Icon(Icons.language_outlined), settingsLabel: Text(context.l10n.language), settingsValue: localeToLocalizedName( generalPrefs.locale ?? Localizations.localeOf(context), @@ -279,12 +257,11 @@ class _Body extends ConsumerWidget { showChoicePicker( context, choices: kSupportedLocales, - selectedItem: - generalPrefs.locale ?? Localizations.localeOf(context), + selectedItem: generalPrefs.locale ?? Localizations.localeOf(context), labelBuilder: (t) => Text(localeToLocalizedName(t)), - onSelectedItemChanged: (Locale? locale) => ref - .read(generalPreferencesProvider.notifier) - .setLocale(locale), + onSelectedItemChanged: + (Locale? locale) => + ref.read(generalPreferencesProvider.notifier).setLocale(locale), ); } else { AppSettings.openAppSettings(); @@ -298,7 +275,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ PlatformListTile( - leading: const Icon(Icons.info), + leading: const Icon(Icons.info_outlined), title: Text(context.l10n.aboutX('Lichess')), trailing: const _OpenInNewIcon(), onTap: () { @@ -306,7 +283,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.feedback), + leading: const Icon(Icons.feedback_outlined), title: Text(context.l10n.mobileFeedbackButton), trailing: const _OpenInNewIcon(), onTap: () { @@ -314,7 +291,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.article), + leading: const Icon(Icons.article_outlined), title: Text(context.l10n.termsOfService), trailing: const _OpenInNewIcon(), onTap: () { @@ -322,7 +299,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.privacy_tip), + leading: const Icon(Icons.privacy_tip_outlined), title: Text(context.l10n.privacyPolicy), trailing: const _OpenInNewIcon(), onTap: () { @@ -336,7 +313,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ PlatformListTile( - leading: const Icon(Icons.code), + leading: const Icon(Icons.code_outlined), title: Text(context.l10n.sourceCode), trailing: const _OpenInNewIcon(), onTap: () { @@ -344,7 +321,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.bug_report), + leading: const Icon(Icons.bug_report_outlined), title: Text(context.l10n.contribute), trailing: const _OpenInNewIcon(), onTap: () { @@ -352,7 +329,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.star), + leading: const Icon(Icons.star_border_outlined), title: Text(context.l10n.thankYou), trailing: const _OpenInNewIcon(), onTap: () { @@ -361,6 +338,21 @@ class _Body extends ConsumerWidget { ), ], ), + ListSection( + hasLeading: true, + showDivider: true, + children: [ + PlatformListTile( + leading: const Icon(Icons.storage_outlined), + title: const Text('Local database size'), + subtitle: + Theme.of(context).platform == TargetPlatform.iOS + ? null + : Text(_getSizeString(dbSize.value)), + additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, + ), + ], + ), Padding( padding: Styles.bodySectionPadding, child: Column( @@ -368,10 +360,7 @@ class _Body extends ConsumerWidget { children: [ LichessMessage(style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 10), - Text( - 'v${packageInfo.version}', - style: Theme.of(context).textTheme.bodySmall, - ), + Text('v${packageInfo.version}', style: Theme.of(context).textTheme.bodySmall), ], ), ), @@ -404,18 +393,14 @@ class _Body extends ConsumerWidget { title: Text(context.l10n.logOut), actions: [ TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.mobileOkButton), onPressed: () async { Navigator.of(context).pop(); @@ -428,6 +413,14 @@ class _Body extends ConsumerWidget { ); } } + + String _getSizeString(int? bytes) => '${_bytesToMB(bytes ?? 0).toStringAsFixed(2)}MB'; + + double _bytesToMB(int bytes) => bytes * 0.000001; + + void _refreshData(WidgetRef ref) { + ref.invalidate(getDbSizeInBytesProvider); + } } class _OpenInNewIcon extends StatelessWidget { @@ -437,9 +430,11 @@ class _OpenInNewIcon extends StatelessWidget { Widget build(BuildContext context) { return Icon( Icons.open_in_new, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey2.resolveFrom(context) - : null, + size: 18, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey2.resolveFrom(context) + : null, ); } } diff --git a/lib/src/view/settings/sound_settings_screen.dart b/lib/src/view/settings/sound_settings_screen.dart index 03737c6d54..19648fbb44 100644 --- a/lib/src/view/settings/sound_settings_screen.dart +++ b/lib/src/view/settings/sound_settings_screen.dart @@ -3,49 +3,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -const kMasterVolumeValues = [ - 0.0, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.7, - 0.8, - 0.9, - 1.0, -]; +const kMasterVolumeValues = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; class SoundSettingsScreen extends StatelessWidget { const SoundSettingsScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.sound)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.sound)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } } @@ -62,42 +40,33 @@ class _Body extends ConsumerWidget { final generalPrefs = ref.watch(generalPreferencesProvider); void onChanged(SoundTheme? value) { - ref - .read(generalPreferencesProvider.notifier) - .setSoundTheme(value ?? SoundTheme.standard); - ref.read(soundServiceProvider).changeTheme( - value ?? SoundTheme.standard, - playSound: true, - ); + ref.read(generalPreferencesProvider.notifier).setSoundTheme(value ?? SoundTheme.standard); + ref.read(soundServiceProvider).changeTheme(value ?? SoundTheme.standard, playSound: true); } - return SafeArea( - child: ListView( - children: [ - ListSection( - children: [ - SliderSettingsTile( - icon: const Icon(Icons.volume_up), - value: generalPrefs.masterVolume, - values: kMasterVolumeValues, - onChangeEnd: (value) { - ref - .read(generalPreferencesProvider.notifier) - .setMasterVolume(value); - }, - labelBuilder: volumeLabel, - ), - ], - ), - ChoicePicker( - notchedTile: true, - choices: SoundTheme.values, - selectedItem: generalPrefs.soundTheme, - titleBuilder: (t) => Text(soundThemeL10n(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), + return ListView( + children: [ + ListSection( + children: [ + SliderSettingsTile( + icon: const Icon(Icons.volume_up), + value: generalPrefs.masterVolume, + values: kMasterVolumeValues, + onChangeEnd: (value) { + ref.read(generalPreferencesProvider.notifier).setMasterVolume(value); + }, + labelBuilder: volumeLabel, + ), + ], + ), + ChoicePicker( + notchedTile: true, + choices: SoundTheme.values, + selectedItem: generalPrefs.soundTheme, + titleBuilder: (t) => Text(soundThemeL10n(context, t)), + onSelectedItemChanged: onChanged, + ), + ], ); } } diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 3a940a913c..8fe386ed46 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -1,16 +1,23 @@ -import 'dart:math' as math; +import 'dart:ui' show ImageFilter; import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -21,99 +28,372 @@ class ThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + androidBuilder: (context) => const Scaffold(body: _Body()), + iosBuilder: + (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), + border: null, + ), + child: const _Body(), + ), ); } +} - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Theme')), - body: _Body(), - ); +String shapeColorL10n(BuildContext context, ShapeColor shapeColor) => +// TODO add l10n +switch (shapeColor) { + ShapeColor.green => 'Green', + ShapeColor.red => 'Red', + ShapeColor.blue => 'Blue', + ShapeColor.yellow => 'Yellow', +}; + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + late double brightness; + late double hue; + + double headerOpacity = 0; + + bool openAdjustColorSection = false; + + @override + void initState() { + super.initState(); + final boardPrefs = ref.read(boardPreferencesProvider); + brightness = boardPrefs.brightness; + hue = boardPrefs.hue; } - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; } -} -class _Body extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { + final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - const horizontalPadding = 52.0; - - return SafeArea( - child: ListView( - children: [ - LayoutBuilder( - builder: (context, constraints) { - final double boardSize = math.min( - 290, - constraints.biggest.shortestSide - horizontalPadding * 2, - ); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: horizontalPadding, - vertical: 16, - ), - child: Center( - child: Board( - size: boardSize, - data: const BoardData( - interactableSide: InteractableSide.none, - orientation: Side.white, - fen: dartchess.kInitialFEN, + final bool hasAjustedColors = + brightness != kBoardDefaultBrightnessFilter || hue != kBoardDefaultHueFilter; + + final boardSize = isTabletOrLarger(context) ? 350.0 : 200.0; + + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); + + return NotificationListener( + onNotification: handleScrollNotification, + child: CustomScrollView( + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: headerOpacity == 1.0 ? backgroundColor : backgroundColor.withAlpha(0), + shape: LinearBorder.bottom( + side: BorderSide( + color: + headerOpacity == 1.0 ? const Color(0x4D000000) : Colors.transparent, + width: 0.0, + ), + ), ), - settings: BoardSettings( - enableCoordinates: false, - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, + padding: + Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: _BoardPreview( + size: boardSize, + boardPrefs: boardPrefs, + brightness: brightness, + hue: hue, ), ), ), - ); - }, - ), - ListSection( - hasLeading: true, + ), + ) + else + SliverAppBar( + pinned: true, + title: const Text('Theme'), + bottom: PreferredSize( + preferredSize: Size.fromHeight(boardSize + 16.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: _BoardPreview( + size: boardSize, + boardPrefs: boardPrefs, + brightness: brightness, + hue: hue, + ), + ), + ), + ), + SliverList.list( children: [ - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, + ListSection( + hasLeading: true, + children: [ + if (getCorePalette() != null) + SettingsListTile( + icon: const Icon(Icons.colorize_outlined), + settingsLabel: const Text('Color scheme'), + settingsValue: switch (generalPrefs.appThemeSeed) { + AppThemeSeed.board => context.l10n.board, + AppThemeSeed.system => context.l10n.mobileSystemColors, + }, + onTap: () { + showAdaptiveActionSheet( + context: context, + actions: + AppThemeSeed.values + .where( + (t) => t != AppThemeSeed.system || getCorePalette() != null, + ) + .map( + (t) => BottomSheetAction( + makeLabel: + (context) => switch (t) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text( + context.l10n.mobileSystemColors, + ), + }, + onPressed: (context) { + ref + .read(generalPreferencesProvider.notifier) + .setAppThemeSeed(t); + }, + dismissOnPress: true, + ), + ) + .toList(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_pawn), + settingsLabel: Text(context.l10n.pieceSet), + settingsValue: boardPrefs.pieceSet.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.pieceSet, + builder: (context) => const PieceSetScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: + (t) => Text.rich( + TextSpan( + children: [ + TextSpan(text: shapeColorL10n(context, t)), + const TextSpan(text: ' '), + WidgetSpan( + child: Container(width: 15, height: 15, color: t.color), + ), + ], + ), + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text(context.l10n.preferencesBoardCoordinates), + value: boardPrefs.coordinates, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); + }, + ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), + ], ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_pawn), - settingsLabel: Text(context.l10n.pieceSet), - settingsValue: boardPrefs.pieceSet.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.pieceSet, - builder: (context) => const PieceSetScreen(), - ); - }, + ListSection( + header: SettingsSectionTitle(context.l10n.advancedSettings), + hasLeading: true, + children: [ + PlatformListTile( + leading: const Icon(Icons.brightness_6), + title: Slider.adaptive( + min: 0.2, + max: 1.4, + value: brightness, + onChanged: (value) { + setState(() { + brightness = value; + }); + }, + onChangeEnd: (value) { + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: brightness); + }, + ), + ), + PlatformListTile( + leading: const Icon(Icons.invert_colors), + title: Slider.adaptive( + min: 0.0, + max: 360.0, + value: hue, + onChanged: (value) { + setState(() { + hue = value; + }); + }, + onChangeEnd: (value) { + ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); + }, + ), + ), + PlatformListTile( + leading: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: const Icon(Icons.cancel), + ), + title: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: Text(context.l10n.boardReset), + ), + onTap: + hasAjustedColors + ? () { + setState(() { + brightness = kBoardDefaultBrightnessFilter; + hue = kBoardDefaultHueFilter; + }); + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: brightness, hue: hue); + } + : null, + ), + ], ), ], ), + const SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter(child: SizedBox(height: 16.0)), + ), ], ), ); } } + +class _BoardPreview extends StatelessWidget { + const _BoardPreview({ + required this.size, + required this.boardPrefs, + required this.brightness, + required this.hue, + }); + + final BoardPrefs boardPrefs; + final double brightness; + final double hue; + final double size; + + @override + Widget build(BuildContext context) { + return Center( + child: BrightnessHueFilter( + brightness: brightness, + hue: hue, + child: Chessboard.fixed( + size: size, + orientation: Side.white, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: + { + Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, + settings: boardPrefs.toBoardSettings().copyWith( + brightness: kBoardDefaultBrightnessFilter, + hue: kBoardDefaultHueFilter, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), + ), + ), + ); + } +} diff --git a/lib/src/view/settings/toggle_sound_button.dart b/lib/src/view/settings/toggle_sound_button.dart index 06f6bc4927..f22b286bbf 100644 --- a/lib/src/view/settings/toggle_sound_button.dart +++ b/lib/src/view/settings/toggle_sound_button.dart @@ -1,44 +1,23 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +/// A button that toggles the sound on and off. class ToggleSoundButton extends ConsumerWidget { + const ToggleSoundButton({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final isSoundEnabled = ref.watch( - generalPreferencesProvider.select( - (prefs) => prefs.isSoundEnabled, - ), + generalPreferencesProvider.select((prefs) => prefs.isSoundEnabled), ); - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return IconButton( - // TODO translate - tooltip: 'Toggle sound', - icon: isSoundEnabled - ? const Icon(Icons.volume_up) - : const Icon(Icons.volume_off), - onPressed: () => ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(), - ); - case TargetPlatform.iOS: - return CupertinoIconButton( - padding: EdgeInsets.zero, - semanticsLabel: 'Toggle sound', - icon: isSoundEnabled - ? const Icon(CupertinoIcons.volume_up) - : const Icon(CupertinoIcons.volume_off), - onPressed: () => ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(), - ); - default: - assert(false, 'Unexpected platform $Theme.of(context).platform'); - return const SizedBox.shrink(); - } + return AppBarIconButton( + // TODO: i18n + semanticsLabel: 'Toggle sound', + onPressed: () => ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), + icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), + ); } } diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart new file mode 100644 index 0000000000..4a1cf84ccb --- /dev/null +++ b/lib/src/view/study/study_bottom_bar.dart @@ -0,0 +1,304 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +class StudyBottomBar extends ConsumerWidget { + const StudyBottomBar({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gamebook = ref.watch( + studyControllerProvider(id).select((s) => s.requireValue.gamebookActive), + ); + + return gamebook ? _GamebookBottomBar(id: id) : _AnalysisBottomBar(id: id); + } +} + +class _AnalysisBottomBar extends ConsumerWidget { + const _AnalysisBottomBar({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).valueOrNull; + if (state == null) { + return const BottomBar(children: []); + } + + final onGoForward = + state.canGoNext ? ref.read(studyControllerProvider(id).notifier).userNext : null; + final onGoBack = + state.canGoBack ? ref.read(studyControllerProvider(id).notifier).userPrevious : null; + + return BottomBar( + children: [ + _ChapterButton(state: state), + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, + blink: !state.isIntroductoryChapter && state.isAtEndOfChapter && state.hasNextChapter, + ), + RepeatButton( + onLongPress: onGoBack, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: onGoBack, + label: context.l10n.studyBack, + showLabel: true, + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: onGoForward, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + onTap: onGoForward, + label: context.l10n.studyNext, + showLabel: true, + showTooltip: false, + ), + ), + ], + ); + } +} + +class _GamebookBottomBar extends ConsumerWidget { + const _GamebookBottomBar({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + return BottomBar( + children: [ + _ChapterButton(state: state), + ...switch (state.gamebookState) { + GamebookState.findTheMove => [ + if (!state.currentNode.isRoot) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.skip_previous, + label: 'Back', + showLabel: true, + ), + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: ref.read(studyControllerProvider(id).notifier).showGamebookSolution, + ), + ], + GamebookState.startLesson || GamebookState.correctMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userNext, + icon: Icons.play_arrow, + label: context.l10n.studyNext, + showLabel: true, + blink: state.gamebookComment != null && !state.isIntroductoryChapter, + ), + ], + GamebookState.incorrectMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userPrevious, + label: context.l10n.retry, + showLabel: true, + icon: Icons.refresh, + blink: state.gamebookComment != null, + ), + ], + GamebookState.lessonComplete => [ + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.refresh, + label: context.l10n.studyPlayAgain, + showLabel: true, + ), + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, + blink: !state.isIntroductoryChapter && state.hasNextChapter, + ), + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: state.pov, + standalone: ( + pgn: state.pgn, + isComputerAnalysisAllowed: true, + variant: state.variant, + ), + ), + ), + ), + icon: Icons.biotech, + label: context.l10n.analysis, + showLabel: true, + ), + ], + }, + ], + ); + } +} + +class _NextChapterButton extends ConsumerStatefulWidget { + const _NextChapterButton({ + required this.id, + required this.chapterId, + required this.hasNextChapter, + required this.blink, + }); + + final StudyId id; + final StudyChapterId chapterId; + final bool hasNextChapter; + final bool blink; + + @override + ConsumerState<_NextChapterButton> createState() => _NextChapterButtonState(); +} + +class _NextChapterButtonState extends ConsumerState<_NextChapterButton> { + bool isLoading = false; + + @override + void didUpdateWidget(_NextChapterButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.chapterId != widget.chapterId) { + setState(() => isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return isLoading + ? const Center(child: CircularProgressIndicator()) + : BottomBarButton( + onTap: + widget.hasNextChapter + ? () { + ref.read(studyControllerProvider(widget.id).notifier).nextChapter(); + setState(() => isLoading = true); + } + : null, + icon: Icons.play_arrow, + label: context.l10n.studyNextChapter, + showLabel: true, + blink: widget.blink, + ); + } +} + +class _ChapterButton extends ConsumerWidget { + const _ChapterButton({required this.state}); + + final StudyState state; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomBarButton( + onTap: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.6, + snap: true, + expand: false, + builder: (context, scrollController) { + return _StudyChaptersMenu( + id: state.study.id, + scrollController: scrollController, + ); + }, + ), + ), + label: context.l10n.studyNbChapters(state.study.chapters.length), + showLabel: true, + icon: Icons.menu_book, + ); + } +} + +class _StudyChaptersMenu extends ConsumerStatefulWidget { + const _StudyChaptersMenu({required this.id, required this.scrollController}); + + final StudyId id; + final ScrollController scrollController; + + @override + ConsumerState<_StudyChaptersMenu> createState() => _StudyChaptersMenuState(); +} + +class _StudyChaptersMenuState extends ConsumerState<_StudyChaptersMenu> { + final currentChapterKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final state = ref.watch(studyControllerProvider(widget.id)).requireValue; + + // Scroll to the current chapter + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentChapterKey.currentContext != null) { + Scrollable.ensureVisible(currentChapterKey.currentContext!, alignment: 0.5); + } + }); + + return BottomSheetScrollableContainer( + scrollController: widget.scrollController, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + context.l10n.studyNbChapters(state.study.chapters.length), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 16), + for (final chapter in state.study.chapters) + PlatformListTile( + key: chapter.id == state.currentChapter.id ? currentChapterKey : null, + title: Text(chapter.name, maxLines: 2), + onTap: () { + ref.read(studyControllerProvider(widget.id).notifier).goToChapter(chapter.id); + Navigator.of(context).pop(); + }, + selected: chapter.id == state.currentChapter.id, + ), + ], + ); + } +} diff --git a/lib/src/view/study/study_gamebook.dart b/lib/src/view/study/study_gamebook.dart new file mode 100644 index 0000000000..c12c09ed5e --- /dev/null +++ b/lib/src/view/study/study_gamebook.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class StudyGamebook extends StatelessWidget { + const StudyGamebook(this.id); + + final StudyId id; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [_Comment(id: id), _Hint(id: id)], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _Comment extends ConsumerWidget { + const _Comment({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + final comment = + state.gamebookComment ?? + switch (state.gamebookState) { + GamebookState.findTheMove => context.l10n.studyWhatWouldYouPlay, + GamebookState.correctMove => context.l10n.studyGoodMove, + GamebookState.incorrectMove => context.l10n.puzzleNotTheMove, + GamebookState.lessonComplete => context.l10n.studyYouCompletedThisLesson, + _ => '', + }; + + return Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: Linkify( + text: comment, + style: const TextStyle(fontSize: 16), + onOpen: (link) async { + launchUrl(Uri.parse(link.url)); + }, + ), + ), + ), + ), + ); + } +} + +class _Hint extends ConsumerStatefulWidget { + const _Hint({required this.id}); + + final StudyId id; + + @override + ConsumerState<_Hint> createState() => _HintState(); +} + +class _HintState extends ConsumerState<_Hint> { + bool showHint = false; + + @override + Widget build(BuildContext context) { + final hint = ref.watch(studyControllerProvider(widget.id)).requireValue.gamebookHint; + return hint == null + ? const SizedBox.shrink() + : SizedBox( + height: 40, + child: + showHint + ? Center(child: Text(hint)) + : TextButton( + onPressed: () { + setState(() { + showHint = true; + }); + }, + child: Text(context.l10n.getAHint), + ), + ); + } +} + +class GamebookButton extends StatelessWidget { + const GamebookButton({ + required this.icon, + required this.label, + required this.onTap, + this.highlighted = false, + super.key, + }); + + final IconData icon; + final String label; + final VoidCallback? onTap; + + final bool highlighted; + + bool get enabled => onTap != null; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + + return Semantics( + container: true, + enabled: enabled, + button: true, + label: label, + excludeSemantics: true, + child: AdaptiveInkWell( + borderRadius: BorderRadius.zero, + onTap: onTap, + child: Opacity( + opacity: enabled ? 1.0 : 0.4, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: highlighted ? primary : null, size: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + label, + style: TextStyle(fontSize: 16.0, color: highlighted ? primary : null), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart new file mode 100644 index 0000000000..e134f99849 --- /dev/null +++ b/lib/src/view/study/study_list_screen.dart @@ -0,0 +1,393 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/study/study_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/filter.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('StudyListScreen'); + +/// A screen that displays a paginated list of studies +class StudyListScreen extends ConsumerWidget { + const StudyListScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isLoggedIn = ref.watch(authSessionProvider)?.user.id != null; + + final filter = ref.watch(studyFilterProvider); + final title = Text(isLoggedIn ? filter.category.l10n(context.l10n) : context.l10n.studyMenu); + + return PlatformScaffold( + appBar: PlatformAppBar( + title: title, + actions: [ + AppBarIconButton( + icon: const Icon(Icons.tune), + // TODO: translate + semanticsLabel: 'Filter studies', + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (_) => _StudyFilterSheet(isLoggedIn: isLoggedIn), + ), + ), + ], + ), + body: SafeArea(top: false, child: _Body(filter: filter)), + ); + } +} + +class _StudyFilterSheet extends ConsumerWidget { + const _StudyFilterSheet({required this.isLoggedIn}); + + final bool isLoggedIn; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final filter = ref.watch(studyFilterProvider); + + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + // If we're not logged in, the only category available is "All" + if (isLoggedIn) ...[ + Filter( + filterType: FilterType.singleChoice, + choices: StudyCategory.values, + choiceSelected: (choice) => filter.category == choice, + choiceLabel: (category) => Text(category.l10n(context.l10n)), + onSelected: + (value, selected) => ref.read(studyFilterProvider.notifier).setCategory(value), + ), + const PlatformDivider(thickness: 1, indent: 0), + const SizedBox(height: 10.0), + ], + Filter( + // TODO mobile l10n + filterName: 'Sort by', + filterType: FilterType.singleChoice, + choices: StudyListOrder.values, + choiceSelected: (choice) => filter.order == choice, + choiceLabel: (order) => Text(order.l10n(context.l10n)), + onSelected: (value, selected) => ref.read(studyFilterProvider.notifier).setOrder(value), + ), + ], + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body({required this.filter}); + + final StudyFilterState filter; + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + String? search; + + final _searchController = SearchController(); + + final _scrollController = ScrollController(keepScrollOffset: true); + + bool requestedNextPage = false; + + StudyListPaginatorProvider get paginatorProvider => + StudyListPaginatorProvider(filter: widget.filter, search: search); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!requestedNextPage && + _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { + final studiesList = ref.read(paginatorProvider); + + if (!studiesList.isLoading) { + setState(() { + requestedNextPage = true; + }); + + ref.read(paginatorProvider.notifier).next(); + } + } + } + + @override + Widget build(BuildContext context) { + ref.listen(paginatorProvider, (prev, next) { + if (prev?.value?.nextPage != next.value?.nextPage) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + requestedNextPage = false; + }); + } + }); + } + }); + + final studiesAsync = ref.watch(paginatorProvider); + + final searchBar = Padding( + padding: Styles.bodySectionPadding, + child: PlatformSearchBar( + controller: _searchController, + onClear: + () => setState(() { + search = null; + _searchController.clear(); + }), + hintText: search ?? context.l10n.searchSearch, + onSubmitted: (term) { + setState(() { + search = term; + }); + }, + ), + ); + + return studiesAsync.when( + data: (studies) { + return ListView.separated( + shrinkWrap: true, + itemCount: studies.studies.length + 1, + controller: _scrollController, + separatorBuilder: + (context, index) => + index == 0 + ? const SizedBox.shrink() + : Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider(height: 1, cupertinoHasLeading: true) + : const PlatformDivider(height: 1, color: Colors.transparent), + itemBuilder: + (context, index) => + index == 0 ? searchBar : _StudyListItem(study: studies.studies[index - 1]), + ); + }, + loading: () { + return Column( + children: [ + searchBar, + const Expanded(child: Center(child: CircularProgressIndicator.adaptive())), + ], + ); + }, + error: (error, stack) { + _logger.severe('Error loading studies', error, stack); + return Center(child: Text(context.l10n.studyMenu)); + }, + ); + } +} + +class _StudyListItem extends StatelessWidget { + const _StudyListItem({required this.study}); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return PlatformListTile( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, + leading: _StudyFlair(flair: study.flair, size: 30), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(study.name, overflow: TextOverflow.ellipsis, maxLines: 2)], + ), + subtitle: _StudySubtitle(study: study), + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => StudyScreen(id: study.id), + ), + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + builder: (context) => _ContextMenu(study: study), + ); + }, + ); + } +} + +class _ContextMenu extends ConsumerWidget { + const _ContextMenu({required this.study}); + + final StudyPageData study; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + _StudyChapters(study: study), + const SizedBox(height: 10.0), + _StudyMembers(study: study), + ], + ); + } +} + +class _StudyChapters extends StatelessWidget { + const _StudyChapters({required this.study}); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...study.chapters.map( + (chapter) => Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + WidgetSpan( + child: Icon( + Icons.circle_outlined, + size: DefaultTextStyle.of(context).style.fontSize, + ), + ), + TextSpan(text: ' $chapter'), + ], + ), + ), + ), + ], + ); + } +} + +class _StudyMembers extends StatelessWidget { + const _StudyMembers({required this.study}); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...study.members.map( + (member) => Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + member.role == 'w' ? LichessIcons.radio_tower_lichess : Icons.remove_red_eye, + size: DefaultTextStyle.of(context).style.fontSize, + ), + ), + const TextSpan(text: ' '), + WidgetSpan( + alignment: PlaceholderAlignment.bottom, + child: UserFullNameWidget(user: member.user, showFlair: false), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _StudyFlair extends StatelessWidget { + const _StudyFlair({required this.flair, required this.size}); + + final String? flair; + + final double size; + + @override + Widget build(BuildContext context) { + final iconIfNoFlair = Icon(LichessIcons.study, size: size); + + return (flair != null) + ? CachedNetworkImage( + imageUrl: lichessFlairSrc(flair!), + errorWidget: (_, __, ___) => iconIfNoFlair, + width: size, + height: size, + ) + : iconIfNoFlair; + } +} + +class _StudySubtitle extends StatelessWidget { + const _StudySubtitle({required this.study}); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(study.liked ? Icons.favorite : Icons.favorite_outline, size: 14), + ), + TextSpan(text: ' ${study.likes}'), + const TextSpan(text: ' • '), + if (study.owner != null) ...[ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: UserFullNameWidget(user: study.owner, showFlair: false), + ), + const TextSpan(text: ' • '), + ], + TextSpan(text: relativeDate(context.l10n, study.updatedAt)), + ], + ), + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart new file mode 100644 index 0000000000..9024b0a01b --- /dev/null +++ b/lib/src/view/study/study_screen.dart @@ -0,0 +1,546 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:chessground/chessground.dart'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; +import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; +import 'package:lichess_mobile/src/view/study/study_settings.dart'; +import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('StudyScreen'); + +class StudyScreen extends ConsumerWidget { + const StudyScreen({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final boardPrefs = ref.watch(boardPreferencesProvider); + switch (ref.watch(studyControllerProvider(id))) { + case AsyncData(:final value): + return _StudyScreen(id: id, studyState: value); + case AsyncError(:final error, :final stackTrace): + _logger.severe('Cannot load study: $error', stackTrace); + return PlatformScaffold( + appBar: const PlatformAppBar(title: Text('')), + body: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: + (context, boardSize, borderRadius) => Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: borderRadius, + boxShadow: borderRadius != null ? boardShadows : const [], + ), + orientation: Side.white, + fen: kEmptyFEN, + ), + children: const [Center(child: Text('Failed to load study.'))], + ), + ), + ); + case _: + return PlatformScaffold( + appBar: const PlatformAppBar(title: Text('')), + body: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: + (context, boardSize, borderRadius) => Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: borderRadius, + boxShadow: borderRadius != null ? boardShadows : const [], + ), + orientation: Side.white, + fen: kEmptyFEN, + ), + children: const [SizedBox.shrink()], + ), + ), + ); + } + } +} + +class _StudyScreen extends ConsumerStatefulWidget { + const _StudyScreen({required this.id, required this.studyState}); + + final StudyId id; + final StudyState studyState; + + @override + ConsumerState<_StudyScreen> createState() => _StudyScreenState(); +} + +class _StudyScreenState extends ConsumerState<_StudyScreen> with TickerProviderStateMixin { + late List tabs; + late TabController _tabController; + + @override + void initState() { + super.initState(); + + tabs = [ + if (widget.studyState.isOpeningExplorerAvailable) AnalysisTab.opening, + AnalysisTab.moves, + ]; + + _tabController = TabController(vsync: this, initialIndex: tabs.length - 1, length: tabs.length); + } + + @override + void didUpdateWidget(covariant _StudyScreen oldWidget) { + // If the study has not yet loaded the opening explorer and it is now available + // with this chapter, add the opening tab. + // We don't want to remove a tab, so if the opening explorer is not available + // anymore, we keep the tabs as they are. + // In theory, studies mixing chapters with and without opening explorer should be pretty rare. + if (tabs.length < 2 && widget.studyState.isOpeningExplorerAvailable) { + tabs = [AnalysisTab.opening, AnalysisTab.moves]; + _tabController = TabController( + vsync: this, + initialIndex: tabs.length - 1, + length: tabs.length, + ); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: AutoSizeText( + widget.studyState.currentChapterTitle, + maxLines: 2, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + actions: [ + if (tabs.length > 1) AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), + _StudyMenu(id: widget.id), + ], + ), + body: _Body(id: widget.id, tabController: _tabController, tabs: tabs), + ); + } +} + +class _StudyMenu extends ConsumerWidget { + const _StudyMenu({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + return PlatformAppBarMenuButton( + semanticsLabel: 'Study menu', + icon: const Icon(Icons.more_horiz), + actions: [ + AppBarMenuAction( + icon: Icons.settings, + label: context.l10n.settingsSettings, + onPressed: () { + pushPlatformRoute(context, screen: StudySettings(id)); + }, + ), + AppBarMenuAction( + icon: state.study.liked ? Icons.favorite : Icons.favorite_border, + label: state.study.liked ? context.l10n.studyUnlike : context.l10n.studyLike, + onPressed: () { + ref.read(studyControllerProvider(id).notifier).toggleLike(); + }, + ), + AppBarMenuAction( + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, + label: context.l10n.studyShareAndExport, + onPressed: () { + showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyStudyUrl), + onPressed: (context) async { + launchShareDialog(context, uri: lichessUri('/study/${state.study.id}')); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyCurrentChapterUrl), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri('/study/${state.study.id}/${state.study.chapter.id}'), + ); + }, + ), + if (!state.gamebookActive) ...[ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyStudyPgn), + onPressed: (context) async { + try { + final pgn = await ref + .read(studyRepositoryProvider) + .getStudyPgn(state.study.id); + if (context.mounted) { + launchShareDialog(context, text: pgn); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyChapterPgn), + onPressed: (context) async { + launchShareDialog(context, text: state.pgn); + }, + ), + if (state.position != null) + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition(state.pov, state.position!.fen, state.lastMove); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/study/${state.study.id}').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => const Text('GIF'), + onPressed: (_) async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .chapterGif(state.study.id, state.study.chapter.id); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: context.l10n.studyChapterX( + state.study.currentChapterMeta.name, + ), + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ], + ); + }, + ), + ], + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({required this.id, required this.tabController, required this.tabs}); + + final StudyId id; + final TabController tabController; + final List tabs; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final studyState = ref.watch(studyControllerProvider(id)).requireValue; + final variant = studyState.variant; + if (!variant.isReadSupported) { + return DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: + (context, boardSize, borderRadius) => SizedBox.square( + dimension: boardSize, + child: Center(child: Text('${variant.label} is not supported yet.')), + ), + children: const [SizedBox.shrink()], + ), + ); + } + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; + + final gamebookActive = studyState.gamebookActive; + final engineGaugeParams = studyState.engineGaugeParams; + final isComputerAnalysisAllowed = studyState.isComputerAnalysisAllowed; + final isLocalEvaluationEnabled = studyState.isLocalEvaluationEnabled; + final currentNode = studyState.currentNode; + + final bottomChild = gamebookActive ? StudyGamebook(id) : StudyTreeView(id); + + return AnalysisLayout( + tabController: tabController, + boardBuilder: + (context, boardSize, borderRadius) => + _StudyBoard(id: id, boardSize: boardSize, borderRadius: borderRadius), + engineGaugeBuilder: + isComputerAnalysisAllowed && showEvaluationGauge && engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: + isComputerAnalysisAllowed && isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref.read(studyControllerProvider(id).notifier).onUserMove, + ) + : null, + bottomBar: StudyBottomBar(id: id), + children: + tabs.map((tab) { + switch (tab) { + case AnalysisTab.opening: + if (studyState.isOpeningExplorerAvailable && + studyState.currentNode.position != null) { + return OpeningExplorerView( + position: studyState.currentNode.position!, + onMoveSelected: (move) { + ref.read(studyControllerProvider(id).notifier).onUserMove(move); + }, + ); + } else { + return const Center(child: Text('Opening explorer not available.')); + } + case _: + return bottomChild; + } + }).toList(), + ); + } +} + +extension on PgnCommentShape { + Shape get chessground { + final shapeColor = switch (color) { + CommentShapeColor.green => ShapeColor.green, + CommentShapeColor.red => ShapeColor.red, + CommentShapeColor.blue => ShapeColor.blue, + CommentShapeColor.yellow => ShapeColor.yellow, + }; + return from != to + ? Arrow(color: shapeColor.color, orig: from, dest: to) + : Circle(color: shapeColor.color, orig: from); + } +} + +class _StudyBoard extends ConsumerStatefulWidget { + const _StudyBoard({required this.id, required this.boardSize, this.borderRadius}); + + final StudyId id; + + final double boardSize; + + final BorderRadiusGeometry? borderRadius; + + @override + ConsumerState<_StudyBoard> createState() => _StudyBoardState(); +} + +class _StudyBoardState extends ConsumerState<_StudyBoard> { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + // Clear shapes when switching to a new chapter. + // This avoids "leftover" shapes from the previous chapter when the engine has not evaluated the new position yet. + ref.listen(studyControllerProvider(widget.id).select((state) => state.hasValue), (prev, next) { + if (prev != next) { + setState(() { + userShapes = ISet(); + }); + } + }); + final boardPrefs = ref.watch(boardPreferencesProvider); + + final studyState = ref.watch(studyControllerProvider(widget.id)).requireValue; + + final currentNode = studyState.currentNode; + final position = currentNode.position; + + final showVariationArrows = + ref.watch(studyPreferencesProvider.select((prefs) => prefs.showVariationArrows)) && + !studyState.gamebookActive && + currentNode.children.length > 1; + + final pgnShapes = ISet(studyState.pgnShapes.map((shape) => shape.chessground)); + + final variationArrows = ISet( + showVariationArrows + ? currentNode.children.mapIndexed((i, move) { + final color = Colors.white.withValues(alpha: i == 0 ? 0.9 : 0.5); + return Arrow(color: color, orig: (move as NormalMove).from, dest: move.to); + }).toList() + : [], + ); + + final showAnnotationsOnBoard = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); + + final showBestMoveArrow = ref.watch( + analysisPreferencesProvider.select((value) => value.showBestMoveArrow), + ); + final bestMoves = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)); + final ISet bestMoveShapes = + showBestMoveArrow && studyState.isEngineAvailable && bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position!.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + final sanMove = currentNode.sanMove; + final annotation = makeAnnotation(studyState.currentNode.nags); + + return Chessboard( + size: widget.boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + fen: studyState.position?.board.fen ?? studyState.study.currentChapterMeta.fen ?? kInitialFEN, + lastMove: studyState.lastMove as NormalMove?, + orientation: studyState.pov, + shapes: pgnShapes.union(userShapes).union(variationArrows).union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) + : IMap({sanMove.move.to: annotation}) + : null, + game: + position != null + ? GameData( + playerSide: studyState.playerSide, + isCheck: position.isCheck, + sideToMove: position.turn, + validMoves: makeLegalMoves(position), + promotionMove: studyState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref.read(studyControllerProvider(widget.id).notifier).onUserMove(move); + }, + onPromotionSelection: (role) { + ref.read(studyControllerProvider(widget.id).notifier).onPromotionSelection(role); + }, + ) + : null, + ); + } + + void _onCompleteShape(Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } + + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); + } +} diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart new file mode 100644 index 0000000000..ee8c4ac90a --- /dev/null +++ b/lib/src/view/study/study_settings.dart @@ -0,0 +1,93 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class StudySettings extends ConsumerWidget { + const StudySettings(this.id); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final studyController = studyControllerProvider(id); + + final isComputerAnalysisAllowed = ref.watch( + studyController.select((s) => s.requireValue.isComputerAnalysisAllowed), + ); + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final studyPrefs = ref.watch(studyPreferencesProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); + + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), + body: ListView( + children: [ + if (isComputerAnalysisAllowed) + StockfishSettingsWidget( + onToggleLocalEvaluation: + () => ref.read(studyController.notifier).toggleLocalEvaluation(), + onSetEngineSearchTime: + (value) => ref.read(studyController.notifier).setEngineSearchTime(value), + onSetNumEvalLines: + (value) => ref.read(studyController.notifier).setNumEvalLines(value), + onSetEngineCores: (value) => ref.read(studyController.notifier).setEngineCores(value), + ), + ListSection( + children: [ + SwitchSettingTile( + title: Text(context.l10n.showVariationArrows), + value: studyPrefs.showVariationArrows, + onChanged: + (value) => + ref.read(studyPreferencesProvider.notifier).toggleShowVariationArrows(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: analysisPrefs.showAnnotations, + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).toggleAnnotations(), + ), + ], + ), + ListSection( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/view/study/study_tree_view.dart b/lib/src/view/study/study_tree_view.dart new file mode 100644 index 0000000000..b6ff6857a9 --- /dev/null +++ b/lib/src/view/study/study_tree_view.dart @@ -0,0 +1,58 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; + +const kNextChapterButtonHeight = 32.0; + +class StudyTreeView extends ConsumerWidget { + const StudyTreeView(this.id); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final root = + ref.watch(studyControllerProvider(id).select((value) => value.requireValue.root)) ?? + // If root is null, the study chapter's position is illegal. + // We still want to display the root comments though, so create a dummy root. + const ViewRoot(position: Chess.initial, children: IList.empty()); + + final currentPath = ref.watch( + studyControllerProvider(id).select((value) => value.requireValue.currentPath), + ); + + final pgnRootComments = ref.watch( + studyControllerProvider(id).select((value) => value.requireValue.pgnRootComments), + ); + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + + return CustomScrollView( + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + pgnRootComments: pgnRootComments, + notifier: ref.read(studyControllerProvider(id).notifier), + shouldShowAnnotations: analysisPrefs.showAnnotations, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/view/analysis/analysis_position_choice_screen.dart b/lib/src/view/tools/load_position_screen.dart similarity index 56% rename from lib/src/view/analysis/analysis_position_choice_screen.dart rename to lib/src/view/tools/load_position_screen.dart index b84bc3daca..9f753a7981 100644 --- a/lib/src/view/analysis/analysis_position_choice_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -8,40 +8,21 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class AnalysisPositionChoiceScreen extends StatelessWidget { - const AnalysisPositionChoiceScreen({super.key}); +class LoadPositionScreen extends StatelessWidget { + const LoadPositionScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.loadPosition), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.loadPosition)), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - middle: Text(context.l10n.loadPosition), - ), - child: const _Body(), - ); - } } class _Body extends StatefulWidget { @@ -85,7 +66,7 @@ class _BodyState extends State<_Body> { child: AdaptiveTextField( maxLines: 500, placeholder: - '${context.l10n.pasteTheFenStringHere}\n\n${context.l10n.pasteThePgnStringHere}\n\nLeave empty for initial position', + '${context.l10n.pasteTheFenStringHere} / ${context.l10n.pasteThePgnStringHere}', controller: _controller, readOnly: true, onTap: _getClipboardData, @@ -94,19 +75,34 @@ class _BodyState extends State<_Body> { ), Padding( padding: Styles.bodySectionBottomPadding, - child: FatButton( - semanticsLabel: context.l10n.analysis, - onPressed: parsedInput != null - ? () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => AnalysisScreen( - pgnOrId: parsedInput!.$1, - options: parsedInput!.$2, - ), - ) - : null, - child: Text(context.l10n.studyStart), + child: Column( + children: [ + FatButton( + semanticsLabel: context.l10n.analysis, + onPressed: + parsedInput != null + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => AnalysisScreen(options: parsedInput!.options), + ) + : null, + child: Text(context.l10n.analysis), + ), + const SizedBox(height: 16.0), + FatButton( + semanticsLabel: context.l10n.boardEditor, + onPressed: + parsedInput != null + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => BoardEditorScreen(initialFen: parsedInput!.fen), + ) + : null, + child: Text(context.l10n.boardEditor), + ), + ], ), ), ], @@ -121,32 +117,26 @@ class _BodyState extends State<_Body> { } } - (String, AnalysisOptions)? get parsedInput { + ({String fen, AnalysisOptions options})? get parsedInput { if (textInput == null || textInput!.trim().isEmpty) { - return const ( - '', - AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, - ) - ); + return null; } // try to parse as FEN first try { final pos = Chess.fromSetup(Setup.parseFen(textInput!.trim())); return ( - '[FEN "${pos.fen}"]', - const AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, + fen: pos.fen, + options: AnalysisOptions( orientation: Side.white, - id: standaloneAnalysisId, - ) + standalone: ( + pgn: '[FEN "${pos.fen}"]', + isComputerAnalysisAllowed: true, + variant: Variant.standard, + ), + ), ); - } catch (_, __) {} + } catch (_) {} // try to parse as PGN try { @@ -162,17 +152,24 @@ class _BodyState extends State<_Body> { return null; } + final lastPosition = mainlineMoves.fold( + initialPosition, + (pos, move) => pos.play(pos.parseSan(move.san)!), + ); + return ( - textInput!, - AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: rule != null ? Variant.fromRule(rule) : Variant.standard, - initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, + fen: lastPosition.fen, + options: AnalysisOptions( orientation: Side.white, - id: standaloneAnalysisId, - ) + standalone: ( + pgn: textInput!, + isComputerAnalysisAllowed: true, + variant: rule != null ? Variant.fromRule(rule) : Variant.standard, + ), + initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, + ), ); - } catch (_, __) {} + } catch (_) {} return null; } diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 689f3bd60c..40e2abde94 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -5,12 +5,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_position_choice_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/clock/clock_tool_screen.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; +import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -35,10 +42,8 @@ class ToolsTabScreen extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.tools), - ), - body: const Center(child: _Body()), + appBar: AppBar(title: Text(context.l10n.tools)), + body: const Column(children: [ConnectivityBanner(), Expanded(child: _Body())]), ), ); } @@ -49,109 +54,154 @@ class ToolsTabScreen extends ConsumerWidget { controller: puzzlesScrollController, slivers: [ CupertinoSliverNavigationBar(largeTitle: Text(context.l10n.tools)), - const SliverSafeArea( - top: false, - sliver: _Body(), - ), + const SliverToBoxAdapter(child: ConnectivityBanner()), + const SliverSafeArea(top: false, sliver: _Body()), ], ), ); } } -class _Body extends StatelessWidget { - const _Body(); +class _ToolsButton extends StatelessWidget { + const _ToolsButton({required this.icon, required this.title, required this.onTap}); + + final IconData icon; + + final String title; + + final VoidCallback? onTap; @override Widget build(BuildContext context) { - final tilePadding = Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(vertical: 8.0) - : EdgeInsets.zero; + final tilePadding = + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(vertical: 8.0) + : EdgeInsets.zero; - final content = [ - const SizedBox(height: 16.0), - ListSection( - hasLeading: true, - children: [ - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.biotech, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS + return Padding( + padding: + Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, + child: Opacity( + opacity: onTap == null ? 0.5 : 1.0, + child: PlatformListTile( + leading: Icon( + icon, + size: Styles.mainListTileIconSize, + color: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoTheme.of(context).primaryColor : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.analysis, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS + ), + title: Padding(padding: tilePadding, child: Text(title, style: Styles.callout)), + trailing: + Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const AnalysisScreen( - pgnOrId: '', - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, - ), + onTap: onTap, + ), + ), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + final content = [ + if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(height: 16.0), + ListSection( + hasLeading: true, + children: [ + _ToolsButton( + icon: Icons.upload_file, + title: context.l10n.loadPosition, + onTap: + () => pushPlatformRoute(context, builder: (context) => const LoadPositionScreen()), + ), + _ToolsButton( + icon: Icons.biotech, + title: context.l10n.analysis, + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => const AnalysisScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: '', + isComputerAnalysisAllowed: true, + variant: Variant.standard, + ), + ), + ), ), - ), - ), ), - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.upload_file, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.loadPosition, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const AnalysisPositionChoiceScreen(), - ), - ), + _ToolsButton( + icon: Icons.explore_outlined, + title: context.l10n.openingExplorer, + onTap: + isOnline + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => const OpeningExplorerScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: '', + isComputerAnalysisAllowed: false, + variant: Variant.standard, + ), + ), + ), + ) + : null, ), - PlatformListTile( - leading: Icon( - Icons.alarm, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.clock, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const ClockScreen(), - rootNavigator: true, + if (isOnline) + _ToolsButton( + icon: LichessIcons.study, + title: context.l10n.studyMenu, + onTap: + () => pushPlatformRoute(context, builder: (context) => const StudyListScreen()), ), + _ToolsButton( + icon: Icons.edit_outlined, + title: context.l10n.boardEditor, + onTap: + () => pushPlatformRoute( + context, + builder: (context) => const BoardEditorScreen(), + rootNavigator: true, + ), + ), + _ToolsButton( + icon: Icons.where_to_vote_outlined, + title: 'Coordinate Training', // TODO l10n + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const CoordinateTrainingScreen(), + ), + ), + _ToolsButton( + icon: Icons.alarm, + title: context.l10n.clock, + onTap: + () => pushPlatformRoute( + context, + builder: (context) => const ClockToolScreen(), + rootNavigator: true, + ), ), ], ), @@ -159,9 +209,6 @@ class _Body extends StatelessWidget { return Theme.of(context).platform == TargetPlatform.iOS ? SliverList(delegate: SliverChildListDelegate(content)) - : ListView( - controller: puzzlesScrollController, - children: content, - ); + : ListView(controller: puzzlesScrollController, children: content); } } diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart new file mode 100644 index 0000000000..990dd32b00 --- /dev/null +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/play/challenge_list_item.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class ChallengeRequestsScreen extends StatelessWidget { + const ChallengeRequestsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.preferencesNotifyChallenge)), + body: _Body(), + ); + } +} + +class _Body extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final challenges = ref.watch(challengesProvider); + final session = ref.watch(authSessionProvider); + + return challenges.when( + data: (challenges) { + final list = challenges.inward.addAll(challenges.outward); + + if (list.isEmpty) { + return Center(child: Text(context.l10n.nothingToSeeHere)); + } + + return ListView.separated( + itemCount: list.length, + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemBuilder: (context, index) { + final challenge = list[index]; + final user = challenge.challenger?.user; + + if (user == null) return null; + + Future acceptChallenge(BuildContext context) async { + final challengeRepo = ref.read(challengeRepositoryProvider); + await challengeRepo.accept(challenge.id); + final fullId = await challengeRepo + .show(challenge.id) + .then((challenge) => challenge.gameFullId); + if (!context.mounted) return; + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen(initialGameId: fullId); + }, + ); + } + + Future declineChallenge(ChallengeDeclineReason? reason) async { + ref.read(challengeRepositoryProvider).decline(challenge.id, reason: reason); + ref.read(notificationServiceProvider).cancel(challenge.id.value.hashCode); + } + + void confirmDialog() { + showAdaptiveActionSheet( + context: context, + title: + challenge.variant.isPlaySupported + ? const Text('Do you accept the challenge?') + : null, + actions: [ + if (challenge.variant.isPlaySupported) + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.accept), + leading: Icon(Icons.check, color: context.lichessColors.good), + isDefaultAction: true, + onPressed: (context) => acceptChallenge(context), + ), + ...ChallengeDeclineReason.values.map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + leading: Icon(Icons.close, color: context.lichessColors.error), + isDestructiveAction: true, + onPressed: (_) { + declineChallenge(reason); + }, + ), + ), + ], + ); + } + + void showMissingAccountMessage() { + showPlatformSnackbar(context, context.l10n.youNeedAnAccountToDoThat); + } + + return ChallengeListItem( + challenge: challenge, + challengerUser: user, + onPressed: + challenge.direction == ChallengeDirection.inward + ? session == null + ? showMissingAccountMessage + : confirmDialog + : null, + onAccept: + challenge.direction == ChallengeDirection.outward || + !challenge.variant.isPlaySupported + ? null + : session == null + ? showMissingAccountMessage + : () => acceptChallenge(context), + onCancel: + challenge.direction == ChallengeDirection.outward + ? () => ref.read(challengeRepositoryProvider).cancel(challenge.id) + : null, + onDecline: challenge.direction == ChallengeDirection.inward ? declineChallenge : null, + ); + }, + ); + }, + loading: () { + return const Center(child: CircularProgressIndicator.adaptive()); + }, + error: (error, stack) => const Center(child: Text('Error loading challenges')), + ); + } +} diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index fa41d2589b..0dc0915038 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,83 +1,88 @@ -import 'package:flutter/cupertino.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/filter.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ required this.user, required this.isOnline, - this.perf, - this.games, + this.gameFilter = const GameFilterState(), super.key, }); final LightUser? user; final bool isOnline; - final Perf? perf; - final int? games; + final GameFilterState gameFilter; @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - final nbGamesAsync = ref.watch( - userNumberOfGamesProvider(user, isOnline: isOnline), - ); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: nbGamesAsync.when( - data: (nbGames) => Text(context.l10n.nbGames(games ?? nbGames)), - loading: () => const CupertinoActivityIndicator(), - error: (e, s) => Text(context.l10n.mobileAllGames), + final filtersInUse = ref.watch(gameFilterProvider(filter: gameFilter)); + final nbGamesAsync = ref.watch(userNumberOfGamesProvider(user, isOnline: isOnline)); + final title = + filtersInUse.count == 0 + ? nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const ButtonLoadingIndicator(), + error: (e, s) => Text(context.l10n.mobileAllGames), + ) + : Text(filtersInUse.selectionLabel(context)); + final filterBtn = AppBarIconButton( + icon: Badge.count( + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, ), + count: filtersInUse.count, + isLabelVisible: filtersInUse.count > 0, + child: const Icon(Icons.tune), ), - child: _Body(user: user, isOnline: isOnline, perf: perf), + semanticsLabel: context.l10n.filterGames, + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + builder: + (_) => _FilterGames( + filter: ref.read(gameFilterProvider(filter: gameFilter)), + user: user, + ), + ).then((value) { + if (value != null) { + ref.read(gameFilterProvider(filter: gameFilter).notifier).setFilter(value); + } + }), ); - } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { - final nbGamesAsync = ref.watch( - userNumberOfGamesProvider(user, isOnline: isOnline), - ); - return Scaffold( - appBar: AppBar( - title: nbGamesAsync.when( - data: (nbGames) => Text(context.l10n.nbGames(games ?? nbGames)), - loading: () => const ButtonLoadingIndicator(), - error: (e, s) => Text(context.l10n.mobileAllGames), - ), - ), - body: _Body(user: user, isOnline: isOnline, perf: perf), + return PlatformScaffold( + appBar: PlatformAppBar(title: title, actions: [filterBtn]), + body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); } } class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.user, - required this.isOnline, - required this.perf, - }); + const _Body({required this.user, required this.isOnline, required this.gameFilter}); final LightUser? user; final bool isOnline; - final Perf? perf; + final GameFilterState gameFilter; @override ConsumerState<_Body> createState() => _BodyState(); @@ -100,13 +105,12 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { final state = ref.read( userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: widget.perf, + filter: ref.read(gameFilterProvider(filter: widget.gameFilter)), ), ); @@ -123,7 +127,7 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: widget.perf, + filter: ref.read(gameFilterProvider(filter: widget.gameFilter)), ).notifier, ) .getNext(); @@ -133,84 +137,196 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { + final gameFilterState = ref.watch(gameFilterProvider(filter: widget.gameFilter)); final gameListState = ref.watch( - userGameHistoryProvider( - widget.user?.id, - isOnline: widget.isOnline, - perf: widget.perf, - ), + userGameHistoryProvider(widget.user?.id, isOnline: widget.isOnline, filter: gameFilterState), ); return gameListState.when( data: (state) { final list = state.gameList; - return SafeArea( - child: ListView.separated( - controller: _scrollController, - separatorBuilder: (context, index) => Theme.of(context).platform == - TargetPlatform.iOS - ? const PlatformDivider(height: 1, cupertinoHasLeading: true) - : const PlatformDivider(height: 1, color: Colors.transparent), - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', - ), + return list.isEmpty + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center(child: Text('No games found')), + ) + : ListView.separated( + controller: _scrollController, + separatorBuilder: + (context, index) => + Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider(height: 1, cupertinoHasLeading: true) + : const PlatformDivider(height: 1, color: Colors.transparent), + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && state.hasMore && index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center(child: Text('Could not load more games')), + ); + } + + return Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (BuildContext context) { + ref.withClient( + (client) => GameRepository(client).bookmark(list[index].game.id, v: 1), + ); + }, + icon: Icons.star_outline_rounded, + label: 'Bookmark', + ), + ], + ), + child: ExtendedGameListTile( + item: list[index], + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, ), ); - } - - return Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - SlidableAction( - onPressed: (BuildContext context) { - ref.withClient( - (client) => GameRepository(client) - .bookmark(list[index].game.id, v: 1), - ); - }, - icon: Icons.star_outline_rounded, - label: 'Bookmark', - ), - ], - ), - child: ExtendedGameListTile( - item: list[index], - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ) - : null, - ), - ); - }, - ), - ); + }, + ); }, error: (e, s) { - debugPrint( - 'SEVERE: [GameHistoryScreen] could not load game list', - ); + debugPrint('SEVERE: [GameHistoryScreen] could not load game list'); return const Center(child: Text('Could not load Game History')); }, loading: () => const CenterLoadingIndicator(), ); } } + +class _FilterGames extends ConsumerStatefulWidget { + const _FilterGames({required this.filter, required this.user}); + + final GameFilterState filter; + final LightUser? user; + + @override + ConsumerState<_FilterGames> createState() => _FilterGamesState(); +} + +class _FilterGamesState extends ConsumerState<_FilterGames> { + late GameFilterState filter; + + @override + void initState() { + super.initState(); + filter = widget.filter; + } + + static const gamePerfs = [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ]; + static const filterGroupSpace = SizedBox(height: 10.0); + + @override + Widget build(BuildContext context) { + final session = ref.read(authSessionProvider); + final userId = widget.user?.id ?? session?.user.id; + + final Widget filters = + userId != null + ? ref + .watch(userProvider(id: userId)) + .when( + data: (user) => perfFilter(availablePerfs(user)), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (_, __) => perfFilter(gamePerfs), + ) + : perfFilter(gamePerfs); + + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + filters, + const SizedBox(height: 12.0), + const PlatformDivider(thickness: 1, indent: 0), + filterGroupSpace, + Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: Side.values, + choiceSelected: (choice) => filter.side == choice, + choiceLabel: + (t) => switch (t) { + Side.white => Text(context.l10n.white), + Side.black => Text(context.l10n.black), + }, + onSelected: + (value, selected) => setState(() { + filter = filter.copyWith(side: selected ? value : null); + }), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AdaptiveTextButton( + onPressed: () => setState(() => filter = const GameFilterState()), + child: Text(context.l10n.reset), + ), + AdaptiveTextButton( + onPressed: () => Navigator.of(context).pop(filter), + child: Text(context.l10n.apply), + ), + ], + ), + ], + ); + } + + List availablePerfs(User user) { + final perfs = gamePerfs + .where((perf) { + final p = user.perfs[perf]; + return p != null && p.numberOfGamesOrRuns > 0; + }) + .toList(growable: false); + perfs.sort( + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + ); + return perfs; + } + + Widget perfFilter(List choices) => Filter( + filterName: context.l10n.variant, + filterType: FilterType.multipleChoice, + choices: choices, + choiceSelected: (choice) => filter.perfs.contains(choice), + choiceLabel: (t) => Text(t.shortTitle), + onSelected: + (value, selected) => setState(() { + filter = filter.copyWith( + perfs: selected ? filter.perfs.add(value) : filter.perfs.remove(value), + ); + }), + ); +} diff --git a/lib/src/view/user/leaderboard_screen.dart b/lib/src/view/user/leaderboard_screen.dart index 52d408725a..db62d23f29 100644 --- a/lib/src/view/user/leaderboard_screen.dart +++ b/lib/src/view/user/leaderboard_screen.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_layout_grid/flutter_layout_grid.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; /// Create a Screen with Top 10 players for each Lichess Variant @@ -21,21 +20,8 @@ class LeaderboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.leaderboard), - ), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.leaderboard)), body: const _Body(), ); } @@ -54,43 +40,15 @@ class _Body extends ConsumerWidget { _Leaderboard(data.bullet, LichessIcons.bullet, 'BULLET'), _Leaderboard(data.blitz, LichessIcons.blitz, 'BLITZ'), _Leaderboard(data.rapid, LichessIcons.rapid, 'RAPID'), - _Leaderboard( - data.classical, - LichessIcons.classical, - 'CLASSICAL', - ), - _Leaderboard( - data.ultrabullet, - LichessIcons.ultrabullet, - 'ULTRA BULLET', - ), - _Leaderboard( - data.crazyhouse, - LichessIcons.h_square, - 'CRAZYHOUSE', - ), - _Leaderboard( - data.chess960, - LichessIcons.die_six, - 'CHESS 960', - ), - _Leaderboard( - data.kingOfThehill, - LichessIcons.bullet, - 'KING OF THE HILL', - ), - _Leaderboard( - data.threeCheck, - LichessIcons.three_check, - 'THREE CHECK', - ), + _Leaderboard(data.classical, LichessIcons.classical, 'CLASSICAL'), + _Leaderboard(data.ultrabullet, LichessIcons.ultrabullet, 'ULTRA BULLET'), + _Leaderboard(data.crazyhouse, LichessIcons.h_square, 'CRAZYHOUSE'), + _Leaderboard(data.chess960, LichessIcons.die_six, 'CHESS 960'), + _Leaderboard(data.kingOfThehill, LichessIcons.bullet, 'KING OF THE HILL'), + _Leaderboard(data.threeCheck, LichessIcons.three_check, 'THREE CHECK'), _Leaderboard(data.atomic, LichessIcons.atom, 'ATOMIC'), _Leaderboard(data.horde, LichessIcons.horde, 'HORDE'), - _Leaderboard( - data.antichess, - LichessIcons.antichess, - 'ANTICHESS', - ), + _Leaderboard(data.antichess, LichessIcons.antichess, 'ANTICHESS'), _Leaderboard( data.racingKings, LichessIcons.racing_kings, @@ -103,17 +61,10 @@ class _Body extends ConsumerWidget { child: SingleChildScrollView( child: LayoutBuilder( builder: (context, constraints) { - final crossAxisCount = - math.min(3, (constraints.maxWidth / 300).floor()); + final crossAxisCount = math.min(3, (constraints.maxWidth / 300).floor()); return LayoutGrid( - columnSizes: List.generate( - crossAxisCount, - (_) => 1.fr, - ), - rowSizes: List.generate( - (list.length / crossAxisCount).ceil(), - (_) => auto, - ), + columnSizes: List.generate(crossAxisCount, (_) => 1.fr), + rowSizes: List.generate((list.length / crossAxisCount).ceil(), (_) => auto), children: list, ); }, @@ -122,8 +73,7 @@ class _Body extends ConsumerWidget { ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stack) => - const Center(child: Text('Could not load leaderboard.')), + error: (error, stack) => const Center(child: Text('Could not load leaderboard.')), ); } } @@ -146,19 +96,12 @@ class LeaderboardListTile extends StatelessWidget { child: UserFullNameWidget(user: user.lightUser), ), subtitle: perfIcon != null ? Text(user.rating.toString()) : null, - trailing: perfIcon != null - ? _Progress(user.progress) - : Text(user.rating.toString()), + trailing: perfIcon != null ? _Progress(user.progress) : Text(user.rating.toString()), ); } void _handleTap(BuildContext context) { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: user.lightUser, - ), - ); + pushPlatformRoute(context, builder: (context) => UserScreen(user: user.lightUser)); } } @@ -174,22 +117,16 @@ class _Progress extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, + progress > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, size: 16, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, ), Text( '${progress.abs()}', maxLines: 1, style: TextStyle( fontSize: 12, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, ), ), ], @@ -198,12 +135,7 @@ class _Progress extends StatelessWidget { } class _Leaderboard extends StatelessWidget { - const _Leaderboard( - this.userList, - this.iconData, - this.title, { - this.showDivider = true, - }); + const _Leaderboard(this.userList, this.iconData, this.title, {this.showDivider = true}); final List userList; final IconData iconData; final String title; @@ -223,8 +155,7 @@ class _Leaderboard extends StatelessWidget { Text(title), ], ), - children: - userList.map((user) => LeaderboardListTile(user: user)).toList(), + children: userList.map((user) => LeaderboardListTile(user: user)).toList(), ), ); } diff --git a/lib/src/view/user/leaderboard_widget.dart b/lib/src/view/user/leaderboard_widget.dart index 954f866aab..6ab071ad7e 100644 --- a/lib/src/view/user/leaderboard_widget.dart +++ b/lib/src/view/user/leaderboard_widget.dart @@ -22,9 +22,7 @@ class LeaderboardWidget extends ConsumerWidget { data: (data) { return ListSection( hasLeading: true, - header: Text( - context.l10n.leaderboard, - ), + header: Text(context.l10n.leaderboard), headerTrailing: NoPaddingTextButton( onPressed: () { pushPlatformRoute( @@ -33,16 +31,11 @@ class LeaderboardWidget extends ConsumerWidget { builder: (context) => const LeaderboardScreen(), ); }, - child: Text( - context.l10n.more, - ), + child: Text(context.l10n.more), ), children: [ for (final entry in data.entries) - LeaderboardListTile( - user: entry.value, - perfIcon: entry.key.icon, - ), + LeaderboardListTile(user: entry.value, perfIcon: entry.key.icon), ], ); }, @@ -50,20 +43,18 @@ class LeaderboardWidget extends ConsumerWidget { debugPrint( 'SEVERE: [LeaderboardWidget] could not lead leaderboard data; $error\n $stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load leaderboard.'), + child: Text('Could not load leaderboard.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 5, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 5, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index 51d3bffff7..32dff9b364 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -13,13 +13,9 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; +/// A widget that displays the performance cards of a user. class PerfCards extends StatelessWidget { - const PerfCards({ - required this.user, - required this.isMe, - this.padding, - super.key, - }); + const PerfCards({required this.user, required this.isMe, this.padding, super.key}); final User user; @@ -30,32 +26,34 @@ class PerfCards extends StatelessWidget { @override Widget build(BuildContext context) { const puzzlePerfsSet = {Perf.puzzle, Perf.streak, Perf.storm}; - final List gamePerfs = Perf.values.where((element) { - if (puzzlePerfsSet.contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && - p.numberOfGamesOrRuns > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + final List gamePerfs = Perf.values + .where((element) { + if (puzzlePerfsSet.contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); gamePerfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); - final List puzzlePerfs = Perf.values.where((element) { - if (!puzzlePerfsSet.contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && p.numberOfGamesOrRuns > 0; - }).toList(growable: false); + final List puzzlePerfs = Perf.values + .where((element) { + if (!puzzlePerfsSet.contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0; + }) + .toList(growable: false); puzzlePerfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); final userPerfs = [...gamePerfs, ...puzzlePerfs]; @@ -82,19 +80,14 @@ class PerfCards extends StatelessWidget { width: 100, child: PlatformCard( child: AdaptiveInkWell( - borderRadius: BorderRadius.circular(10), - onTap: isPerfWithoutStats - ? null - : () => _handlePerfCardTap(context, perf), + borderRadius: const BorderRadius.all(Radius.circular(10)), + onTap: isPerfWithoutStats ? null : () => _handlePerfCardTap(context, perf), child: Padding( padding: const EdgeInsets.all(6.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text( - perf.shortTitle, - style: TextStyle(color: textShade(context, 0.7)), - ), + Text(perf.shortTitle, style: TextStyle(color: textShade(context, 0.7))), Icon(perf.icon, color: textShade(context, 0.6)), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -113,17 +106,19 @@ class PerfCards extends StatelessWidget { userPerf.progression > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: userPerf.progression > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + userPerf.progression > 0 + ? context.lichessColors.good + : context.lichessColors.error, size: 12, ), Text( userPerf.progression.abs().toString(), style: TextStyle( - color: userPerf.progression > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + userPerf.progression > 0 + ? context.lichessColors.good + : context.lichessColors.error, fontSize: 11, ), ), @@ -152,10 +147,7 @@ class PerfCards extends StatelessWidget { case Perf.storm: return StormDashboardModal(user: user.lightUser); default: - return PerfStatsScreen( - user: user, - perf: perf, - ); + return PerfStatsScreen(user: user, perf: perf); } }, ); diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index cad1bf787e..0c4bf74f56 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -11,12 +11,12 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -29,12 +29,13 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -final _currentLocale = Intl.getCurrentLocale(); -final _dateFormatter = DateFormat.yMMMd(_currentLocale); +final _dateFormatter = DateFormat.yMMMd(); const _customOpacity = 0.6; const _defaultStatFontSize = 12.0; @@ -42,41 +43,18 @@ const _defaultValueFontSize = 18.0; const _mainValueStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 30); class PerfStatsScreen extends StatelessWidget { - const PerfStatsScreen({ - required this.user, - required this.perf, - super.key, - }); + const PerfStatsScreen({required this.user, required this.perf, super.key}); final User user; final Perf perf; @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - titleSpacing: 0, - title: _Title(user: user, perf: perf), - ), + return PlatformScaffold( + appBar: PlatformAppBar(androidTitleSpacing: 0, title: _Title(user: user, perf: perf)), body: _Body(user: user, perf: perf), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: _Title(user: user, perf: perf), - ), - child: _Body(user: user, perf: perf), - ); - } } class _Title extends StatelessWidget { @@ -87,60 +65,62 @@ class _Title extends StatelessWidget { @override Widget build(BuildContext context) { - final allPerfs = Perf.values.where((element) { - if ([perf, Perf.storm, Perf.streak, Perf.fromPosition] - .contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && - p.games != null && - p.games! > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + final allPerfs = Perf.values + .where((element) { + if ([perf, Perf.storm, Perf.streak, Perf.fromPosition].contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && + p.games != null && + p.games! > 0 && + p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); return AppBarTextButton( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(perf.icon), - Text( - ' ${context.l10n.perfStatPerfStats(perf.title)}', - overflow: TextOverflow.ellipsis, - ), + Text(' ${context.l10n.perfStatPerfStats(perf.title)}', overflow: TextOverflow.ellipsis), const Icon(Icons.arrow_drop_down), ], ), onPressed: () { showAdaptiveActionSheet( context: context, - actions: allPerfs.map((p) { - return BottomSheetAction( - makeLabel: (context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - p.icon, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : null, - ), - const SizedBox(width: 6), - Text( - context.l10n.perfStatPerfStats(p.title), - overflow: TextOverflow.ellipsis, - ), - ], - ), - onPressed: (ctx) { - pushReplacementPlatformRoute( - context, - builder: (ctx) { - return PerfStatsScreen(user: user, perf: p); + actions: allPerfs + .map((p) { + return BottomSheetAction( + makeLabel: + (context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + p.icon, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : null, + ), + const SizedBox(width: 6), + Text( + context.l10n.perfStatPerfStats(p.title), + overflow: TextOverflow.ellipsis, + ), + ], + ), + onPressed: (ctx) { + pushReplacementPlatformRoute( + context, + builder: (ctx) { + return PerfStatsScreen(user: user, perf: p); + }, + ); }, ); - }, - ); - }).toList(growable: false), + }) + .toList(growable: false), ); }, ); @@ -148,10 +128,7 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.user, - required this.perf, - }); + const _Body({required this.user, required this.perf}); final User user; final Perf perf; @@ -166,275 +143,242 @@ class _Body extends ConsumerWidget { return perfStats.when( data: (data) { - return SafeArea( - child: ListView( - padding: Styles.bodyPadding, - scrollDirection: Axis.vertical, - children: [ - ratingHistory.when( - data: (ratingHistoryData) { - final ratingHistoryPerfData = ratingHistoryData - .firstWhereOrNull((element) => element.perf == perf); - - if (ratingHistoryPerfData == null || - ratingHistoryPerfData.points.isEmpty) { - return const SizedBox.shrink(); - } - return _EloChart(ratingHistoryPerfData); - }, - error: (error, stackTrace) { - debugPrint( - 'SEVERE: [PerfStatsScreen] could not load rating history data; $error\n$stackTrace', - ); - return const Text('Could not show chart elo chart'); - }, - loading: () { + return ListView( + padding: Styles.bodyPadding.add(MediaQuery.paddingOf(context)), + scrollDirection: Axis.vertical, + children: [ + ratingHistory.when( + data: (ratingHistoryData) { + final ratingHistoryPerfData = ratingHistoryData.firstWhereOrNull( + (element) => element.perf == perf, + ); + + if (ratingHistoryPerfData == null || ratingHistoryPerfData.points.isEmpty) { return const SizedBox.shrink(); - }, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - '${context.l10n.rating} ', - style: Styles.sectionTitle, - ), - RatingWidget( - rating: data.rating, - deviation: data.deviation, - provisional: data.provisional, - style: _mainValueStyle, - ), - ], - ), - if (perf != Perf.puzzle) ...[ - if (data.percentile != null && data.percentile! > 0.0) - Text( - (loggedInUser != null && loggedInUser.user.id == user.id) - ? context.l10n.youAreBetterThanPercentOfPerfTypePlayers( - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ) - : context.l10n.userIsBetterThanPercentOfPerfTypePlayers( - user.username, - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ), - style: TextStyle(color: textShade(context, 0.7)), - ), - subStatSpace, - // The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know). - StatCard( - context.l10n - .perfStatProgressOverLastXGames('12') - .replaceAll(':', ''), - child: _ProgressionWidget(data.progress), + } + return _EloChart(ratingHistoryPerfData); + }, + error: (error, stackTrace) { + debugPrint( + 'SEVERE: [PerfStatsScreen] could not load rating history data; $error\n$stackTrace', + ); + return const Text('Could not show chart elo chart'); + }, + loading: () { + return const SizedBox.shrink(); + }, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text('${context.l10n.rating} ', style: Styles.sectionTitle), + RatingWidget( + rating: data.rating, + deviation: data.deviation, + provisional: data.provisional, + style: _mainValueStyle, ), - StatCardRow([ - if (data.rank != null) - StatCard( - context.l10n.rank, - value: data.rank == null - ? '?' - : NumberFormat.decimalPattern( + ], + ), + if (perf != Perf.puzzle) ...[ + if (data.percentile != null && data.percentile! > 0.0) + Text( + (loggedInUser != null && loggedInUser.user.id == user.id) + ? context.l10n.youAreBetterThanPercentOfPerfTypePlayers( + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ) + : context.l10n.userIsBetterThanPercentOfPerfTypePlayers( + user.username, + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ), + style: TextStyle(color: textShade(context, 0.7)), + ), + subStatSpace, + // The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know). + StatCard( + context.l10n.perfStatProgressOverLastXGames('12').replaceAll(':', ''), + child: ProgressionWidget(data.progress), + ), + StatCardRow([ + if (data.rank != null) + StatCard( + context.l10n.rank, + value: + data.rank == null + ? '?' + : NumberFormat.decimalPattern( Intl.getCurrentLocale(), ).format(data.rank), - ), - StatCard( - context.l10n - .perfStatRatingDeviation('') - .replaceAll(': .', ''), - value: data.deviation.toStringAsFixed(2), ), - ]), - StatCardRow([ - StatCard( - context.l10n.perfStatHighestRating('').replaceAll(':', ''), - child: _RatingWidget( - data.highestRating, - data.highestRatingGame, - context.lichessColors.good, - ), + StatCard( + context.l10n.perfStatRatingDeviation('').replaceAll(': .', ''), + value: data.deviation.toStringAsFixed(2), + ), + ]), + StatCardRow([ + StatCard( + context.l10n.perfStatHighestRating('').replaceAll(':', ''), + child: _RatingWidget( + data.highestRating, + data.highestRatingGame, + context.lichessColors.good, ), - StatCard( - context.l10n.perfStatLowestRating('').replaceAll(':', ''), - child: _RatingWidget( - data.lowestRating, - data.lowestRatingGame, - context.lichessColors.error, - ), + ), + StatCard( + context.l10n.perfStatLowestRating('').replaceAll(':', ''), + child: _RatingWidget( + data.lowestRating, + data.lowestRatingGame, + context.lichessColors.error, ), - ]), - statGroupSpace, - Semantics( - container: true, - enabled: true, - button: true, - label: context.l10n.perfStatViewTheGames, - child: Tooltip( - excludeFromSemantics: true, - message: context.l10n.perfStatViewTheGames, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (context) => GameHistoryScreen( - user: user.lightUser, - isOnline: true, - perf: perf, - games: data.totalGames, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - '${context.l10n.perfStatTotalGames} ' - .localizeNumbers(), - style: Styles.sectionTitle, - ), - Text( - data.totalGames.toString().localizeNumbers(), - style: _mainValueStyle, - ), - Text( - String.fromCharCode( - Icons.arrow_forward_ios.codePoint, - ), - style: Styles.sectionTitle.copyWith( - fontFamily: 'MaterialIcons', - ), + ), + ]), + statGroupSpace, + Semantics( + container: true, + enabled: true, + button: true, + label: context.l10n.perfStatViewTheGames, + child: Tooltip( + excludeFromSemantics: true, + message: context.l10n.perfStatViewTheGames, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => GameHistoryScreen( + user: user.lightUser, + isOnline: true, + gameFilter: GameFilterState(perfs: ISet({perf})), ), - ], - ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '${context.l10n.perfStatTotalGames} '.localizeNumbers(), + style: Styles.sectionTitle, + ), + Text( + data.totalGames.toString().localizeNumbers(), + style: _mainValueStyle, + ), + Text( + String.fromCharCode(Icons.arrow_forward_ios.codePoint), + style: Styles.sectionTitle.copyWith(fontFamily: 'MaterialIcons'), + ), + ], ), ), ), ), - subStatSpace, - StatCardRow([ - StatCard( - context.l10n.wins, - child: _PercentageValueWidget( - data.wonGames, - data.totalGames, - color: context.lichessColors.good, - ), - ), - StatCard( - context.l10n.draws, - child: _PercentageValueWidget( - data.drawnGames, - data.totalGames, - color: textShade(context, _customOpacity), - isShaded: true, - ), - ), - StatCard( - context.l10n.losses, - child: _PercentageValueWidget( - data.lostGames, - data.totalGames, - color: context.lichessColors.error, - ), - ), - ]), - StatCardRow([ - StatCard( - context.l10n.rated, - child: _PercentageValueWidget( - data.ratedGames, - data.totalGames, - ), - ), - StatCard( - context.l10n.tournament, - child: _PercentageValueWidget( - data.tournamentGames, - data.totalGames, - ), - ), - StatCard( - context.l10n.perfStatBerserkedGames.replaceAll( - ' ${context.l10n.games.toLowerCase()}', - '', - ), - child: _PercentageValueWidget( - data.berserkGames, - data.totalGames, - ), - ), - StatCard( - context.l10n.perfStatDisconnections, - child: _PercentageValueWidget( - data.disconnections, - data.totalGames, - ), - ), - ]), - StatCardRow([ - StatCard( - context.l10n.averageOpponent, - value: data.avgOpponent == null - ? '?' - : data.avgOpponent.toString(), - ), - StatCard( - context.l10n.perfStatTimeSpentPlaying, - value: data.timePlayed - .toDaysHoursMinutes(AppLocalizations.of(context)), - ), - ]), + ), + subStatSpace, + StatCardRow([ StatCard( - context.l10n.perfStatWinningStreak, - child: _StreakWidget( - data.maxWinStreak, - data.curWinStreak, + context.l10n.wins, + child: _PercentageValueWidget( + data.wonGames, + data.totalGames, color: context.lichessColors.good, ), ), StatCard( - context.l10n.perfStatLosingStreak, - child: _StreakWidget( - data.maxLossStreak, - data.curLossStreak, + context.l10n.draws, + child: _PercentageValueWidget( + data.drawnGames, + data.totalGames, + color: textShade(context, _customOpacity), + isShaded: true, + ), + ), + StatCard( + context.l10n.losses, + child: _PercentageValueWidget( + data.lostGames, + data.totalGames, color: context.lichessColors.error, ), ), + ]), + StatCardRow([ StatCard( - context.l10n.perfStatGamesInARow, - child: _StreakWidget(data.maxPlayStreak, data.curPlayStreak), + context.l10n.rated, + child: _PercentageValueWidget(data.ratedGames, data.totalGames), ), StatCard( - context.l10n.perfStatMaxTimePlaying, - child: _StreakWidget(data.maxTimeStreak, data.curTimeStreak), + context.l10n.tournament, + child: _PercentageValueWidget(data.tournamentGames, data.totalGames), ), - if (data.bestWins != null && data.bestWins!.isNotEmpty) ...[ - statGroupSpace, - _GameListWidget( - games: data.bestWins!, - perf: perf, - user: user, - header: Text( - context.l10n.perfStatBestRated, - style: Styles.sectionTitle, - ), + StatCard( + context.l10n.perfStatBerserkedGames.replaceAll( + ' ${context.l10n.games.toLowerCase()}', + '', ), - ], + child: _PercentageValueWidget(data.berserkGames, data.totalGames), + ), + StatCard( + context.l10n.perfStatDisconnections, + child: _PercentageValueWidget(data.disconnections, data.totalGames), + ), + ]), + StatCardRow([ + StatCard( + context.l10n.averageOpponent, + value: data.avgOpponent == null ? '?' : data.avgOpponent.toString(), + ), + StatCard( + context.l10n.perfStatTimeSpentPlaying, + value: data.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), + ), + ]), + StatCard( + context.l10n.perfStatWinningStreak, + child: _StreakWidget( + data.maxWinStreak, + data.curWinStreak, + color: context.lichessColors.good, + ), + ), + StatCard( + context.l10n.perfStatLosingStreak, + child: _StreakWidget( + data.maxLossStreak, + data.curLossStreak, + color: context.lichessColors.error, + ), + ), + StatCard( + context.l10n.perfStatGamesInARow, + child: _StreakWidget(data.maxPlayStreak, data.curPlayStreak), + ), + StatCard( + context.l10n.perfStatMaxTimePlaying, + child: _StreakWidget(data.maxTimeStreak, data.curTimeStreak), + ), + if (data.bestWins != null && data.bestWins!.isNotEmpty) ...[ + statGroupSpace, + _GameListWidget( + games: data.bestWins!, + perf: perf, + user: user, + header: Text(context.l10n.perfStatBestRated, style: Styles.sectionTitle), + ), ], ], - ), + ], ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace', - ); + debugPrint('SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace'); return const Center(child: Text('Could not load user stats.')); }, loading: () => const CenterLoadingIndicator(), @@ -442,49 +386,6 @@ class _Body extends ConsumerWidget { } } -class _ProgressionWidget extends StatelessWidget { - final int progress; - - const _ProgressionWidget(this.progress); - - @override - Widget build(BuildContext context) { - const progressionFontSize = 20.0; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (progress != 0) ...[ - Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - ), - Text( - progress.abs().toString(), - style: TextStyle( - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - fontSize: progressionFontSize, - ), - ), - ] else - Text( - '0', - style: TextStyle( - color: textShade(context, _customOpacity), - fontSize: progressionFontSize, - ), - ), - ], - ); - } -} - class _UserGameWidget extends StatelessWidget { final UserPerfGame? game; @@ -502,10 +403,7 @@ class _UserGameWidget extends StatelessWidget { return game == null ? Text('?', style: defaultDateStyle) - : Text( - _dateFormatter.format(game!.finishedAt), - style: defaultDateStyle, - ); + : Text(_dateFormatter.format(game!.finishedAt), style: defaultDateStyle); } } @@ -521,15 +419,15 @@ class _RatingWidget extends StatelessWidget { return (rating == null) ? const Text('?', style: TextStyle(fontSize: _defaultValueFontSize)) : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - rating.toString(), - style: TextStyle(fontSize: _defaultValueFontSize, color: color), - ), - _UserGameWidget(game), - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + rating.toString(), + style: TextStyle(fontSize: _defaultValueFontSize, color: color), + ), + _UserGameWidget(game), + ], + ); } } @@ -539,12 +437,7 @@ class _PercentageValueWidget extends StatelessWidget { final Color? color; final bool isShaded; - const _PercentageValueWidget( - this.value, - this.denominator, { - this.color, - this.isShaded = false, - }); + const _PercentageValueWidget(this.value, this.denominator, {this.color, this.isShaded = false}); String _getPercentageString(num numerator, num denominator) { return '${((numerator / denominator) * 100).round()}%'; @@ -563,9 +456,10 @@ class _PercentageValueWidget extends StatelessWidget { _getPercentageString(value, denominator), style: TextStyle( fontSize: _defaultValueFontSize, - color: isShaded - ? textShade(context, _customOpacity / 2) - : textShade(context, _customOpacity), + color: + isShaded + ? textShade(context, _customOpacity / 2) + : textShade(context, _customOpacity), ), ), ], @@ -589,83 +483,76 @@ class _StreakWidget extends StatelessWidget { color: textShade(context, _customOpacity), ); - final longestStreakStr = - context.l10n.perfStatLongestStreak('').replaceAll(':', ''); - final currentStreakStr = - context.l10n.perfStatCurrentStreak('').replaceAll(':', ''); - - final List streakWidgets = - [maxStreak, curStreak].mapIndexed((index, streak) { - final streakTitle = Text( - index == 0 ? longestStreakStr : currentStreakStr, - style: streakTitleStyle, - ); - - if (streak == null || streak.isValueEmpty) { - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - streakTitle, - Text( - '-', - style: const TextStyle(fontSize: _defaultValueFontSize), - semanticsLabel: context.l10n.none, - ), - ], - ), - ); - } + final longestStreakStr = context.l10n.perfStatLongestStreak('').replaceAll(':', ''); + final currentStreakStr = context.l10n.perfStatCurrentStreak('').replaceAll(':', ''); - final Text valueText = streak.map( - timeStreak: (UserTimeStreak streak) { - return Text( - streak.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), - style: valueStyle, - textAlign: TextAlign.center, + final List streakWidgets = [maxStreak, curStreak] + .mapIndexed((index, streak) { + final streakTitle = Text( + index == 0 ? longestStreakStr : currentStreakStr, + style: streakTitleStyle, ); - }, - gameStreak: (UserGameStreak streak) { - return Text( - context.l10n.nbGames(streak.gamesPlayed), - style: valueStyle, - textAlign: TextAlign.center, - ); - }, - ); - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - streakTitle, - valueText, - if (streak.startGame != null && streak.endGame != null) - Column( + if (streak == null || streak.isValueEmpty) { + return Expanded( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 5.0), - _UserGameWidget(streak.startGame), - Icon( - Icons.arrow_downward_rounded, - color: textShade(context, _customOpacity), + streakTitle, + Text( + '-', + style: const TextStyle(fontSize: _defaultValueFontSize), + semanticsLabel: context.l10n.none, ), - _UserGameWidget(streak.endGame), ], ), - ], - ), - ); - }).toList(growable: false); + ); + } + + final Text valueText = streak.map( + timeStreak: (UserTimeStreak streak) { + return Text( + streak.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), + style: valueStyle, + textAlign: TextAlign.center, + ); + }, + gameStreak: (UserGameStreak streak) { + return Text( + context.l10n.nbGames(streak.gamesPlayed), + style: valueStyle, + textAlign: TextAlign.center, + ); + }, + ); + + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + streakTitle, + valueText, + if (streak.startGame != null && streak.endGame != null) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 5.0), + _UserGameWidget(streak.startGame), + Icon(Icons.arrow_downward_rounded, color: textShade(context, _customOpacity)), + _UserGameWidget(streak.endGame), + ], + ), + ], + ), + ); + }) + .toList(growable: false); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 5.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: streakWidgets, - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: streakWidgets), ], ); } @@ -698,35 +585,23 @@ class _GameListWidget extends ConsumerWidget { final list = await ref.withClient( (client) => GameRepository(client).getGamesByIds(gameIds), ); - final gameData = - list.firstWhereOrNull((g) => g.id == game.gameId); - if (context.mounted && - gameData != null && - gameData.variant.isReadSupported) { + final gameData = list.firstWhereOrNull((g) => g.id == game.gameId); + if (context.mounted && gameData != null && gameData.variant.isReadSupported) { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => ArchivedGameScreen( - gameData: gameData, - orientation: user.id == gameData.white.user?.id - ? Side.white - : Side.black, - ), + builder: + (context) => ArchivedGameScreen( + gameData: gameData, + orientation: user.id == gameData.white.user?.id ? Side.white : Side.black, + ), ); } else if (context.mounted && gameData != null) { - showPlatformSnackbar( - context, - 'This variant is not supported yet', - ); + showPlatformSnackbar(context, 'This variant is not supported yet'); } }, - playerTitle: UserFullNameWidget( - user: game.opponent, - rating: game.opponentRating, - ), - subtitle: Text( - _dateFormatter.format(game.finishedAt), - ), + playerTitle: UserFullNameWidget(user: game.opponent, rating: game.opponentRating), + subtitle: Text(_dateFormatter.format(game.finishedAt)), ), ], ); @@ -734,11 +609,7 @@ class _GameListWidget extends ConsumerWidget { } class _GameListTile extends StatelessWidget { - const _GameListTile({ - required this.playerTitle, - this.subtitle, - this.onTap, - }); + const _GameListTile({required this.playerTitle, this.subtitle, this.onTap}); final Widget playerTitle; final Widget? subtitle; @@ -749,14 +620,13 @@ class _GameListTile extends StatelessWidget { return PlatformListTile( onTap: onTap, title: playerTitle, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, ); } } @@ -775,11 +645,8 @@ class _EloChartState extends State<_EloChart> { late List _allFlSpot; - List get _flSpot => _allFlSpot - .where( - (element) => element.x >= _minX && element.x <= _maxX, - ) - .toList(); + List get _flSpot => + _allFlSpot.where((element) => element.x >= _minX && element.x <= _maxX).toList(); IList get _points => widget.value.points; @@ -787,24 +654,21 @@ class _EloChartState extends State<_EloChart> { DateTime get _lastDate => _points.last.date; - double get _minY => - (_flSpot.map((e) => e.y).reduce(min) / 100).floorToDouble() * 100; + double get _minY => (_flSpot.map((e) => e.y).reduce(min) / 100).floorToDouble() * 100; - double get _maxY => - (_flSpot.map((e) => e.y).reduce(max) / 100).ceilToDouble() * 100; + double get _maxY => (_flSpot.map((e) => e.y).reduce(max) / 100).ceilToDouble() * 100; - double get _minX => - _startDate(_selectedRange).difference(_firstDate).inDays.toDouble(); + double get _minX => _startDate(_selectedRange).difference(_firstDate).inDays.toDouble(); double get _maxX => _allFlSpot.last.x; DateTime _startDate(DateRange dateRange) => switch (dateRange) { - DateRange.oneWeek => _lastDate.subtract(const Duration(days: 7)), - DateRange.oneMonth => _lastDate.copyWith(month: _lastDate.month - 1), - DateRange.threeMonths => _lastDate.copyWith(month: _lastDate.month - 3), - DateRange.oneYear => _lastDate.copyWith(year: _lastDate.year - 1), - DateRange.allTime => _firstDate, - }; + DateRange.oneWeek => _lastDate.subtract(const Duration(days: 7)), + DateRange.oneMonth => _lastDate.copyWith(month: _lastDate.month - 1), + DateRange.threeMonths => _lastDate.copyWith(month: _lastDate.month - 3), + DateRange.oneYear => _lastDate.copyWith(year: _lastDate.year - 1), + DateRange.allTime => _firstDate, + }; bool _dateIsInRange(DateRange dateRange) => _firstDate.isBefore(_startDate(dateRange)) || @@ -827,22 +691,20 @@ class _EloChartState extends State<_EloChart> { j += 1; } else { pointsHistoryRatingCompleted.add( - UserRatingHistoryPoint( - date: currentDate, - elo: _points[j - 1].elo, - ), + UserRatingHistoryPoint(date: currentDate, elo: _points[j - 1].elo), ); } } - _allFlSpot = pointsHistoryRatingCompleted - .map( - (element) => FlSpot( - element.date.difference(_firstDate).inDays.toDouble(), - element.elo.toDouble(), - ), - ) - .toList(); + _allFlSpot = + pointsHistoryRatingCompleted + .map( + (element) => FlSpot( + element.date.difference(_firstDate).inDays.toDouble(), + element.elo.toDouble(), + ), + ) + .toList(); if (_dateIsInRange(DateRange.threeMonths)) { _selectedRange = DateRange.threeMonths; @@ -857,35 +719,28 @@ class _EloChartState extends State<_EloChart> { @override Widget build(BuildContext context) { - final borderColor = - Theme.of(context).colorScheme.onSurface.withOpacity(0.5); + final borderColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; final chartDateFormatter = switch (_selectedRange) { - DateRange.oneWeek => DateFormat.MMMd(_currentLocale), - DateRange.oneMonth => DateFormat.MMMd(_currentLocale), - DateRange.threeMonths => DateFormat.yMMM(_currentLocale), - DateRange.oneYear => DateFormat.yMMM(_currentLocale), - DateRange.allTime => DateFormat.yMMM(_currentLocale), + DateRange.oneWeek => DateFormat.MMMd(), + DateRange.oneMonth => DateFormat.MMMd(), + DateRange.threeMonths => DateFormat.yMMM(), + DateRange.oneYear => DateFormat.yMMM(), + DateRange.allTime => DateFormat.yMMM(), }; - String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( - _firstDate.add(Duration(days: nbDays.toInt())), - ); + String formatDateFromTimestamp(double nbDays) => + chartDateFormatter.format(_firstDate.add(Duration(days: nbDays.toInt()))); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd(_currentLocale).format( - _firstDate.add(Duration(days: nbDays.toInt())), - ); + DateFormat.yMMMd().format(_firstDate.add(Duration(days: nbDays.toInt()))); Widget leftTitlesWidget(double value, TitleMeta meta) { return SideTitleWidget( axisSide: meta.axisSide, child: Text( value.toInt().toString(), - style: const TextStyle( - color: Colors.grey, - fontSize: 10, - ), + style: const TextStyle(color: Colors.grey, fontSize: 10), ), ); } @@ -897,10 +752,7 @@ class _EloChartState extends State<_EloChart> { axisSide: meta.axisSide, child: Text( formatDateFromTimestamp(value), - style: const TextStyle( - color: Colors.grey, - fontSize: 10, - ), + style: const TextStyle(color: Colors.grey, fontSize: 10), ), ); } @@ -910,9 +762,7 @@ class _EloChartState extends State<_EloChart> { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const SizedBox( - width: 25, - ), + const SizedBox(width: 25), ...DateRange.values .where((dateRange) => _dateIsInRange(dateRange)) .map( @@ -941,10 +791,7 @@ class _EloChartState extends State<_EloChart> { spots: _flSpot, dotData: const FlDotData(show: false), color: chartColor, - belowBarData: BarAreaData( - color: chartColor.withOpacity(0.2), - show: true, - ), + belowBarData: BarAreaData(color: chartColor.withValues(alpha: 0.2), show: true), barWidth: 1.5, ), ], @@ -958,15 +805,12 @@ class _EloChartState extends State<_EloChart> { gridData: FlGridData( show: true, drawVerticalLine: false, - getDrawingHorizontalLine: (value) => FlLine( - color: borderColor, - strokeWidth: 0.5, - ), + getDrawingHorizontalLine: (value) => FlLine(color: borderColor, strokeWidth: 0.5), ), lineTouchData: LineTouchData( touchSpotThreshold: double.infinity, touchTooltipData: LineTouchTooltipData( - getTooltipColor: (_) => chartColor.withOpacity(0.5), + getTooltipColor: (_) => chartColor.withValues(alpha: 0.5), fitInsideHorizontally: true, fitInsideVertically: true, getTooltipItems: (touchedSpots) { @@ -977,13 +821,8 @@ class _EloChartState extends State<_EloChart> { Styles.bold, children: [ TextSpan( - text: formatDateFromTimestampForTooltip( - touchedSpot.x, - ), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 10, - ), + text: formatDateFromTimestampForTooltip(touchedSpot.x), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 10), ), ], ), @@ -994,17 +833,11 @@ class _EloChartState extends State<_EloChart> { getTouchedSpotIndicator: (barData, spotIndexes) { return spotIndexes.map((spotIndex) { return TouchedSpotIndicatorData( - FlLine( - color: chartColor, - strokeWidth: 2, - ), + FlLine(color: chartColor, strokeWidth: 2), FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 5, - color: chartColor, - ); + return FlDotCirclePainter(radius: 5, color: chartColor); }, ), ); @@ -1012,10 +845,8 @@ class _EloChartState extends State<_EloChart> { }, ), titlesData: FlTitlesData( - rightTitles: - const AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: - const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -1044,11 +875,7 @@ class _EloChartState extends State<_EloChart> { } class _RangeButton extends StatelessWidget { - const _RangeButton({ - required this.text, - required this.onPressed, - this.selected = false, - }); + const _RangeButton({required this.text, required this.onPressed, this.selected = false}); final String text; final VoidCallback onPressed; @@ -1059,15 +886,14 @@ class _RangeButton extends StatelessWidget { final chartColor = Theme.of(context).colorScheme.tertiary; return PlatformCard( - color: selected ? chartColor.withOpacity(0.2) : null, + color: selected ? chartColor.withValues(alpha: 0.2) : null, shadowColor: selected ? Colors.transparent : null, child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(8.0)), onTap: onPressed, child: Center( child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), + padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Text(text), ), ), @@ -1085,10 +911,10 @@ enum DateRange { @override String toString() => switch (this) { - DateRange.oneWeek => '1W', - DateRange.oneMonth => '1M', - DateRange.threeMonths => '3M', - DateRange.oneYear => '1Y', - DateRange.allTime => 'ALL', - }; + DateRange.oneWeek => '1W', + DateRange.oneMonth => '1M', + DateRange.threeMonths => '3M', + DateRange.oneYear => '1Y', + DateRange.allTime => 'ALL', + }; } diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 5d5a9c4855..ef9408f0d3 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -1,5 +1,4 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -16,7 +15,8 @@ import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -34,28 +34,12 @@ class PlayerScreen extends ConsumerWidget { ref.read(onlineFriendsProvider.notifier).stopWatchingFriends(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + child: PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.players)), + body: _Body(), ), ); } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.players), - ), - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } } class _Body extends ConsumerWidget { @@ -63,17 +47,12 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - return SafeArea( - child: ListView( - children: [ - Padding( - padding: Styles.bodySectionPadding, - child: const _SearchButton(), - ), - if (session != null) _OnlineFriendsWidget(), - RatingPrefAware(child: LeaderboardWidget()), - ], - ), + return ListView( + children: [ + const Padding(padding: Styles.bodySectionPadding, child: _SearchButton()), + if (session != null) _OnlineFriendsWidget(), + RatingPrefAware(child: LeaderboardWidget()), + ], ); } } @@ -88,26 +67,18 @@ class _SearchButton extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: (context) => SearchBar( - leading: const Icon(Icons.search), - hintText: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => const SearchScreen(), - ), - ), - iosBuilder: (context) => CupertinoSearchTextField( - placeholder: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => const SearchScreen(), - ), - ), + void onUserTap(LightUser user) => + pushPlatformRoute(context, builder: (ctx) => UserScreen(user: user)); + + return PlatformSearchBar( + hintText: context.l10n.searchSearch, + focusNode: AlwaysDisabledFocusNode(), + onTap: + () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen(onUserTap: onUserTap), + ), ); } } @@ -121,21 +92,18 @@ class _OnlineFriendsWidget extends ConsumerWidget { data: (data) { return ListSection( header: Text(context.l10n.nbFriendsOnline(data.length)), - headerTrailing: data.isEmpty - ? null - : NoPaddingTextButton( - onPressed: () => _handleTap(context, data), - child: Text( - context.l10n.more, + headerTrailing: + data.isEmpty + ? null + : NoPaddingTextButton( + onPressed: () => _handleTap(context, data), + child: Text(context.l10n.more), ), - ), children: [ if (data.isEmpty) PlatformListTile( title: Text(context.l10n.friends), - trailing: const Icon( - Icons.chevron_right, - ), + trailing: const Icon(Icons.chevron_right), onTap: () => _handleTap(context, data), ), for (final user in data) @@ -144,13 +112,12 @@ class _OnlineFriendsWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5.0), child: UserFullNameWidget(user: user), ), - onTap: () => pushPlatformRoute( - context, - title: user.name, - builder: (_) => UserScreen( - user: user, - ), - ), + onTap: + () => pushPlatformRoute( + context, + title: user.name, + builder: (_) => UserScreen(user: user), + ), ), ], ); @@ -159,19 +126,15 @@ class _OnlineFriendsWidget extends ConsumerWidget { debugPrint( 'SEVERE: [PlayerScreen] could not load following online users; $error\n $stackTrace', ); - return const Center( - child: Text('Could not load online friends'), - ); + return const Center(child: Text('Could not load online friends')); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 3, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 3, header: true), + ), ), - ), - ), ); } diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 38d687df2e..cab5de57e6 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -2,8 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; @@ -25,16 +25,15 @@ class RecentGamesWidget extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final recentGames = user != null - ? ref.watch(userRecentGamesProvider(userId: user!.id)) - : ref.watch(myRecentGamesProvider); + final recentGames = + user != null + ? ref.watch(userRecentGamesProvider(userId: user!.id)) + : ref.watch(myRecentGamesProvider); - final nbOfGames = ref + final nbOfGames = + ref .watch( - userNumberOfGamesProvider( - user, - isOnline: connectivity.valueOrNull?.isOnline == true, - ), + userNumberOfGamesProvider(user, isOnline: connectivity.valueOrNull?.isOnline == true), ) .valueOrNull ?? 0; @@ -47,45 +46,42 @@ class RecentGamesWidget extends ConsumerWidget { return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, - headerTrailing: nbOfGames > data.length - ? NoPaddingTextButton( - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => GameHistoryScreen( - user: user, - isOnline: connectivity.valueOrNull?.isOnline == true, - ), - ); - }, - child: Text( - context.l10n.more, - ), - ) - : null, - children: data.map((item) { - return ExtendedGameListTile(item: item); - }).toList(), + headerTrailing: + nbOfGames > data.length + ? NoPaddingTextButton( + onPressed: () { + pushPlatformRoute( + context, + builder: + (context) => GameHistoryScreen( + user: user, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), + ); + }, + child: Text(context.l10n.more), + ) + : null, + children: + data.map((item) { + return ExtendedGameListTile(item: item); + }).toList(), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [RecentGames] could not recent games; $error\n$stackTrace', - ); - return Padding( + debugPrint('SEVERE: [RecentGames] could not recent games; $error\n$stackTrace'); + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load recent games.'), + child: Text('Could not load recent games.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 10, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 10, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/view/user/search_screen.dart b/lib/src/view/user/search_screen.dart index 3a415e2886..9640201853 100644 --- a/lib/src/view/user/search_screen.dart +++ b/lib/src/view/user/search_screen.dart @@ -3,21 +3,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/user/search_history.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; const _kSaveHistoryDebouncTimer = Duration(seconds: 2); class SearchScreen extends ConsumerStatefulWidget { - const SearchScreen(); + const SearchScreen({this.onUserTap}); + + final void Function(LightUser)? onUserTap; @override ConsumerState createState() => _SearchScreenState(); @@ -41,6 +43,9 @@ class _SearchScreenState extends ConsumerState { } void _onSearchChanged() { + if (!context.mounted) { + return; + } final term = _searchController.text; if (term.length >= 3) { ref.read(autoCompleteUserProvider(term)); @@ -64,95 +69,75 @@ class _SearchScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + final searchBar = PlatformSearchBar( + hintText: context.l10n.searchSearch, + controller: _searchController, + autoFocus: true, ); - } - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - toolbarHeight: 80, // Custom height to fit the search bar - title: SearchBar( - leading: const Icon(Icons.search), - trailing: [ - if (_searchController.text.isNotEmpty) - IconButton( - onPressed: () => _searchController.clear(), - tooltip: 'Clear', - icon: const Icon( - Icons.close, - ), - ), - ], - hintText: context.l10n.searchSearch, - controller: _searchController, - autoFocus: true, - ), - ), - body: _Body(_term, setSearchText), - ); - } + final body = _Body(_term, setSearchText, widget.onUserTap); - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - automaticallyImplyLeading: false, - middle: SizedBox( - height: 36.0, - child: CupertinoSearchTextField( - placeholder: context.l10n.searchSearch, - controller: _searchController, - autofocus: true, + return PlatformWidget( + androidBuilder: + (context) => Scaffold( + appBar: AppBar( + toolbarHeight: 80, // Custom height to fit the search bar + title: searchBar, + ), + body: body, + ), + iosBuilder: + (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: SizedBox(height: 36.0, child: searchBar), + trailing: NoPaddingTextButton( + child: Text(context.l10n.close), + onPressed: () => Navigator.pop(context), + ), + ), + child: body, ), - ), - trailing: NoPaddingTextButton( - child: Text(context.l10n.close), - onPressed: () => Navigator.pop(context), - ), - ), - child: _Body(_term, setSearchText), ); } } class _Body extends ConsumerWidget { - const _Body(this.term, this.onRecentSearchTap); + const _Body(this.term, this.onRecentSearchTap, this.onUserTap); final String? term; final void Function(String) onRecentSearchTap; + final void Function(LightUser)? onUserTap; @override Widget build(BuildContext context, WidgetRef ref) { if (term != null) { - return SafeArea( - child: _UserList(term!), - ); + return SafeArea(child: _UserList(term!, onUserTap)); } else { final searchHistory = ref.watch(searchHistoryProvider).history; return SafeArea( child: SingleChildScrollView( - child: searchHistory.isEmpty - ? kEmptyWidget - : ListSection( - header: Text(context.l10n.mobileRecentSearches), - headerTrailing: NoPaddingTextButton( - child: Text(context.l10n.mobileClearButton), - onPressed: () => - ref.read(searchHistoryProvider.notifier).clear(), + child: + searchHistory.isEmpty + ? kEmptyWidget + : ListSection( + header: Text(context.l10n.mobileRecentSearches), + headerTrailing: NoPaddingTextButton( + child: Text(context.l10n.mobileClearButton), + onPressed: () => ref.read(searchHistoryProvider.notifier).clear(), + ), + showDividerBetweenTiles: true, + hasLeading: true, + children: + searchHistory + .map( + (term) => PlatformListTile( + leading: const Icon(Icons.history), + title: Text(term), + onTap: () => onRecentSearchTap(term), + ), + ) + .toList(), ), - showDividerBetweenTiles: true, - hasLeading: true, - children: searchHistory - .map( - (term) => PlatformListTile( - leading: const Icon(Icons.history), - title: Text(term), - onTap: () => onRecentSearchTap(term), - ), - ) - .toList(), - ), ), ); } @@ -160,46 +145,49 @@ class _Body extends ConsumerWidget { } class _UserList extends ConsumerWidget { - const _UserList(this.term); + const _UserList(this.term, this.onUserTap); final String term; + final void Function(LightUser)? onUserTap; @override Widget build(BuildContext context, WidgetRef ref) { final autoComplete = ref.watch(autoCompleteUserProvider(term)); return SingleChildScrollView( child: autoComplete.when( - data: (userList) => userList.isNotEmpty - ? ListSection( - header: Row( - children: [ - const Icon(Icons.person), - const SizedBox(width: 8), - Text(context.l10n.mobilePlayersMatchingSearchTerm(term)), - ], - ), - hasLeading: true, - showDividerBetweenTiles: true, - children: userList - .map( - (user) => UserListTile.fromLightUser( - user, - onTap: () => { - pushPlatformRoute( - context, - builder: (ctx) => UserScreen(user: user), - ), - }, + data: + (userList) => + userList.isNotEmpty + ? ListSection( + header: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 8), + Text(context.l10n.mobilePlayersMatchingSearchTerm(term)), + ], ), + hasLeading: true, + showDividerBetweenTiles: true, + children: + userList + .map( + (user) => UserListTile.fromLightUser( + user, + onTap: () { + if (onUserTap != null) { + onUserTap!.call(user); + } + }, + ), + ) + .toList(), ) - .toList(), - ) - : Column( - children: [ - const SizedBox(height: 16.0), - Center(child: Text(context.l10n.mobileNoSearchResults)), - ], - ), + : Column( + children: [ + const SizedBox(height: 16.0), + Center(child: Text(context.l10n.mobileNoSearchResults)), + ], + ), error: (e, _) { debugPrint('Error loading search results: $e'); return const Column( @@ -209,12 +197,7 @@ class _UserList extends ConsumerWidget { ], ); }, - loading: () => const Column( - children: [ - SizedBox(height: 16.0), - CenterLoadingIndicator(), - ], - ), + loading: () => const Column(children: [SizedBox(height: 16.0), CenterLoadingIndicator()]), ), ); } diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 1e87d8cbe9..ab57c5c966 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -1,12 +1,9 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -15,27 +12,8 @@ import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'user_activity.g.dart'; - -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); - -@riverpod -Future> _userActivity( - _UserActivityRef ref, { - required UserId id, -}) async { - return ref.withClientCacheFor( - (client) => UserRepository(client).getActivity(id), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} +final _dateFormatter = DateFormat.yMMMd(); class UserActivityWidget extends ConsumerWidget { const UserActivityWidget({this.user, super.key}); @@ -44,9 +22,10 @@ class UserActivityWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final activity = user != null - ? ref.watch(_userActivityProvider(id: user!.id)) - : ref.watch(accountActivityProvider); + final activity = + user != null + ? ref.watch(userActivityProvider(id: user!.id)) + : ref.watch(accountActivityProvider); return activity.when( data: (data) { @@ -55,30 +34,23 @@ class UserActivityWidget extends ConsumerWidget { return const SizedBox.shrink(); } return ListSection( - header: - Text(context.l10n.activityActivity, style: Styles.sectionTitle), + header: Text(context.l10n.activityActivity, style: Styles.sectionTitle), hasLeading: true, - children: nonEmptyActivities - .take(10) - .map((entry) => UserActivityEntry(entry: entry)) - .toList(), + children: + nonEmptyActivities.take(10).map((entry) => UserActivityEntry(entry: entry)).toList(), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [UserScreen] could not load user activity; $error\n$stackTrace', - ); + debugPrint('SEVERE: [UserScreen] could not load user activity; $error\n$stackTrace'); return const Text('Could not load user activity'); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 10, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 10, header: true), + ), ), - ), - ), ); } } @@ -92,8 +64,7 @@ class UserActivityEntry extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final leadingIconSize = theme.platform == TargetPlatform.iOS ? 26.0 : 36.0; - final emptySubtitle = - theme.platform == TargetPlatform.iOS ? const SizedBox.shrink() : null; + final emptySubtitle = theme.platform == TargetPlatform.iOS ? const SizedBox.shrink() : null; final redColor = theme.extension()?.error; final greenColor = theme.extension()?.good; @@ -102,68 +73,45 @@ class UserActivityEntry extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( - padding: const EdgeInsets.only( - left: 14.0, - top: 16.0, - right: 14.0, - bottom: 4.0, - ), + padding: const EdgeInsets.only(left: 14.0, top: 16.0, right: 14.0, bottom: 4.0), child: Text( _dateFormatter.format(entry.startTime), - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), ), if (entry.games != null) for (final gameEntry in entry.games!.entries) _UserActivityListTile( - leading: Icon( - gameEntry.key.icon, - size: leadingIconSize, - ), + leading: Icon(gameEntry.key.icon, size: leadingIconSize), title: context.l10n.activityPlayedNbGames( - gameEntry.value.win + - gameEntry.value.draw + - gameEntry.value.loss, + gameEntry.value.win + gameEntry.value.draw + gameEntry.value.loss, gameEntry.key.title, ), subtitle: RatingPrefAware( child: Row( children: [ - RatingWidget( - deviation: 0, - rating: gameEntry.value.ratingAfter, - ), + RatingWidget(deviation: 0, rating: gameEntry.value.ratingAfter), const SizedBox(width: 3), - if (gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore != - 0) ...[ + if (gameEntry.value.ratingAfter - gameEntry.value.ratingBefore != 0) ...[ Icon( - gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 - ? greenColor - : redColor, + color: + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 + ? greenColor + : redColor, size: 12, ), Text( - (gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore) + (gameEntry.value.ratingAfter - gameEntry.value.ratingBefore) .abs() .toString(), style: TextStyle( - color: gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 - ? greenColor - : redColor, + color: + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 + ? greenColor + : redColor, fontSize: 11, ), ), @@ -179,46 +127,31 @@ class UserActivityEntry extends ConsumerWidget { ), if (entry.puzzles != null) _UserActivityListTile( - leading: Icon( - LichessIcons.target, - size: leadingIconSize, - ), - title: context.l10n.activitySolvedNbPuzzles( - entry.puzzles!.win + entry.puzzles!.loss, - ), + leading: Icon(LichessIcons.target, size: leadingIconSize), + title: context.l10n.activitySolvedNbPuzzles(entry.puzzles!.win + entry.puzzles!.loss), subtitle: RatingPrefAware( child: Row( children: [ - RatingWidget( - deviation: 0, - rating: entry.puzzles!.ratingAfter, - ), + RatingWidget(deviation: 0, rating: entry.puzzles!.ratingAfter), const SizedBox(width: 3), - if (entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore != - 0) ...[ + if (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore != 0) ...[ Icon( - entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > - 0 + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore > - 0 - ? greenColor - : redColor, + color: + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 + ? greenColor + : redColor, size: 12, ), Text( - (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore) - .abs() - .toString(), + (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore).abs().toString(), style: TextStyle( - color: entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore > - 0 - ? greenColor - : redColor, + color: + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 + ? greenColor + : redColor, fontSize: 11, ), ), @@ -234,44 +167,21 @@ class UserActivityEntry extends ConsumerWidget { ), if (entry.streak != null) _UserActivityListTile( - leading: Icon( - LichessIcons.streak, - size: leadingIconSize, - ), - title: context.l10n.stormPlayedNbRunsOfPuzzleStorm( - entry.streak!.runs, - 'Puzzle Streak', - ), + leading: Icon(LichessIcons.streak, size: leadingIconSize), + title: context.l10n.stormPlayedNbRunsOfPuzzleStorm(entry.streak!.runs, 'Puzzle Streak'), subtitle: emptySubtitle, - trailing: BriefGameResultBox( - win: entry.streak!.score, - draw: 0, - loss: 0, - ), + trailing: BriefGameResultBox(win: entry.streak!.score, draw: 0, loss: 0), ), if (entry.storm != null) _UserActivityListTile( - leading: Icon( - LichessIcons.storm, - size: leadingIconSize, - ), - title: context.l10n.stormPlayedNbRunsOfPuzzleStorm( - entry.storm!.runs, - 'Puzzle Storm', - ), + leading: Icon(LichessIcons.storm, size: leadingIconSize), + title: context.l10n.stormPlayedNbRunsOfPuzzleStorm(entry.storm!.runs, 'Puzzle Storm'), subtitle: emptySubtitle, - trailing: BriefGameResultBox( - win: entry.storm!.score, - draw: 0, - loss: 0, - ), + trailing: BriefGameResultBox(win: entry.storm!.score, draw: 0, loss: 0), ), if (entry.correspondenceEnds != null) _UserActivityListTile( - leading: Icon( - LichessIcons.correspondence, - size: leadingIconSize, - ), + leading: Icon(LichessIcons.correspondence, size: leadingIconSize), title: context.l10n.activityCompletedNbGames( entry.correspondenceEnds!.win + entry.correspondenceEnds!.draw + @@ -284,49 +194,34 @@ class UserActivityEntry extends ConsumerWidget { loss: entry.correspondenceEnds!.loss, ), ), - if (entry.correspondenceMovesNb != null && - entry.correspondenceGamesNb != null) + if (entry.correspondenceMovesNb != null && entry.correspondenceGamesNb != null) _UserActivityListTile( - leading: Icon( - LichessIcons.correspondence, - size: leadingIconSize, - ), - title: context.l10n.activityPlayedNbMoves( - entry.correspondenceMovesNb!, - ), + leading: Icon(LichessIcons.correspondence, size: leadingIconSize), + title: context.l10n.activityPlayedNbMoves(entry.correspondenceMovesNb!), subtitle: Text( - context.l10n.activityInNbCorrespondenceGames( - entry.correspondenceGamesNb!, - ), + context.l10n.activityInNbCorrespondenceGames(entry.correspondenceGamesNb!), ), ), if (entry.tournamentNb != null) _UserActivityListTile( - leading: Icon( - Icons.emoji_events, - size: leadingIconSize, - ), - title: context.l10n.activityCompetedInNbTournaments( - entry.tournamentNb!, - ), - subtitle: entry.bestTournament != null - ? Text( - context.l10n.activityRankedInTournament( - entry.bestTournament!.rank, - entry.bestTournament!.rankPercent.toString(), - entry.bestTournament!.nbGames.toString(), - entry.bestTournament!.name, - ), - maxLines: 2, - ) - : emptySubtitle, + leading: Icon(Icons.emoji_events, size: leadingIconSize), + title: context.l10n.activityCompetedInNbTournaments(entry.tournamentNb!), + subtitle: + entry.bestTournament != null + ? Text( + context.l10n.activityRankedInTournament( + entry.bestTournament!.rank, + entry.bestTournament!.rankPercent.toString(), + entry.bestTournament!.nbGames.toString(), + entry.bestTournament!.name, + ), + maxLines: 2, + ) + : emptySubtitle, ), if (entry.followInNb != null) _UserActivityListTile( - leading: Icon( - Icons.thumb_up, - size: leadingIconSize, - ), + leading: Icon(Icons.thumb_up, size: leadingIconSize), title: context.l10n.activityGainedNbFollowers(entry.followInNb!), subtitle: emptySubtitle, ), @@ -336,12 +231,7 @@ class UserActivityEntry extends ConsumerWidget { } class _UserActivityListTile extends StatelessWidget { - const _UserActivityListTile({ - required this.title, - this.subtitle, - this.trailing, - this.leading, - }); + const _UserActivityListTile({required this.title, this.subtitle, this.trailing, this.leading}); final String title; final Widget? subtitle; @@ -369,10 +259,7 @@ const _gameStatsFontStyle = TextStyle( ); class _ResultBox extends StatelessWidget { - const _ResultBox({ - required this.number, - required this.color, - }); + const _ResultBox({required this.number, required this.color}); final int number; final Color color; @@ -390,10 +277,7 @@ class _ResultBox extends StatelessWidget { padding: const EdgeInsets.all(1.0), child: FittedBox( fit: BoxFit.contain, - child: Text( - number.toString(), - style: _gameStatsFontStyle, - ), + child: Text(number.toString(), style: _gameStatsFontStyle), ), ), ); @@ -401,11 +285,7 @@ class _ResultBox extends StatelessWidget { } class BriefGameResultBox extends StatelessWidget { - const BriefGameResultBox({ - required this.win, - required this.draw, - required this.loss, - }); + const BriefGameResultBox({required this.win, required this.draw, required this.loss}); final int win; final int draw; @@ -417,39 +297,27 @@ class BriefGameResultBox extends StatelessWidget { padding: const EdgeInsets.only(left: 5.0), child: SizedBox( height: 20, - width: (win != 0 ? 1 : 0) * _boxSize + + width: + (win != 0 ? 1 : 0) * _boxSize + (draw != 0 ? 1 : 0) * _boxSize + (loss != 0 ? 1 : 0) * _boxSize + - ((win != 0 ? 1 : 0) + - (draw != 0 ? 1 : 0) + - (loss != 0 ? 1 : 0) - - 1) * - _spaceWidth, + ((win != 0 ? 1 : 0) + (draw != 0 ? 1 : 0) + (loss != 0 ? 1 : 0) - 1) * _spaceWidth, child: Row( children: [ if (win != 0) _ResultBox( number: win, - color: Theme.of(context).extension()?.good ?? - LichessColors.green, - ), - if (win != 0 && draw != 0) - const SizedBox( - width: _spaceWidth, - ), - if (draw != 0) - _ResultBox( - number: draw, - color: context.lichessColors.brag, + color: Theme.of(context).extension()?.good ?? LichessColors.green, ), + if (win != 0 && draw != 0) const SizedBox(width: _spaceWidth), + if (draw != 0) _ResultBox(number: draw, color: context.lichessColors.brag), if ((draw != 0 && loss != 0) || (win != 0 && loss != 0)) - const SizedBox( - width: _spaceWidth, - ), + const SizedBox(width: _spaceWidth), if (loss != 0) _ResultBox( number: loss, - color: Theme.of(context).extension()?.error ?? + color: + Theme.of(context).extension()?.error ?? context.lichessColors.error, ), ], diff --git a/lib/src/view/user/user_profile.dart b/lib/src/view/user/user_profile.dart index 78862eb3cd..5fb7758813 100644 --- a/lib/src/view/user/user_profile.dart +++ b/lib/src/view/user/user_profile.dart @@ -10,13 +10,13 @@ import 'package:lichess_mobile/src/model/user/profile.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:linkify/linkify.dart'; -import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; import 'countries.dart'; @@ -24,10 +24,7 @@ import 'countries.dart'; const _userNameStyle = TextStyle(fontSize: 20, fontWeight: FontWeight.w500); class UserProfileWidget extends ConsumerWidget { - const UserProfileWidget({ - required this.user, - this.bioMaxLines = 10, - }); + const UserProfileWidget({required this.user, this.bioMaxLines = 10}); final User user; @@ -36,12 +33,10 @@ class UserProfileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userFullName = user.profile?.realName != null - ? Text( - user.profile!.realName!, - style: _userNameStyle, - ) - : null; + final userFullName = + user.profile?.realName != null + ? Text(user.profile!.realName!, style: _userNameStyle) + : null; return Padding( padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), @@ -54,9 +49,7 @@ class UserProfileWidget extends ConsumerWidget { child: Row( children: [ Icon(Icons.error, color: context.lichessColors.error), - const SizedBox( - width: 5, - ), + const SizedBox(width: 5), Flexible( child: Text( context.l10n.thisAccountViolatedTos, @@ -70,10 +63,7 @@ class UserProfileWidget extends ConsumerWidget { ), ), if (userFullName != null) - Padding( - padding: const EdgeInsets.only(bottom: 5), - child: userFullName, - ), + Padding(padding: const EdgeInsets.only(bottom: 5), child: userFullName), if (user.profile?.bio != null) Linkify( onOpen: (link) async { @@ -81,52 +71,37 @@ class UserProfileWidget extends ConsumerWidget { final username = link.originText.substring(1); pushPlatformRoute( context, - builder: (ctx) => UserScreen( - user: LightUser( - id: UserId.fromUserName(username), - name: username, - ), - ), + builder: + (ctx) => UserScreen( + user: LightUser(id: UserId.fromUserName(username), name: username), + ), ); } else { launchUrl(Uri.parse(link.url)); } }, - linkifiers: const [ - UrlLinkifier(), - EmailLinkifier(), - UserTagLinkifier(), - ], + linkifiers: const [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()], text: user.profile!.bio!.replaceAll('\n', ' '), maxLines: bioMaxLines, style: bioStyle, overflow: TextOverflow.ellipsis, - linkStyle: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.none, - ), + linkStyle: const TextStyle(color: Colors.blueAccent, decoration: TextDecoration.none), ), const SizedBox(height: 10), if (user.profile?.fideRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('FIDE')}: ${user.profile!.fideRating}', - ), + child: Text('${context.l10n.xRating('FIDE')}: ${user.profile!.fideRating}'), ), if (user.profile?.uscfRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('USCF')}: ${user.profile!.uscfRating}', - ), + child: Text('${context.l10n.xRating('USCF')}: ${user.profile!.uscfRating}'), ), if (user.profile?.ecfRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('ECF')}: ${user.profile!.ecfRating}', - ), + child: Text('${context.l10n.xRating('ECF')}: ${user.profile!.ecfRating}'), ), if (user.profile != null) Padding( @@ -134,19 +109,16 @@ class UserProfileWidget extends ConsumerWidget { child: Location(profile: user.profile!), ), if (user.createdAt != null) - Text( - '${context.l10n.memberSince} ${DateFormat.yMMMMd().format(user.createdAt!)}', - ), + Text('${context.l10n.memberSince} ${DateFormat.yMMMMd().format(user.createdAt!)}'), if (user.seenAt != null) ...[ const SizedBox(height: 5), - Text(context.l10n.lastSeenActive(timeago.format(user.seenAt!))), + Text(context.l10n.lastSeenActive(relativeDate(context.l10n, user.seenAt!))), ], if (user.playTime != null) ...[ const SizedBox(height: 5), Text( context.l10n.tpTimeSpentPlaying( - user.playTime!.total - .toDaysHoursMinutes(AppLocalizations.of(context)), + user.playTime!.total.toDaysHoursMinutes(AppLocalizations.of(context)), ), ), ], diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 14250e25e5..416ed1a3d9 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -1,18 +1,20 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show ClientException; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -32,46 +34,26 @@ class UserScreen extends ConsumerStatefulWidget { class _UserScreenState extends ConsumerState { bool isLoading = false; - @override - Widget build(BuildContext context) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - void setIsLoading(bool value) { setState(() { isLoading = value; }); } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { + @override + Widget build(BuildContext context) { final asyncUser = ref.watch(userAndStatusProvider(id: widget.user.id)); final updatedLightUser = asyncUser.maybeWhen( data: (data) => data.$1.lightUser.copyWith(isOnline: data.$2.online), orElse: () => null, ); - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: UserFullNameWidget( user: updatedLightUser ?? widget.user, shouldShowOnline: updatedLightUser != null, ), - actions: [ - if (isLoading) - const Padding( - padding: EdgeInsets.only(right: 16), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator(), - ), - ), - ), - ], + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], ), body: asyncUser.when( data: (data) => _UserProfileListView(data.$1, isLoading, setIsLoading), @@ -93,44 +75,6 @@ class _UserScreenState extends ConsumerState { ), ); } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - final asyncUser = ref.watch(userAndStatusProvider(id: widget.user.id)); - final updatedLightUser = asyncUser.maybeWhen( - data: (data) => data.$1.lightUser.copyWith(isOnline: data.$2.online), - orElse: () => null, - ); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: UserFullNameWidget( - user: updatedLightUser ?? widget.user, - shouldShowOnline: updatedLightUser != null, - ), - trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, - ), - child: asyncUser.when( - data: (data) => SafeArea( - child: _UserProfileListView(data.$1, isLoading, setIsLoading), - ), - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - if (error is ClientException && error.message.contains('404')) { - return Center( - child: Text( - textAlign: TextAlign.center, - context.l10n.usernameNotFound(widget.user.name), - style: Styles.bold, - ), - ); - } - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(userProvider(id: widget.user.id)), - ); - }, - ), - ); - } } class _UserProfileListView extends ConsumerWidget { @@ -145,22 +89,15 @@ class _UserProfileListView extends ConsumerWidget { final session = ref.watch(authSessionProvider); if (user.disabled == true) { - return Center( - child: Text( - context.l10n.settingsThisAccountIsClosed, - style: Styles.bold, - ), - ); + return Center(child: Text(context.l10n.settingsThisAccountIsClosed, style: Styles.bold)); } - Future userAction( - Future Function(LichessClient client) action, - ) async { + Future userAction(Future Function(LichessClient client) action) async { setIsLoading(true); try { - await ref.withClient(action).then( - (_) => ref.invalidate(userAndStatusProvider(id: user.id)), - ); + await ref + .withClient(action) + .then((_) => ref.invalidate(userAndStatusProvider(id: user.id))); } finally { setIsLoading(false); } @@ -174,72 +111,61 @@ class _UserProfileListView extends ConsumerWidget { ListSection( hasLeading: true, children: [ - // TODO: re-enable when challenges are fully supported - // if (user.canChallenge == true) - // PlatformListTile( - // title: Text(context.l10n.challengeChallengeToPlay), - // leading: const Icon(LichessIcons.crossed_swords), - // onTap: () { - // pushPlatformRoute( - // context, - // builder: (context) => ChallengeScreen(user.lightUser), - // ); - // }, - // ), + if (user.canChallenge == true) + PlatformListTile( + title: Text(context.l10n.challengeChallengeToPlay), + leading: const Icon(LichessIcons.crossed_swords), + onTap: () { + pushPlatformRoute( + context, + builder: (context) => CreateChallengeScreen(user.lightUser), + ); + }, + ), if (user.followable == true && user.following != true) PlatformListTile( leading: const Icon(Icons.person_add), title: Text(context.l10n.follow), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).follow(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).follow(user.id)), ) else if (user.following == true) PlatformListTile( leading: const Icon(Icons.person_remove), title: Text(context.l10n.unfollow), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).unfollow(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).unfollow(user.id)), ), if (user.following != true && user.blocking != true) PlatformListTile( leading: const Icon(Icons.block), title: Text(context.l10n.block), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).block(user.id), - ), + onTap: + isLoading + ? null + : () => userAction((client) => RelationRepository(client).block(user.id)), ) else if (user.blocking == true) PlatformListTile( leading: const Icon(Icons.block), title: Text(context.l10n.unblock), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).unblock(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).unblock(user.id)), ), PlatformListTile( leading: const Icon(Icons.report_problem), title: Text(context.l10n.reportXToModerators(user.username)), onTap: () { - launchUrl( - lichessUri('/report', { - 'username': user.id, - 'login': session.user.id, - }), - ); + launchUrl(lichessUri('/report', {'username': user.id, 'login': session.user.id})); }, ), ], diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index 3e2a8c52e8..7afd2ddc54 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -5,12 +5,11 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/tv/live_tv_channels.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; class LiveTvChannelsScreen extends ConsumerWidget { @@ -27,34 +26,12 @@ class LiveTvChannelsScreen extends ConsumerWidget { ref.read(liveTvChannelsProvider.notifier).stopWatching(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + child: const PlatformScaffold( + appBar: PlatformAppBar(title: Text('Lichess TV')), + body: _Body(), ), ); } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: const Text('Lichess TV'), - ), - body: const _Body(), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('Lichess TV'), - ), - child: _Body(), - ); - } } class _Body extends ConsumerWidget { @@ -78,27 +55,27 @@ class _Body extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => TvScreen( - channel: game.channel, - initialGame: (game.id, game.orientation), - ), + builder: + (_) => + TvScreen(channel: game.channel, initialGame: (game.id, game.orientation)), ); }, - orientation: game.orientation.cg, + orientation: game.orientation, fen: game.fen ?? kEmptyFen, - lastMove: game.lastMove?.cg, + lastMove: game.lastMove, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text( - game.channel.label, - style: Styles.boardPreviewTitle, - ), + Text(game.channel.label, style: Styles.boardPreviewTitle), Icon( game.channel.icon, - color: context.lichessColors.brag, + + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, size: 30, ), UserFullNameWidget.player( @@ -112,12 +89,8 @@ class _Body extends ConsumerWidget { }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text(error.toString())), ); } } diff --git a/lib/src/view/watch/streamer_screen.dart b/lib/src/view/watch/streamer_screen.dart index ff83ce9183..49902b7115 100644 --- a/lib/src/view/watch/streamer_screen.dart +++ b/lib/src/view/watch/streamer_screen.dart @@ -21,51 +21,35 @@ class StreamerScreen extends StatelessWidget { Widget _buildAndroid(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(context.l10n.mobileLiveStreamers), - ), - body: ListView( - children: [ - ListSection( - showDividerBetweenTiles: true, - children: streamers - .map( - (e) => StreamerListTile( - streamer: e, - showSubtitle: true, - maxSubtitleLines: 4, - ), - ) - .toList(growable: false), - ), - ], + appBar: AppBar(title: Text(context.l10n.mobileLiveStreamers)), + body: ListView.builder( + itemCount: streamers.length, + itemBuilder: + (context, index) => StreamerListTile( + streamer: streamers[index], + showSubtitle: true, + maxSubtitleLines: 4, + ), ), ); } Widget _buildIos(BuildContext context) { return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.mobileLiveStreamers), - ), + navigationBar: CupertinoNavigationBar(middle: Text(context.l10n.mobileLiveStreamers)), child: CustomScrollView( slivers: [ SliverSafeArea( - sliver: SliverList( - delegate: SliverChildListDelegate([ - ListSection( - hasLeading: true, - children: streamers - .map( - (e) => StreamerListTile( - streamer: e, - showSubtitle: true, - maxSubtitleLines: 4, - ), - ) - .toList(), - ), - ]), + sliver: SliverList.separated( + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemCount: streamers.length, + itemBuilder: + (context, index) => StreamerListTile( + streamer: streamers[index], + showSubtitle: true, + maxSubtitleLines: 4, + ), ), ), ], @@ -87,22 +71,32 @@ class StreamerListTile extends StatelessWidget { @override Widget build(BuildContext context) { + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); + return PlatformListTile( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, onTap: () async { - final url = - streamer.platform == 'twitch' ? streamer.twitch : streamer.youTube; - if (!await launchUrl( - Uri.parse(url!), - mode: LaunchMode.externalApplication, - )) { + final url = streamer.platform == 'twitch' ? streamer.twitch : streamer.youTube; + if (!await launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)) { debugPrint('ERROR: [StreamerWidget] Could not launch $url'); } }, leading: Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.all(5.0) - : EdgeInsets.zero, - child: Image.network(streamer.image), + padding: + Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.all(5.0) + : EdgeInsets.zero, + child: Image.network( + streamer.image, + width: 50, + height: 50, + cacheWidth: (50.0 * devicePixelRatio).toInt(), + cacheHeight: (50.0 * devicePixelRatio).toInt(), + fit: BoxFit.cover, + ), ), title: Padding( padding: const EdgeInsets.only(right: 5.0), @@ -111,22 +105,15 @@ class StreamerListTile extends StatelessWidget { if (streamer.title != null) ...[ Text( streamer.title!, - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), const SizedBox(width: 5), ], - Flexible( - child: Text(streamer.username, overflow: TextOverflow.ellipsis), - ), + Flexible(child: Text(streamer.username, overflow: TextOverflow.ellipsis)), ], ), ), - subtitle: showSubtitle - ? Text(streamer.status, maxLines: maxSubtitleLines) - : null, + subtitle: showSubtitle ? Text(streamer.status, maxLines: maxSubtitleLines) : null, isThreeLine: showSubtitle && maxSubtitleLines >= 2, trailing: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index c63f218cb3..64e2a639c0 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,24 +1,20 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_controller.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class TvScreen extends ConsumerStatefulWidget { const TvScreen({required this.channel, this.initialGame, super.key}); @@ -31,8 +27,7 @@ class TvScreen extends ConsumerStatefulWidget { } class _TvScreenState extends ConsumerState { - TvControllerProvider get _tvGameCtrl => - tvControllerProvider(widget.channel, widget.initialGame); + TvControllerProvider get _tvGameCtrl => tvControllerProvider(widget.channel, widget.initialGame); final _whiteClockKey = GlobalKey(debugLabel: 'whiteClockOnTvScreen'); final _blackClockKey = GlobalKey(debugLabel: 'blackClockOnTvScreen'); @@ -48,46 +43,17 @@ class _TvScreenState extends ConsumerState { ref.read(_tvGameCtrl.notifier).stopWatching(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: Text('${widget.channel.label} TV'), - actions: [ - ToggleSoundButton(), - ], - ), - body: _Body( - widget.channel, - widget.initialGame, - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, - ), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: Text('${widget.channel.label} TV'), - trailing: ToggleSoundButton(), - ), - child: _Body( - widget.channel, - widget.initialGame, - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, + child: PlatformScaffold( + appBar: PlatformAppBar( + title: Text('${widget.channel.label} TV'), + actions: const [ToggleSoundButton()], + ), + body: _Body( + widget.channel, + widget.initialGame, + whiteClockKey: _whiteClockKey, + blackClockKey: _blackClockKey, + ), ), ); } @@ -108,7 +74,6 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boardPreferences = ref.watch(boardPreferencesProvider); final asyncGame = ref.watch(tvControllerProvider(channel, initialGame)); return Column( @@ -118,80 +83,79 @@ class _Body extends ConsumerWidget { child: asyncGame.when( data: (gameState) { final game = gameState.game; - final position = - gameState.game.positionAt(gameState.stepCursor); - final sideToMove = position.turn; + final position = gameState.game.positionAt(gameState.stepCursor); - final boardData = cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: gameState.orientation.cg, - fen: position.fen, - sideToMove: sideToMove.cg, - lastMove: game.moveAt(gameState.stepCursor)?.cg, - isCheck: boardPreferences.boardHighlights && position.isCheck, - ); final blackPlayerWidget = GamePlayer( player: game.black.setOnGame(true), - clock: gameState.game.clock != null - ? CountdownClock( - key: blackClockKey, - duration: gameState.game.clock!.black, - active: gameState.activeClockSide == Side.black, - ) - : null, + clock: + gameState.game.clock != null + ? CountdownClockBuilder( + key: blackClockKey, + timeLeft: gameState.game.clock!.black, + delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), + clockUpdatedAt: gameState.game.clock!.at, + active: gameState.activeClockSide == Side.black, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.black, + ); + }, + ) + : null, materialDiff: game.lastMaterialDiffAt(Side.black), ); final whitePlayerWidget = GamePlayer( player: game.white.setOnGame(true), - clock: gameState.game.clock != null - ? CountdownClock( - key: whiteClockKey, - duration: gameState.game.clock!.white, - active: gameState.activeClockSide == Side.white, - ) - : null, + clock: + gameState.game.clock != null + ? CountdownClockBuilder( + key: whiteClockKey, + timeLeft: gameState.game.clock!.white, + clockUpdatedAt: gameState.game.clock!.at, + delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), + active: gameState.activeClockSide == Side.white, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.white, + ); + }, + ) + : null, materialDiff: game.lastMaterialDiffAt(Side.white), ); + return BoardTable( - boardData: boardData, + orientation: gameState.orientation, + fen: position.fen, boardSettingsOverrides: const BoardSettingsOverrides( animationDuration: Duration.zero, ), - topTable: gameState.orientation == Side.white - ? blackPlayerWidget - : whitePlayerWidget, - bottomTable: gameState.orientation == Side.white - ? whitePlayerWidget - : blackPlayerWidget, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + topTable: + gameState.orientation == Side.white ? blackPlayerWidget : whitePlayerWidget, + bottomTable: + gameState.orientation == Side.white ? whitePlayerWidget : blackPlayerWidget, + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: gameState.stepCursor, + lastMove: game.moveAt(gameState.stepCursor), ); }, - loading: () => const BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), - showMoveListPlaceholder: true, - ), + loading: + () => const BoardTable( + topTable: kEmptyWidget, + bottomTable: kEmptyWidget, + orientation: Side.white, + fen: kEmptyFEN, + showMoveListPlaceholder: true, + ), error: (err, stackTrace) { - debugPrint( - 'SEVERE: [TvScreen] could not load stream; $err\n$stackTrace', - ); + debugPrint('SEVERE: [TvScreen] could not load stream; $err\n$stackTrace'); return const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + orientation: Side.white, + fen: kEmptyFEN, errorMessage: 'Could not load TV stream.', showMoveListPlaceholder: true, ); @@ -199,91 +163,59 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - tvChannel: channel, - game: initialGame, - ), + _BottomBar(tvChannel: channel, game: initialGame), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.tvChannel, - required this.game, - }); + const _BottomBar({required this.tvChannel, required this.game}); final TvChannel tvChannel; final (GameId id, Side orientation)? game; @override Widget build(BuildContext context, WidgetRef ref) { - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.flipBoard, - onTap: () => _flipBoard(ref), - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoBack() - ? () => _moveBackward(ref) - : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoBack() - ? () => _moveBackward(ref) - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoForward() - ? () => _moveForward(ref) - : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoForward() - ? () => _moveForward(ref) - : null, - showTooltip: false, - ), - ), - ), - ], + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () => _flipBoard(ref), + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoBack() + ? () => _moveBackward(ref) + : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoBack() + ? () => _moveBackward(ref) + : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, ), ), - ), + RepeatButton( + onLongPress: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoForward() + ? () => _moveForward(ref) + : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoForward() + ? () => _moveForward(ref) + : null, + showTooltip: false, + ), + ), + ], ); } diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 92c839b361..73cbd0f712 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -4,19 +4,24 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/tv/featured_player.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_game.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; +import 'package:lichess_mobile/src/model/user/streamer.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_tile.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/view/watch/live_tv_channels_screen.dart'; import 'package:lichess_mobile/src/view/watch/streamer_screen.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; @@ -25,9 +30,6 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'watch_tab_screen.g.dart'; const _featuredChannelsSet = ISetConst({ TvChannel.best, @@ -36,31 +38,27 @@ const _featuredChannelsSet = ISetConst({ TvChannel.rapid, }); -@riverpod -Future> featuredChannels(FeaturedChannelsRef ref) async { - return ref.withClientCacheFor( - (client) async { - final channels = await TvRepository(client).channels(); - return channels.entries - .where((channel) => _featuredChannelsSet.contains(channel.key)) - .map( - (entry) => TvGameSnapshot( - channel: entry.key, - id: entry.value.id, - orientation: entry.value.side ?? Side.white, - player: FeaturedPlayer( - name: entry.value.user.name, - title: entry.value.user.title, - side: entry.value.side ?? Side.white, - rating: entry.value.rating, - ), +final featuredChannelsProvider = FutureProvider.autoDispose>((ref) async { + return ref.withClientCacheFor((client) async { + final channels = await TvRepository(client).channels(); + return channels.entries + .where((channel) => _featuredChannelsSet.contains(channel.key)) + .map( + (entry) => TvGameSnapshot( + channel: entry.key, + id: entry.value.id, + orientation: entry.value.side ?? Side.white, + player: FeaturedPlayer( + name: entry.value.user.name, + title: entry.value.user.title, + side: entry.value.side ?? Side.white, + rating: entry.value.rating, ), - ) - .toIList(); - }, - const Duration(minutes: 5), - ); -} + ), + ) + .toIList(); + }, const Duration(minutes: 5)); +}); class WatchTabScreen extends ConsumerStatefulWidget { const WatchTabScreen({super.key}); @@ -80,20 +78,9 @@ class _WatchScreenState extends ConsumerState { } }); - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); + return ConsumerPlatformWidget(ref: ref, androidBuilder: _buildAndroid, iosBuilder: _buildIos); } - List get watchTabWidgets => const [ - // TODO: show widget when broadcasts feature is ready - //_BroadcastWidget(), - _WatchTvWidget(), - _StreamerWidget(), - ]; - Widget _buildAndroid(BuildContext context, WidgetRef ref) { return PopScope( canPop: false, @@ -103,32 +90,15 @@ class _WatchScreenState extends ConsumerState { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.watch), - ), - body: RefreshIndicator( - key: _androidRefreshKey, - onRefresh: refreshData, - child: SafeArea( - child: OrientationBuilder( - builder: (context, orientation) { - return orientation == Orientation.portrait - ? ListView( - controller: watchScrollController, - children: watchTabWidgets, - ) - : GridView( - controller: watchScrollController, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.92, - ), - children: watchTabWidgets, - ); - }, - ), - ), + appBar: AppBar(title: Text(context.l10n.watch)), + body: OrientationBuilder( + builder: (context, orientation) { + return RefreshIndicator( + key: _androidRefreshKey, + onRefresh: refreshData, + child: _Body(orientation), + ); + }, ), ), ); @@ -142,31 +112,10 @@ class _WatchScreenState extends ConsumerState { controller: watchScrollController, slivers: [ const CupertinoSliverNavigationBar( - padding: EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), - ), - CupertinoSliverRefreshControl( - onRefresh: refreshData, - ), - SliverSafeArea( - top: false, - sliver: orientation == Orientation.portrait - ? SliverList( - delegate: SliverChildListDelegate( - watchTabWidgets, - ), - ) - : SliverGrid( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.92, - ), - delegate: SliverChildListDelegate(watchTabWidgets), - ), + padding: EdgeInsetsDirectional.only(start: 16.0, end: 8.0), ), + CupertinoSliverRefreshControl(onRefresh: refreshData), + SliverSafeArea(top: false, sliver: _Body(orientation)), ], ); }, @@ -177,78 +126,193 @@ class _WatchScreenState extends ConsumerState { Future refreshData() => _refreshData(ref); } +class _Body extends ConsumerStatefulWidget { + const _Body(this.orientation); + + final Orientation orientation; + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + ImageColorWorker? _worker; + bool _imageAreCached = false; + + @override + void initState() { + super.initState(); + _precacheImages(); + } + + @override + void dispose() { + _worker?.close(); + super.dispose(); + } + + Future _precacheImages() async { + _worker = await ref.read(broadcastImageWorkerFactoryProvider).spawn(); + ref.listenManual(broadcastsPaginatorProvider, (_, current) async { + if (current.hasValue && !_imageAreCached) { + _imageAreCached = true; + try { + await preCacheBroadcastImages( + context, + broadcasts: current.value!.active, + worker: _worker!, + ); + } finally { + _worker?.close(); + } + } + }); + } + + @override + Widget build(BuildContext context) { + final broadcastList = ref.watch(broadcastsPaginatorProvider); + final featuredChannels = ref.watch(featuredChannelsProvider); + final streamers = ref.watch(liveStreamersProvider); + + final content = + widget.orientation == Orientation.portrait + ? [ + _BroadcastWidget(broadcastList), + _WatchTvWidget(featuredChannels), + _StreamerWidget(streamers), + ] + : [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _BroadcastWidget(broadcastList)), + Expanded(child: _WatchTvWidget(featuredChannels)), + ], + ), + _StreamerWidget(streamers), + ]; + + return Theme.of(context).platform == TargetPlatform.iOS + ? SliverList(delegate: SliverChildListDelegate(content)) + : ListView(controller: watchScrollController, children: content); + } +} + Future _refreshData(WidgetRef ref) { return Future.wait([ - // TODO uncomment when broadcasts feature is ready - // ref.refresh(broadcastsPaginatorProvider.future), + ref.refresh(broadcastsPaginatorProvider.future), ref.refresh(featuredChannelsProvider.future), ref.refresh(liveStreamersProvider.future), ]); } -// TODO remove this ignore comment when broadcasts feature is ready -// ignore: unused_element class _BroadcastWidget extends ConsumerWidget { - const _BroadcastWidget(); + final AsyncValue broadcastList; + + const _BroadcastWidget(this.broadcastList); static const int numberOfItems = 5; @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastList = ref.watch(broadcastsPaginatorProvider); - return broadcastList.when( data: (data) { return ListSection( - header: Text(context.l10n.broadcastBroadcasts), hasLeading: true, + header: Text(context.l10n.broadcastBroadcasts), headerTrailing: NoPaddingTextButton( onPressed: () { - pushPlatformRoute( - context, - builder: (context) => const BroadcastsListScreen(), - ); + pushPlatformRoute(context, builder: (context) => const BroadcastListScreen()); }, - child: Text( - context.l10n.more, - ), + child: Text(context.l10n.more), ), children: [ - ...CombinedIterableView([data.active, data.upcoming, data.past]) - .take(numberOfItems) - .map((broadcast) => BroadcastTile(broadcast: broadcast)), + ...CombinedIterableView([ + data.active, + data.past, + ]).take(numberOfItems).map((broadcast) => _BroadcastTile(broadcast: broadcast)), ], ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [BroadcastWidget] could not load broadcast data; $error\n $stackTrace', - ); - return Padding( + debugPrint('SEVERE: [BroadcastWidget] could not load broadcast data; $error\n $stackTrace'); + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load broadcasts'), + child: Text('Could not load broadcasts'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: numberOfItems, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: numberOfItems, header: true), + ), ), - ), + ); + } +} + +class _BroadcastTile extends ConsumerWidget { + const _BroadcastTile({required this.broadcast}); + + final Broadcast broadcast; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); + + return PlatformListTile( + onTap: () { + pushPlatformRoute( + context, + title: broadcast.title, + rootNavigator: true, + builder: (context) => BroadcastRoundScreen(broadcast: broadcast), + ); + }, + leading: + broadcast.tour.imageUrl != null + ? Image.network( + broadcast.tour.imageUrl!, + width: 50.0, + height: 50.0, + cacheWidth: (50.0 * devicePixelRatio).toInt(), + cacheHeight: (50.0 * devicePixelRatio).toInt(), + fit: BoxFit.cover, + errorBuilder: (context, _, __) => const Icon(LichessIcons.radio_tower_lichess), + ) + : const Image(image: kDefaultBroadcastImage), + subtitle: Row( + children: [ + Text(broadcast.round.name), + if (broadcast.isLive) ...[ + const SizedBox(width: 5.0), + Text( + 'LIVE', + style: TextStyle(color: context.lichessColors.error, fontWeight: FontWeight.bold), + ), + ] else if (broadcast.round.startsAt != null) ...[ + const SizedBox(width: 5.0), + Text(relativeDate(context.l10n, broadcast.round.startsAt!)), + ], + ], + ), + title: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: Text(broadcast.title, maxLines: 1, overflow: TextOverflow.ellipsis), ), ); } } class _WatchTvWidget extends ConsumerWidget { - const _WatchTvWidget(); + final AsyncValue> featuredChannels; + + const _WatchTvWidget(this.featuredChannels); @override Widget build(BuildContext context, WidgetRef ref) { - final featuredChannels = ref.watch(featuredChannelsProvider); - return featuredChannels.when( data: (data) { if (data.isEmpty) { @@ -258,79 +322,76 @@ class _WatchTvWidget extends ConsumerWidget { header: const Text('Lichess TV'), hasLeading: true, headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => const LiveTvChannelsScreen(), - ).then((_) => _refreshData(ref)), - child: Text( - context.l10n.more, - ), + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => const LiveTvChannelsScreen(), + ).then((_) => _refreshData(ref)), + child: Text(context.l10n.more), ), - children: data.map((snapshot) { - return PlatformListTile( - leading: Icon(snapshot.channel.icon), - title: Text(snapshot.channel.label), - subtitle: UserFullNameWidget.player( - user: snapshot.player.asPlayer.user, - aiLevel: snapshot.player.asPlayer.aiLevel, - rating: snapshot.player.rating, - ), - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => TvScreen(channel: snapshot.channel), - ).then((_) => _refreshData(ref)), - ); - }).toList(growable: false), + children: data + .map((snapshot) { + return PlatformListTile( + leading: Icon(snapshot.channel.icon), + title: Text(snapshot.channel.label), + subtitle: UserFullNameWidget.player( + user: snapshot.player.asPlayer.user, + aiLevel: snapshot.player.asPlayer.aiLevel, + rating: snapshot.player.rating, + ), + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => TvScreen(channel: snapshot.channel), + ).then((_) => _refreshData(ref)), + ); + }) + .toList(growable: false), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [StreamerWidget] could not load channels data; $error\n $stackTrace', - ); - return Padding( + debugPrint('SEVERE: [StreamerWidget] could not load channels data; $error\n $stackTrace'); + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load TV channels'), + child: Text('Could not load TV channels'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 4, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 4, header: true), + ), ), - ), - ), ); } } class _StreamerWidget extends ConsumerWidget { - const _StreamerWidget(); + final AsyncValue> streamers; + + const _StreamerWidget(this.streamers); static const int numberOfItems = 10; @override Widget build(BuildContext context, WidgetRef ref) { - final streamerState = ref.watch(liveStreamersProvider); - - return streamerState.when( + return streamers.when( data: (data) { if (data.isEmpty) { return const SizedBox.shrink(); } return ListSection( - header: Text(context.l10n.streamerLichessStreamers), + header: Text(context.l10n.streamersMenu), hasLeading: true, headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => StreamerScreen(streamers: data), - ), - child: Text( - context.l10n.more, - ), + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => StreamerScreen(streamers: data), + ), + child: Text(context.l10n.more), ), children: [ ...data @@ -340,23 +401,19 @@ class _StreamerWidget extends ConsumerWidget { ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [StreamerWidget] could not load streamer data; $error\n $stackTrace', - ); - return Padding( + debugPrint('SEVERE: [StreamerWidget] could not load streamer data; $error\n $stackTrace'); + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load live streamers'), + child: Text('Could not load live streamers'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: numberOfItems, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: numberOfItems, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/widgets/adaptive_action_sheet.dart b/lib/src/widgets/adaptive_action_sheet.dart index 922dc41542..c7187a47d5 100644 --- a/lib/src/widgets/adaptive_action_sheet.dart +++ b/lib/src/widgets/adaptive_action_sheet.dart @@ -64,18 +64,14 @@ Future showConfirmDialog( title: title, actions: [ TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.mobileOkButton), onPressed: () { Navigator.of(context).pop(); @@ -101,27 +97,29 @@ Future showCupertinoActionSheet({ builder: (BuildContext context) { return CupertinoActionSheet( title: title, - actions: actions - .map( - // Builder is used to retrieve the context immediately surrounding the button - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - (action) => Builder( - builder: (context) { - return CupertinoActionSheetAction( - onPressed: () { - if (action.dismissOnPress) { - Navigator.of(context).pop(); - } - action.onPressed(context); + actions: + actions + .map( + // Builder is used to retrieve the context immediately surrounding the button + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + (action) => Builder( + builder: (context) { + return CupertinoActionSheetAction( + onPressed: () { + if (action.dismissOnPress) { + Navigator.of(context).pop(); + } + action.onPressed(context); + }, + isDestructiveAction: action.isDestructiveAction, + isDefaultAction: action.isDefaultAction, + child: action.makeLabel(context), + ); }, - isDestructiveAction: action.isDestructiveAction, - child: action.makeLabel(context), - ); - }, - ), - ) - .toList(), + ), + ) + .toList(), cancelButton: CupertinoActionSheetAction( isDefaultAction: true, onPressed: () { @@ -140,8 +138,7 @@ Future showMaterialActionSheet({ required List actions, bool isDismissible = true, }) { - final defaultTextStyle = - Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 20); + final actionTextStyle = Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 18); final screenWidth = MediaQuery.of(context).size.width; return showDialog( @@ -157,20 +154,13 @@ Future showMaterialActionSheet({ mainAxisSize: MainAxisSize.min, children: [ if (title != null) ...[ - Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: title), - ), + Padding(padding: const EdgeInsets.all(16.0), child: Center(child: title)), ], ...actions.mapIndexed((index, action) { return InkWell( borderRadius: BorderRadius.vertical( - top: Radius.circular( - index == 0 ? 28 : 0, - ), - bottom: Radius.circular( - index == actions.length - 1 ? 28 : 0, - ), + top: Radius.circular(index == 0 ? 28 : 0), + bottom: Radius.circular(index == actions.length - 1 ? 28 : 0), ), onTap: () { if (action.dismissOnPress) { @@ -188,10 +178,9 @@ Future showMaterialActionSheet({ ], Expanded( child: DefaultTextStyle( - style: defaultTextStyle, - textAlign: action.leading != null - ? TextAlign.start - : TextAlign.center, + style: actionTextStyle, + textAlign: + action.leading != null ? TextAlign.start : TextAlign.center, child: action.makeLabel(context), ), ), @@ -233,16 +222,20 @@ class BottomSheetAction { /// A widget to display after the label. /// - /// Typically an [Icon] widget. Only for android. + /// Typically an [Icon] widget. (Android only). final Widget? trailing; /// A widget to display before the label. /// - /// Typically an [Icon] or a [CircleAvatar] widget. Only for android. + /// Typically an [Icon] or a [CircleAvatar] widget. (Android only). final Widget? leading; + /// Whether the action is destructive. (iOS only). final bool isDestructiveAction; + /// Whether the action is the default action. (iOS only). + final bool isDefaultAction; + BottomSheetAction({ required this.makeLabel, required this.onPressed, @@ -250,5 +243,6 @@ class BottomSheetAction { this.trailing, this.leading, this.isDestructiveAction = false, + this.isDefaultAction = false, }); } diff --git a/lib/src/widgets/adaptive_autocomplete.dart b/lib/src/widgets/adaptive_autocomplete.dart index a561aad408..20fc79a014 100644 --- a/lib/src/widgets/adaptive_autocomplete.dart +++ b/lib/src/widgets/adaptive_autocomplete.dart @@ -25,77 +25,74 @@ class AdaptiveAutoComplete extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS ? RawAutocomplete( - initialValue: initialValue, - optionsBuilder: optionsBuilder, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted, - ) { - return CupertinoTextField( - controller: textEditingController, - decoration: cupertinoDecoration, - textInputAction: textInputAction, - focusNode: focusNode, - onSubmitted: (String value) { - onFieldSubmitted(); - }, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Align( - alignment: Alignment.topLeft, - child: ColoredBox( - color: CupertinoColors.secondarySystemGroupedBackground - .resolveFrom(context), - child: SizedBox( - height: 200.0, - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final T option = options.elementAt(index); - return AdaptiveInkWell( - onTap: () { - onSelected(option); - }, - child: ListTile( - title: Text(displayStringForOption(option)), - ), - ); - }, - ), + initialValue: initialValue, + optionsBuilder: optionsBuilder, + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return CupertinoTextField( + controller: textEditingController, + decoration: cupertinoDecoration, + textInputAction: textInputAction, + focusNode: focusNode, + onSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + return Align( + alignment: Alignment.topLeft, + child: ColoredBox( + color: CupertinoColors.secondarySystemGroupedBackground.resolveFrom(context), + child: SizedBox( + height: 200.0, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final T option = options.elementAt(index); + return AdaptiveInkWell( + onTap: () { + onSelected(option); + }, + child: ListTile(title: Text(displayStringForOption(option))), + ); + }, ), ), - ); - }, - onSelected: onSelected, - ) + ), + ); + }, + onSelected: onSelected, + ) : Autocomplete( - initialValue: initialValue, - optionsBuilder: optionsBuilder, - onSelected: onSelected, - displayStringForOption: displayStringForOption, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted, - ) { - return TextField( - controller: textEditingController, - textInputAction: textInputAction, - focusNode: focusNode, - onSubmitted: (String value) { - onFieldSubmitted(); - }, - ); - }, - ); + initialValue: initialValue, + optionsBuilder: optionsBuilder, + onSelected: onSelected, + displayStringForOption: displayStringForOption, + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return TextField( + controller: textEditingController, + textInputAction: textInputAction, + focusNode: focusNode, + onSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + ); } } diff --git a/lib/src/widgets/adaptive_bottom_sheet.dart b/lib/src/widgets/adaptive_bottom_sheet.dart index 072eba48d7..16e964563a 100644 --- a/lib/src/widgets/adaptive_bottom_sheet.dart +++ b/lib/src/widgets/adaptive_bottom_sheet.dart @@ -21,25 +21,54 @@ Future showAdaptiveBottomSheet({ isScrollControlled: isScrollControlled, useRootNavigator: useRootNavigator, useSafeArea: useSafeArea, - shape: Theme.of(context).platform == TargetPlatform.iOS - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(10.0), - ), - ) - : null, + shape: + Theme.of(context).platform == TargetPlatform.iOS + ? const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(10.0)), + ) + : null, constraints: constraints, - backgroundColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemGroupedBackground, - context, - ) - : null, + backgroundColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemGroupedBackground, + context, + ) + : null, elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0 : null, builder: builder, ); } +/// A modal bottom sheet container that adapts to the content size. +/// +/// This is typically used with [showAdaptiveBottomSheet] to display a +/// context menu. +/// +/// This is meant for content that mostly fits on the screen, not for long lists. +class BottomSheetScrollableContainer extends StatelessWidget { + const BottomSheetScrollableContainer({ + required this.children, + this.padding = const EdgeInsets.only(bottom: 16.0), + this.scrollController, + }); + + final List children; + final EdgeInsetsGeometry? padding; + final ScrollController? scrollController; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: SingleChildScrollView( + controller: scrollController, + padding: padding, + child: ListBody(children: children), + ), + ); + } +} + class BottomSheetContextMenuAction extends StatelessWidget { const BottomSheetContextMenuAction({ required this.child, @@ -56,8 +85,9 @@ class BottomSheetContextMenuAction extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - cupertinoBackgroundColor: - CupertinoColors.tertiarySystemGroupedBackground.resolveFrom(context), + cupertinoBackgroundColor: CupertinoColors.tertiarySystemGroupedBackground.resolveFrom( + context, + ), leading: Icon(icon), title: child, onTap: () { diff --git a/lib/src/widgets/adaptive_choice_picker.dart b/lib/src/widgets/adaptive_choice_picker.dart index 933ceaeff8..89a65fa1ad 100644 --- a/lib/src/widgets/adaptive_choice_picker.dart +++ b/lib/src/widgets/adaptive_choice_picker.dart @@ -25,32 +25,28 @@ Future showChoicePicker( scrollable: true, content: Builder( builder: (context) { - final List choiceWidgets = choices.map((value) { - return RadioListTile( - title: labelBuilder(value), - value: value, - groupValue: selectedItem, - onChanged: (value) { - if (value != null && onSelectedItemChanged != null) { - onSelectedItemChanged(value); - Navigator.of(context).pop(); - } - }, - ); - }).toList(growable: false); + final List choiceWidgets = choices + .map((value) { + return RadioListTile( + title: labelBuilder(value), + value: value, + groupValue: selectedItem, + onChanged: (value) { + if (value != null && onSelectedItemChanged != null) { + onSelectedItemChanged(value); + Navigator.of(context).pop(); + } + }, + ); + }) + .toList(growable: false); return choiceWidgets.length >= 10 ? SizedBox( - width: double.maxFinite, - height: deviceHeight * 0.6, - child: ListView( - shrinkWrap: true, - children: choiceWidgets, - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: choiceWidgets, - ); + width: double.maxFinite, + height: deviceHeight * 0.6, + child: ListView(shrinkWrap: true, children: choiceWidgets), + ) + : Column(mainAxisSize: MainAxisSize.min, children: choiceWidgets); }, ), actions: [ @@ -68,17 +64,18 @@ Future showChoicePicker( context: context, builder: (context) { return CupertinoActionSheet( - actions: choices.map((value) { - return CupertinoActionSheetAction( - onPressed: () { - if (onSelectedItemChanged != null) { - onSelectedItemChanged(value); - } - Navigator.of(context).pop(); - }, - child: labelBuilder(value), - ); - }).toList(), + actions: + choices.map((value) { + return CupertinoActionSheetAction( + onPressed: () { + if (onSelectedItemChanged != null) { + onSelectedItemChanged(value); + } + Navigator.of(context).pop(); + }, + child: labelBuilder(value), + ); + }).toList(), cancelButton: CupertinoActionSheetAction( isDefaultAction: true, onPressed: () => Navigator.of(context).pop(), @@ -94,8 +91,7 @@ Future showChoicePicker( return NotificationListener( onNotification: (ScrollEndNotification notification) { if (onSelectedItemChanged != null) { - final index = - (notification.metrics as FixedExtentMetrics).itemIndex; + final index = (notification.metrics as FixedExtentMetrics).itemIndex; onSelectedItemChanged(choices[index]); } return false; @@ -110,11 +106,10 @@ Future showChoicePicker( scrollController: FixedExtentScrollController( initialItem: choices.indexWhere((t) => t == selectedItem), ), - children: choices.map((value) { - return Center( - child: labelBuilder(value), - ); - }).toList(), + children: + choices.map((value) { + return Center(child: labelBuilder(value)); + }).toList(), onSelectedItemChanged: (_) {}, ), ), @@ -144,46 +139,47 @@ Future?> showMultipleChoicesPicker( builder: (BuildContext context, StateSetter setState) { return Column( mainAxisSize: MainAxisSize.min, - children: choices.map((choice) { - return CheckboxListTile.adaptive( - title: labelBuilder(choice), - value: items.contains(choice), - onChanged: (value) { - if (value != null) { - setState(() { - items = value - ? items.union({choice}) - : items.difference({choice}); - }); - } - }, - ); - }).toList(growable: false), + children: choices + .map((choice) { + return CheckboxListTile.adaptive( + title: labelBuilder(choice), + value: items.contains(choice), + onChanged: (value) { + if (value != null) { + setState(() { + items = value ? items.union({choice}) : items.difference({choice}); + }); + } + }, + ); + }) + .toList(growable: false), ); }, ), - actions: Theme.of(context).platform == TargetPlatform.iOS - ? [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.cancel), - ), - CupertinoDialogAction( - isDefaultAction: true, - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ] - : [ - TextButton( - child: Text(context.l10n.cancel), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ], + actions: + Theme.of(context).platform == TargetPlatform.iOS + ? [ + CupertinoDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), + ), + CupertinoDialogAction( + isDefaultAction: true, + child: Text(context.l10n.mobileOkButton), + onPressed: () => Navigator.of(context).pop(items), + ), + ] + : [ + TextButton( + child: Text(context.l10n.cancel), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: Text(context.l10n.mobileOkButton), + onPressed: () => Navigator.of(context).pop(items), + ), + ], ); }, ); diff --git a/lib/src/widgets/adaptive_date_picker.dart b/lib/src/widgets/adaptive_date_picker.dart index 0ab55691f7..1bbc828592 100644 --- a/lib/src/widgets/adaptive_date_picker.dart +++ b/lib/src/widgets/adaptive_date_picker.dart @@ -18,8 +18,7 @@ void showAdaptiveDatePicker( return SizedBox( height: 250, child: CupertinoDatePicker( - backgroundColor: - CupertinoColors.systemBackground.resolveFrom(context), + backgroundColor: CupertinoColors.systemBackground.resolveFrom(context), initialDateTime: initialDate, minimumDate: firstDate, maximumDate: lastDate, diff --git a/lib/src/widgets/adaptive_text_field.dart b/lib/src/widgets/adaptive_text_field.dart index 3e77a84155..483f814dea 100644 --- a/lib/src/widgets/adaptive_text_field.dart +++ b/lib/src/widgets/adaptive_text_field.dart @@ -40,8 +40,7 @@ class AdaptiveTextField extends StatelessWidget { final void Function(String)? onChanged; final void Function(String)? onSubmitted; final GestureTapCallback? onTap; - final Widget? - suffix; //used only for iOS, suffix should be put in InputDecoration for android + final Widget? suffix; //used only for iOS, suffix should be put in InputDecoration for android final BoxDecoration? cupertinoDecoration; final InputDecoration? materialDecoration; @@ -75,10 +74,7 @@ class AdaptiveTextField extends StatelessWidget { maxLines: maxLines, maxLength: maxLength, expands: expands, - decoration: materialDecoration ?? - InputDecoration( - hintText: placeholder, - ), + decoration: materialDecoration ?? InputDecoration(hintText: placeholder), controller: controller, focusNode: focusNode, textInputAction: textInputAction, diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index ddb83cd537..a8b1a0ba3b 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -8,8 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -const _kBoardCarouselItemMargin = - EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); +const _kBoardCarouselItemMargin = EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); class BoardCarouselItem extends ConsumerWidget { const BoardCarouselItem({ @@ -50,86 +50,82 @@ class BoardCarouselItem extends ConsumerWidget { return LayoutBuilder( builder: (context, constraints) { - final boardSize = constraints.biggest.shortestSide - - _kBoardCarouselItemMargin.horizontal; - final card = PlatformCard( - color: backgroundColor, - margin: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : _kBoardCarouselItemMargin, - child: AdaptiveInkWell( - splashColor: splashColor, - borderRadius: BorderRadius.circular(10), - onTap: onTap, - child: Stack( - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.center, - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withOpacity(0.25), - backgroundColor.withOpacity(1.0), - ], - stops: const [0.3, 1.00], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, - child: SizedBox( - height: boardSize, - child: Board( - size: boardSize, - data: BoardData( - interactableSide: InteractableSide.none, + final boardSize = constraints.biggest.shortestSide - _kBoardCarouselItemMargin.horizontal; + final card = BrightnessHueFilter( + hue: boardPrefs.hue, + brightness: boardPrefs.brightness, + child: PlatformCard( + color: backgroundColor, + margin: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : _kBoardCarouselItemMargin, + child: AdaptiveInkWell( + splashColor: splashColor, + borderRadius: BorderRadius.circular(10), + onTap: onTap, + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.center, + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.25), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.3, 1.00], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: SizedBox( + height: boardSize, + child: Chessboard.fixed( + size: boardSize, fen: fen, orientation: orientation, lastMove: lastMove, - ), - settings: BoardSettings( - enableCoordinates: false, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), + settings: ChessboardSettings( + enableCoordinates: false, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, ), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, ), ), ), - ), - Positioned( - left: 0, - bottom: 8, - child: DefaultTextStyle.merge( - style: const TextStyle( - color: Colors.white, + Positioned( + left: 0, + bottom: 8, + child: DefaultTextStyle.merge( + style: const TextStyle(color: Colors.white), + child: description, ), - child: description, ), - ), - ], + ], + ), ), ), ); return Theme.of(context).platform == TargetPlatform.iOS ? Padding( - padding: _kBoardCarouselItemMargin, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 6.0, - ), - ], - ), - child: card, + padding: _kBoardCarouselItemMargin, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6.0), + ], ), - ) + child: card, + ), + ) : card; }, ); @@ -180,10 +176,10 @@ class BoardsPageView extends StatefulWidget { this.scrollBehavior, this.padding = EdgeInsets.zero, }) : childrenDelegate = SliverChildBuilderDelegate( - itemBuilder, - findChildIndexCallback: findChildIndexCallback, - childCount: itemCount, - ); + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + ); final bool allowImplicitScrolling; final String? restorationId; @@ -243,11 +239,8 @@ class _BoardsPageViewState extends State { case Axis.horizontal: assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - final AxisDirection axisDirection = - textDirectionToAxisDirection(textDirection); - return widget.reverse - ? flipAxisDirection(axisDirection) - : axisDirection; + final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); + return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: return widget.reverse ? AxisDirection.up : AxisDirection.down; } @@ -261,9 +254,8 @@ class _BoardsPageViewState extends State { ).applyTo( widget.pageSnapping ? _kPagePhysics.applyTo( - widget.physics ?? - widget.scrollBehavior?.getScrollPhysics(context), - ) + widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), + ) : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), ); @@ -287,8 +279,8 @@ class _BoardsPageViewState extends State { controller: _controller, physics: physics, restorationId: widget.restorationId, - scrollBehavior: widget.scrollBehavior ?? - ScrollConfiguration.of(context).copyWith(scrollbars: false), + scrollBehavior: + widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false), viewportBuilder: (BuildContext context, ViewportOffset position) { return Viewport( cacheExtent: 0, @@ -311,11 +303,8 @@ class _BoardsPageViewState extends State { } class _SliverFillViewport extends StatelessWidget { - const _SliverFillViewport({ - required this.delegate, - this.viewportFraction = 1.0, - this.padding, - }) : assert(viewportFraction > 0.0); + const _SliverFillViewport({required this.delegate, this.viewportFraction = 1.0, this.padding}) + : assert(viewportFraction > 0.0); final double viewportFraction; @@ -335,8 +324,7 @@ class _SliverFillViewport extends StatelessWidget { } } -class _SliverFillViewportRenderObjectWidget - extends SliverMultiBoxAdaptorWidget { +class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget { const _SliverFillViewportRenderObjectWidget({ required super.delegate, this.viewportFraction = 1.0, @@ -346,28 +334,18 @@ class _SliverFillViewportRenderObjectWidget @override RenderSliverFillViewport createRenderObject(BuildContext context) { - final SliverMultiBoxAdaptorElement element = - context as SliverMultiBoxAdaptorElement; - return RenderSliverFillViewport( - childManager: element, - viewportFraction: viewportFraction, - ); + final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; + return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction); } @override - void updateRenderObject( - BuildContext context, - RenderSliverFillViewport renderObject, - ) { + void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) { renderObject.viewportFraction = viewportFraction; } } class _ForceImplicitScrollPhysics extends ScrollPhysics { - const _ForceImplicitScrollPhysics({ - required this.allowImplicitScrolling, - super.parent, - }); + const _ForceImplicitScrollPhysics({required this.allowImplicitScrolling, super.parent}); @override _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index c004cfb4a3..0f74802a2a 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,7 +16,15 @@ class SmallBoardPreview extends ConsumerStatefulWidget { this.padding, this.lastMove, this.onTap, - }); + }) : _showLoadingPlaceholder = false; + + const SmallBoardPreview.loading({this.padding}) + : orientation = Side.white, + fen = kEmptyFEN, + lastMove = null, + description = const SizedBox.shrink(), + onTap = null, + _showLoadingPlaceholder = true; /// Side by which the board is oriented. final Side orientation; @@ -32,6 +41,8 @@ class SmallBoardPreview extends ConsumerStatefulWidget { final EdgeInsetsGeometry? padding; + final bool _showLoadingPlaceholder; + @override ConsumerState createState() => _SmallBoardPreviewState(); } @@ -45,45 +56,99 @@ class _SmallBoardPreviewState extends ConsumerState { final content = LayoutBuilder( builder: (context, constraints) { - final boardSize = constraints.biggest.shortestSide - - (constraints.biggest.shortestSide / 1.618); + final boardSize = + constraints.biggest.shortestSide - (constraints.biggest.shortestSide / 1.618); return Container( decoration: BoxDecoration( - color: _isPressed - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : null, + color: + _isPressed + ? CupertinoDynamicColor.resolve(CupertinoColors.systemGrey5, context) + : null, ), child: Padding( - padding: widget.padding ?? - Styles.horizontalBodyPadding - .add(const EdgeInsets.symmetric(vertical: 8.0)), + padding: + widget.padding ?? + Styles.horizontalBodyPadding.add(const EdgeInsets.symmetric(vertical: 8.0)), child: SizedBox( height: boardSize, child: Row( children: [ - Board( - size: boardSize, - data: BoardData( - interactableSide: InteractableSide.none, + if (widget._showLoadingPlaceholder) + Container( + width: boardSize, + height: boardSize, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ) + else + Chessboard.fixed( + size: boardSize, fen: widget.fen, orientation: widget.orientation, - lastMove: widget.lastMove, - ), - settings: BoardSettings( - enableCoordinates: false, - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - animationDuration: const Duration(milliseconds: 150), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, + lastMove: widget.lastMove as NormalMove?, + settings: ChessboardSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + brightness: boardPrefs.brightness, + hue: boardPrefs.hue, + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + animationDuration: const Duration(milliseconds: 150), + ), ), - ), const SizedBox(width: 10.0), - Expanded(child: widget.description), + if (widget._showLoadingPlaceholder) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 16.0, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + const SizedBox(height: 4.0), + Container( + height: 16.0, + width: MediaQuery.sizeOf(context).width / 3, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + ], + ), + Container( + height: 44.0, + width: 44.0, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + Container( + height: 16.0, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + ], + ), + ) + else + Expanded(child: widget.description), ], ), ), @@ -95,16 +160,13 @@ class _SmallBoardPreviewState extends ConsumerState { return widget.onTap != null ? Theme.of(context).platform == TargetPlatform.iOS ? GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onTap, - child: content, - ) - : InkWell( - onTap: widget.onTap, - child: content, - ) + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onTap, + child: content, + ) + : InkWell(onTap: widget.onTap, child: content) : content; } } diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 7d364113a0..a9d8f06f02 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -1,21 +1,15 @@ import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; - -import 'platform.dart'; - -const _scrollAnimationDuration = Duration(milliseconds: 200); -const _moveListOpacity = 0.6; +import 'package:lichess_mobile/src/widgets/move_list.dart'; /// Board layout that adapts to screen size and aspect ratio. /// @@ -28,13 +22,16 @@ const _moveListOpacity = 0.6; /// /// An optional overlay or error message can be displayed on top of the board. class BoardTable extends ConsumerStatefulWidget { + /// Creates a board table with the given values. const BoardTable({ - this.onMove, - this.onPremove, - required this.boardData, + required this.fen, + required this.orientation, + this.gameData, + this.lastMove, this.boardSettingsOverrides, - required this.topTable, - required this.bottomTable, + this.topTable = const SizedBox.shrink(), + this.bottomTable = const SizedBox.shrink(), + this.shapes, this.engineGauge, this.moves, this.currentMoveIndex, @@ -47,17 +44,43 @@ class BoardTable extends ConsumerStatefulWidget { this.zenMode = false, super.key, }) : assert( - moves == null || currentMoveIndex != null, - 'You must provide `currentMoveIndex` along with `moves`', - ); + moves == null || currentMoveIndex != null, + 'You must provide `currentMoveIndex` along with `moves`', + ); - final void Function(Move, {bool? isDrop, bool? isPremove})? onMove; - final void Function(Move?)? onPremove; - - final BoardData boardData; + /// Creates an empty board table (useful for loading). + const BoardTable.empty({ + this.showMoveListPlaceholder = false, + this.showEngineGaugePlaceholder = false, + this.errorMessage, + }) : fen = kEmptyBoardFEN, + orientation = Side.white, + gameData = null, + lastMove = null, + boardSettingsOverrides = null, + topTable = const SizedBox.shrink(), + bottomTable = const SizedBox.shrink(), + shapes = null, + engineGauge = null, + moves = null, + currentMoveIndex = null, + onSelectMove = null, + boardOverlay = null, + boardKey = null, + zenMode = false; + + final String fen; + + final Side orientation; + + final GameData? gameData; + + final Move? lastMove; final BoardSettingsOverrides? boardSettingsOverrides; + final ISet? shapes; + /// [GlobalKey] for the board. /// /// Used to set gestures exclusion on android. @@ -109,217 +132,168 @@ class _BoardTableState extends ConsumerState { return LayoutBuilder( builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; final isTablet = isTabletOrLarger(context); - final boardSize = isTablet - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - // vertical space left on portrait mode to check if we can display the - // move list - final verticalSpaceLeftBoardOnPortrait = - constraints.biggest.height - boardSize; - - final error = widget.errorMessage != null - ? SizedBox.square( - dimension: boardSize, - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondarySystemBackground - .resolveFrom(context) - : Theme.of(context).colorScheme.surface, - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), - ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text(widget.errorMessage!), - ), - ), - ), - ), - ) - : null; final defaultSettings = boardPrefs.toBoardSettings().copyWith( - borderRadius: isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: isTablet ? boardShadows : const [], - drawShape: DrawShapeOptions( - enable: boardPrefs.enableShapeDrawings, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - ), - ); - - final settings = widget.boardSettingsOverrides != null - ? widget.boardSettingsOverrides!.merge(defaultSettings) - : defaultSettings; - - final board = Board( - key: widget.boardKey, - size: boardSize, - data: widget.boardData.copyWith( - shapes: userShapes.union(widget.boardData.shapes ?? ISet()), + borderRadius: isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, ), - settings: settings, - onMove: widget.onMove, - onPremove: widget.onPremove, ); - Widget boardWidget = board; + final settings = + widget.boardSettingsOverrides != null + ? widget.boardSettingsOverrides!.merge(defaultSettings) + : defaultSettings; + + final shapes = userShapes.union(widget.shapes ?? ISet()); + final slicedMoves = widget.moves?.asMap().entries.slices(2); - if (widget.boardOverlay != null) { - boardWidget = SizedBox.square( - dimension: boardSize, - child: Stack( + if (orientation == Orientation.landscape) { + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); + final sideWidth = constraints.biggest.longestSide - defaultBoardSize; + final boardSize = + sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); + return Padding( + padding: const EdgeInsets.all(kTabletBoardTableSidePadding), + child: Row( + mainAxisSize: MainAxisSize.max, children: [ - board, - SizedBox.square( - dimension: boardSize, - child: Center( - child: SizedBox( - width: (boardSize / 8) * 6.6, - height: (boardSize / 8) * 4.6, - child: widget.boardOverlay, - ), + _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey, + boardOverlay: widget.boardOverlay, + error: widget.errorMessage, + ), + if (widget.engineGauge != null) ...[ + const SizedBox(width: 4.0), + EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.vertical, + ), + ] else if (widget.showEngineGaugePlaceholder) ...[ + const SizedBox(width: kEvalGaugeSize + 4.0), + ], + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + widget.topTable, + if (!widget.zenMode && slicedMoves != null) + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: MoveList( + type: MoveListType.stacked, + slicedMoves: slicedMoves, + currentMoveIndex: widget.currentMoveIndex ?? 0, + onSelectMove: widget.onSelectMove, + ), + ), + ) + else + const Spacer(), + widget.bottomTable, + ], ), ), ], ), ); - } else if (error != null) { - boardWidget = SizedBox.square( - dimension: boardSize, - child: Stack( - children: [ - board, - error, - ], - ), - ); - } - - final slicedMoves = widget.moves?.asMap().entries.slices(2); - - return aspectRatio > 1 - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - boardWidget, - if (widget.engineGauge != null) - EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.vertical, - ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(width: kEvalGaugeSize), - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Padding( - padding: - const EdgeInsets.all(kTabletBoardTableSidePadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Flexible(child: widget.topTable), - if (!widget.zenMode && slicedMoves != null) - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: MoveList( - type: MoveListType.stacked, - slicedMoves: slicedMoves, - currentMoveIndex: - widget.currentMoveIndex ?? 0, - onSelectMove: widget.onSelectMove, - ), - ), - ) - else - // same height as [MoveList] - const Expanded( - child: Padding( - padding: EdgeInsets.all(16.0), - child: SizedBox(height: 40), - ), - ), - Flexible(child: widget.bottomTable), - ], - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!widget.zenMode && - slicedMoves != null && - verticalSpaceLeftBoardOnPortrait >= 130) - MoveList( - type: MoveListType.inline, - slicedMoves: slicedMoves, - currentMoveIndex: widget.currentMoveIndex ?? 0, - onSelectMove: widget.onSelectMove, - ) - else if (widget.showMoveListPlaceholder && - verticalSpaceLeftBoardOnPortrait >= 130) - const SizedBox(height: 40), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, - ), - child: widget.topTable, - ), + } else { + final defaultBoardSize = constraints.biggest.shortestSide; + final double boardSize = + isTablet ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; + + // vertical space left on portrait mode to check if we can display the + // move list + final verticalSpaceLeftBoardOnPortrait = constraints.biggest.height - boardSize; + + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!widget.zenMode && slicedMoves != null && verticalSpaceLeftBoardOnPortrait >= 130) + MoveList( + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: widget.currentMoveIndex ?? 0, + onSelectMove: widget.onSelectMove, + ) + else if (widget.showMoveListPlaceholder && verticalSpaceLeftBoardOnPortrait >= 130) + const SizedBox(height: 40), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? kTabletBoardTableSidePadding : 12.0, ), - if (widget.engineGauge != null) - Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) + child: widget.topTable, + ), + ), + if (widget.engineGauge != null) + Padding( + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) : EdgeInsets.zero, - child: EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.horizontal, - ), - ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(height: kEvalGaugeSize), - boardWidget, - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, - ), - child: widget.bottomTable, - ), + child: EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.horizontal, ), - ], - ); + ) + else if (widget.showEngineGaugePlaceholder) + const SizedBox(height: kEvalGaugeSize), + Padding( + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, + child: _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey, + boardOverlay: widget.boardOverlay, + error: widget.errorMessage, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? kTabletBoardTableSidePadding : 12.0, + ), + child: widget.bottomTable, + ), + ), + ], + ); + } }, ); } @@ -344,294 +318,132 @@ class _BoardTableState extends ConsumerState { } } -class BoardSettingsOverrides { - const BoardSettingsOverrides({ - this.animationDuration, - this.autoQueenPromotion, - this.autoQueenPromotionOnPremove, - this.blindfoldMode, - }); - - final Duration? animationDuration; - final bool? autoQueenPromotion; - final bool? autoQueenPromotionOnPremove; - final bool? blindfoldMode; - - BoardSettings merge(BoardSettings settings) { - return settings.copyWith( - animationDuration: animationDuration, - autoQueenPromotion: autoQueenPromotion, - autoQueenPromotionOnPremove: autoQueenPromotionOnPremove, - blindfoldMode: blindfoldMode, - ); - } -} - -enum MoveListType { inline, stacked } - -class MoveList extends ConsumerStatefulWidget { - const MoveList({ - required this.type, - required this.slicedMoves, - required this.currentMoveIndex, - this.onSelectMove, +class _BoardWidget extends StatelessWidget { + const _BoardWidget({ + required this.size, + required this.boardPrefs, + required this.fen, + required this.orientation, + required this.gameData, + required this.lastMove, + required this.shapes, + required this.settings, + required this.boardOverlay, + required this.error, + this.boardKey, }); - final MoveListType type; - - final Iterable>> slicedMoves; - - final int currentMoveIndex; - final void Function(int moveIndex)? onSelectMove; - - @override - ConsumerState createState() => _MoveListState(); -} - -class _MoveListState extends ConsumerState { - final currentMoveKey = GlobalKey(); - final _debounce = Debouncer(const Duration(milliseconds: 100)); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - ); - } - }); - } - - @override - void dispose() { - _debounce.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant MoveList oldWidget) { - super.didUpdateWidget(oldWidget); - _debounce(() { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - duration: _scrollAnimationDuration, - curve: Curves.easeIn, - ); - } - }); - } + final double size; + final BoardPrefs boardPrefs; + final String fen; + final Side orientation; + final GameData? gameData; + final Move? lastMove; + final ISet shapes; + final ChessboardSettings settings; + final String? error; + final Widget? boardOverlay; + final GlobalKey? boardKey; @override Widget build(BuildContext context) { - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); + final board = Chessboard( + key: boardKey, + size: size, + fen: fen, + orientation: orientation, + game: gameData, + lastMove: lastMove, + shapes: shapes, + settings: settings, + ); - return widget.type == MoveListType.inline - ? Container( - padding: const EdgeInsets.only(left: 5), - height: 40, - width: double.infinity, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: widget.slicedMoves - .mapIndexed( - (index, moves) => Container( - margin: const EdgeInsets.only(right: 10), - child: Row( - children: [ - InlineMoveCount(count: index + 1), - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == move.key + 1; - return InlineMoveItem( - key: isCurrentMove ? currentMoveKey : null, - move: move, - pieceNotation: pieceNotation, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ); - }, - ), - ], - ), - ), - ) - .toList(growable: false), - ), - ), - ) - : PlatformCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: widget.slicedMoves - .mapIndexed( - (index, moves) => Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - StackedMoveCount(count: index + 1), - Expanded( - child: Row( - children: [ - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == - move.key + 1; - return Expanded( - child: StackedMoveItem( - key: isCurrentMove - ? currentMoveKey - : null, - move: move, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ) - .toList(growable: false), + if (boardOverlay != null) { + return SizedBox.square( + dimension: size, + child: Stack( + children: [ + board, + SizedBox.square( + dimension: size, + child: Center( + child: SizedBox( + width: (size / 8) * 6.6, + height: (size / 8) * 4.6, + child: boardOverlay, ), ), ), - ); - } -} - -class InlineMoveCount extends StatelessWidget { - const InlineMoveCount({required this.count}); - - final int count; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(right: 3), - child: Text( - '$count.', - style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), + ], ), - ), - ); - } -} - -class InlineMoveItem extends StatelessWidget { - const InlineMoveItem({ - required this.move, - required this.pieceNotation, - this.current, - this.onSelectMove, - super.key, - }); - - final MapEntry move; - final PieceNotation pieceNotation; - final bool? current; - final void Function(int moveIndex)? onSelectMove; + ); + } else if (error != null) { + return SizedBox.square( + dimension: size, + child: Stack(children: [board, _ErrorWidget(errorMessage: error!, boardSize: size)]), + ); + } - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 4), - decoration: ShapeDecoration( - color: current == true - ? Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.secondarySystemBackground, - context, - ) - : null - // TODO add bg color on android - : null, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - ), - child: Text( - move.value, - style: TextStyle( - fontFamily: - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, - fontWeight: FontWeight.w600, - color: - current != true ? textShade(context, _moveListOpacity) : null, - ), - ), - ), - ); + return board; } } -class StackedMoveCount extends StatelessWidget { - const StackedMoveCount({required this.count}); - - final int count; +class _ErrorWidget extends StatelessWidget { + const _ErrorWidget({required this.errorMessage, required this.boardSize}); + final double boardSize; + final String errorMessage; @override Widget build(BuildContext context) { - return SizedBox( - width: 40, - child: Text( - '$count.', - style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), + return SizedBox.square( + dimension: boardSize, + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + ), + child: Padding(padding: const EdgeInsets.all(10.0), child: Text(errorMessage)), + ), ), ), ); } } -class StackedMoveItem extends StatelessWidget { - const StackedMoveItem({ - required this.move, - this.current, - this.onSelectMove, - super.key, +class BoardSettingsOverrides { + const BoardSettingsOverrides({ + this.animationDuration, + this.autoQueenPromotion, + this.autoQueenPromotionOnPremove, + this.blindfoldMode, + this.drawShape, + this.pieceOrientationBehavior, + this.pieceAssets, }); - final MapEntry move; - final bool? current; - final void Function(int moveIndex)? onSelectMove; + final Duration? animationDuration; + final bool? autoQueenPromotion; + final bool? autoQueenPromotionOnPremove; + final bool? blindfoldMode; + final DrawShapeOptions? drawShape; + final PieceOrientationBehavior? pieceOrientationBehavior; + final PieceAssets? pieceAssets; - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - move.value, - style: TextStyle( - fontWeight: current == true ? FontWeight.bold : null, - color: current != true ? textShade(context, 0.8) : null, - ), - ), - ), + ChessboardSettings merge(ChessboardSettings settings) { + return settings.copyWith( + animationDuration: animationDuration, + autoQueenPromotion: autoQueenPromotion, + autoQueenPromotionOnPremove: autoQueenPromotionOnPremove, + blindfoldMode: blindfoldMode, + drawShape: drawShape, + pieceOrientationBehavior: pieceOrientationBehavior, + pieceAssets: pieceAssets, ); } } diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 2b26132d88..c13fa9e49f 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -1,5 +1,5 @@ import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -15,16 +15,15 @@ class BoardThumbnail extends ConsumerStatefulWidget { this.footer, this.lastMove, this.onTap, + this.animationDuration, }); - const BoardThumbnail.loading({ - required this.size, - this.header, - this.footer, - }) : orientation = Side.white, - fen = dartchess.kInitialFEN, - lastMove = null, - onTap = null; + const BoardThumbnail.loading({required this.size, this.header, this.footer}) + : orientation = Side.white, + fen = kInitialFEN, + lastMove = null, + onTap = null, + animationDuration = null; /// Size of the board. final double size; @@ -46,6 +45,9 @@ class BoardThumbnail extends ConsumerStatefulWidget { final GestureTapCallback? onTap; + /// Optionally animate changes to the board by the specified duration. + final Duration? animationDuration; + @override _BoardThumbnailState createState() => _BoardThumbnailState(); } @@ -67,47 +69,62 @@ class _BoardThumbnailState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final board = Board( - size: widget.size, - data: BoardData( - interactableSide: InteractableSide.none, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove, - ), - settings: BoardSettings( - enableCoordinates: false, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - animationDuration: const Duration(milliseconds: 150), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - ), - ); - - final maybeTappableBoard = widget.onTap != null - ? GestureDetector( - onTap: widget.onTap, - onTapDown: (_) => _onTapDown(), - onTapCancel: _onTapCancel, - onTapUp: (_) => _onTapCancel(), - child: AnimatedScale( - scale: scale, - duration: const Duration(milliseconds: 100), - child: board, - ), - ) - : board; + final board = + widget.animationDuration != null + ? Chessboard.fixed( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + settings: ChessboardSettings( + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + animationDuration: widget.animationDuration!, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + hue: boardPrefs.hue, + brightness: boardPrefs.brightness, + ), + ) + : StaticChessboard( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + hue: boardPrefs.hue, + brightness: boardPrefs.brightness, + ); + + final maybeTappableBoard = + widget.onTap != null + ? GestureDetector( + onTap: widget.onTap, + onTapDown: (_) => _onTapDown(), + onTapCancel: _onTapCancel, + onTapUp: (_) => _onTapCancel(), + child: AnimatedScale( + scale: scale, + duration: const Duration(milliseconds: 100), + child: board, + ), + ) + : board; return widget.header != null || widget.footer != null ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.header != null) widget.header!, - maybeTappableBoard, - if (widget.footer != null) widget.footer!, - ], - ) + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.header != null) widget.header!, + maybeTappableBoard, + if (widget.footer != null) widget.footer!, + ], + ) : maybeTappableBoard; } } diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart new file mode 100644 index 0000000000..d08bb3428e --- /dev/null +++ b/lib/src/widgets/bottom_bar.dart @@ -0,0 +1,61 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/constants.dart'; + +/// A container in the style of a bottom app bar, containg a [Row] of children widgets. +/// +/// The height of the bar is always [kBottomBarHeight]. +class BottomBar extends StatelessWidget { + const BottomBar({ + required this.children, + this.mainAxisAlignment = MainAxisAlignment.spaceAround, + this.expandChildren = true, + }); + + const BottomBar.empty() + : children = const [], + expandChildren = true, + mainAxisAlignment = MainAxisAlignment.spaceAround; + + /// Children to display in the bottom bar's [Row]. Typically instances of [BottomBarButton]. + final List children; + + /// Alignment of the bottom bar's internal row. Defaults to [MainAxisAlignment.spaceAround]. + final MainAxisAlignment mainAxisAlignment; + + /// Whether to expand the children to fill the available space. Defaults to true. + final bool expandChildren; + + @override + Widget build(BuildContext context) { + if (Theme.of(context).platform == TargetPlatform.iOS) { + return ColoredBox( + color: CupertinoTheme.of(context).barBackgroundColor, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, + children: + expandChildren + ? children.map((child) => Expanded(child: child)).toList() + : children, + ), + ), + ), + ); + } + + return BottomAppBar( + height: kBottomBarHeight, + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + child: Row( + mainAxisAlignment: mainAxisAlignment, + children: + expandChildren ? children.map((child) => Expanded(child: child)).toList() : children, + ), + ); + } +} diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index 523730aefd..ae9247711e 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -7,7 +7,7 @@ class BottomBarButton extends StatelessWidget { required this.icon, required this.label, required this.onTap, - this.chip, + this.badgeLabel, this.highlighted = false, this.showLabel = false, this.showTooltip = true, @@ -18,7 +18,7 @@ class BottomBarButton extends StatelessWidget { final IconData icon; final String label; - final Widget? chip; + final String? badgeLabel; final VoidCallback? onTap; final bool highlighted; @@ -36,11 +36,10 @@ class BottomBarButton extends StatelessWidget { Widget build(BuildContext context) { final primary = Theme.of(context).colorScheme.primary; - final chipColor = Theme.of(context).colorScheme.secondary; - - final labelFontSize = Theme.of(context).platform == TargetPlatform.iOS - ? 11.0 - : Theme.of(context).textTheme.bodySmall?.fontSize; + final labelFontSize = + Theme.of(context).platform == TargetPlatform.iOS + ? 11.0 + : Theme.of(context).textTheme.bodySmall?.fontSize; return Semantics( container: true, @@ -51,66 +50,45 @@ class BottomBarButton extends StatelessWidget { child: Tooltip( excludeFromSemantics: true, message: label, - triggerMode: showTooltip - ? TooltipTriggerMode.longPress - : TooltipTriggerMode.manual, + triggerMode: showTooltip ? TooltipTriggerMode.longPress : TooltipTriggerMode.manual, child: AdaptiveInkWell( borderRadius: BorderRadius.zero, onTap: onTap, child: Opacity( opacity: enabled ? 1.0 : 0.4, - child: Stack( - alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (blink) - _BlinkIcon( - icon: icon, - color: highlighted - ? primary - : Theme.of(context).iconTheme.color ?? Colors.black, - ) - else - Icon(icon, color: highlighted ? primary : null), - if (showLabel) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - label, - style: TextStyle( - fontSize: labelFontSize, - color: highlighted ? primary : null, - ), - ), + if (blink) + _BlinkIcon( + badgeLabel: badgeLabel, + icon: icon, + color: + highlighted ? primary : Theme.of(context).iconTheme.color ?? Colors.black, + ) + else + Badge( + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), + isLabelVisible: badgeLabel != null, + label: (badgeLabel != null) ? Text(badgeLabel!) : null, + child: Icon(icon, color: highlighted ? primary : null), + ), + if (showLabel) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: labelFontSize, + color: highlighted ? primary : null, ), - ], - ), - if (chip != null) - Positioned( - top: 2.0, - right: 2.0, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.brightness_1, - size: 20.0, - color: chipColor, - ), - FittedBox( - fit: BoxFit.contain, - child: DefaultTextStyle.merge( - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontWeight: FontWeight.bold, - ), - child: chip!, - ), - ), - ], + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], @@ -123,11 +101,9 @@ class BottomBarButton extends StatelessWidget { } class _BlinkIcon extends StatefulWidget { - const _BlinkIcon({ - required this.icon, - required this.color, - }); + const _BlinkIcon({this.badgeLabel, required this.icon, required this.color}); + final String? badgeLabel; final IconData icon; final Color color; @@ -135,8 +111,7 @@ class _BlinkIcon extends StatefulWidget { _BlinkIconState createState() => _BlinkIconState(); } -class _BlinkIconState extends State<_BlinkIcon> - with SingleTickerProviderStateMixin { +class _BlinkIconState extends State<_BlinkIcon> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _colorAnimation; @@ -144,20 +119,16 @@ class _BlinkIconState extends State<_BlinkIcon> void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 500), - vsync: this, - ); + _controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this); - _colorAnimation = - ColorTween(begin: widget.color, end: null).animate(_controller) - ..addStatusListener((status) { - if (_controller.status == AnimationStatus.completed) { - _controller.reverse(); - } else if (_controller.status == AnimationStatus.dismissed) { - _controller.forward(); - } - }); + _colorAnimation = ColorTween(begin: widget.color, end: null).animate(_controller) + ..addStatusListener((status) { + if (_controller.status == AnimationStatus.completed) { + _controller.reverse(); + } else if (_controller.status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); _controller.forward(); } @@ -173,9 +144,15 @@ class _BlinkIconState extends State<_BlinkIcon> return AnimatedBuilder( animation: _colorAnimation, builder: (context, child) { - return Icon( - widget.icon, - color: _colorAnimation.value ?? Colors.transparent, + return Badge( + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), + isLabelVisible: widget.badgeLabel != null, + label: widget.badgeLabel != null ? Text(widget.badgeLabel!) : null, + child: Icon(widget.icon, color: _colorAnimation.value ?? Colors.transparent), ); }, ); diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index e52f71eee5..8d0e7eff1a 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -4,10 +4,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:popover/popover.dart'; /// Platform agnostic button which is used for important actions. /// -/// Will use an [FilledButton] on Android and a [CupertinoButton.filled] on iOS. +/// Will use an [FilledButton] on Android and a [CupertinoButton.tinted] on iOS. class FatButton extends StatelessWidget { const FatButton({ required this.semanticsLabel, @@ -28,12 +29,10 @@ class FatButton extends StatelessWidget { button: true, label: semanticsLabel, excludeSemantics: true, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.filled(onPressed: onPressed, child: child) - : FilledButton( - onPressed: onPressed, - child: child, - ), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton.tinted(onPressed: onPressed, child: child) + : FilledButton(onPressed: onPressed, child: child), ); } } @@ -59,25 +58,20 @@ class SecondaryButton extends StatefulWidget { State createState() => _SecondaryButtonState(); } -class _SecondaryButtonState extends State - with SingleTickerProviderStateMixin { +class _SecondaryButtonState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; @override void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 1), - vsync: this, - ); + _controller = AnimationController(duration: const Duration(seconds: 1), vsync: this); _animation = (defaultTargetPlatform == TargetPlatform.iOS - ? Tween(begin: 0.5, end: 1.0) - : Tween(begin: 0.0, end: 0.3)) - .animate(_controller) - ..addListener(() { - setState(() {}); - }); + ? Tween(begin: 0.5, end: 1.0) + : Tween(begin: 0.0, end: 0.3)) + .animate(_controller)..addListener(() { + setState(() {}); + }); if (widget.glowing) { _controller.repeat(reverse: true); @@ -110,40 +104,38 @@ class _SecondaryButtonState extends State button: true, label: widget.semanticsLabel, excludeSemantics: true, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - color: widget.glowing - ? CupertinoTheme.of(context) - .primaryColor - .withOpacity(_animation.value) - : null, - onPressed: widget.onPressed, - child: widget.child, - ) - : OutlinedButton( - onPressed: widget.onPressed, - style: OutlinedButton.styleFrom( - textStyle: widget.textStyle, - backgroundColor: widget.glowing - ? Theme.of(context) - .colorScheme - .primary - .withOpacity(_animation.value) - : null, + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton( + color: + widget.glowing + ? CupertinoTheme.of( + context, + ).primaryColor.withValues(alpha: _animation.value) + : null, + onPressed: widget.onPressed, + child: widget.child, + ) + : OutlinedButton( + onPressed: widget.onPressed, + style: OutlinedButton.styleFrom( + textStyle: widget.textStyle, + backgroundColor: + widget.glowing + ? Theme.of( + context, + ).colorScheme.primary.withValues(alpha: _animation.value) + : null, + ), + child: widget.child, ), - child: widget.child, - ), ); } } /// Platform agnostic text button to appear in the app bar. class AppBarTextButton extends StatelessWidget { - const AppBarTextButton({ - required this.child, - required this.onPressed, - super.key, - }); + const AppBarTextButton({required this.child, required this.onPressed, super.key}); final VoidCallback? onPressed; final Widget child; @@ -151,15 +143,8 @@ class AppBarTextButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - padding: EdgeInsets.zero, - onPressed: onPressed, - child: child, - ) - : TextButton( - onPressed: onPressed, - child: child, - ); + ? CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child) + : TextButton(onPressed: onPressed, child: child); } } @@ -179,55 +164,70 @@ class AppBarIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? IconTheme( - data: const IconThemeData(size: 26.0), - child: CupertinoIconButton( - padding: EdgeInsets.zero, - semanticsLabel: semanticsLabel, - onPressed: onPressed, - icon: icon, - ), - ) - : IconButton( - tooltip: semanticsLabel, - icon: icon, - onPressed: onPressed, - ); + ? CupertinoIconButton( + padding: EdgeInsets.zero, + semanticsLabel: semanticsLabel, + onPressed: onPressed, + icon: icon, + ) + : IconButton(tooltip: semanticsLabel, icon: icon, onPressed: onPressed); } } -class AdaptiveTextButton extends StatelessWidget { - const AdaptiveTextButton({ - required this.child, +/// Platform agnostic icon button to appear in the app bar, that has a notification bubble. +class AppBarNotificationIconButton extends StatelessWidget { + const AppBarNotificationIconButton({ + required this.icon, required this.onPressed, + required this.semanticsLabel, + required this.count, + this.color, super.key, }); + final Widget icon; + final VoidCallback? onPressed; + final String semanticsLabel; + final Color? color; + final int count; + + @override + Widget build(BuildContext context) { + return AppBarIconButton( + icon: Badge.count( + backgroundColor: Theme.of(context).colorScheme.tertiary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onTertiary, + fontWeight: FontWeight.bold, + ), + count: count, + isLabelVisible: count > 0, + child: icon, + ), + onPressed: onPressed, + semanticsLabel: semanticsLabel, + ); + } +} + +class AdaptiveTextButton extends StatelessWidget { + const AdaptiveTextButton({required this.child, required this.onPressed, super.key}); + final VoidCallback? onPressed; final Widget child; @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - onPressed: onPressed, - child: child, - ) - : TextButton( - onPressed: onPressed, - child: child, - ); + ? CupertinoButton(onPressed: onPressed, child: child) + : TextButton(onPressed: onPressed, child: child); } } /// Button that explicitly reduce padding, thus does not conform to accessibility /// guidelines. So use sparingly. class NoPaddingTextButton extends StatelessWidget { - const NoPaddingTextButton({ - required this.child, - required this.onPressed, - super.key, - }); + const NoPaddingTextButton({required this.child, required this.onPressed, super.key}); final VoidCallback? onPressed; final Widget child; @@ -235,21 +235,16 @@ class NoPaddingTextButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - padding: EdgeInsets.zero, - onPressed: onPressed, - minSize: 23, - child: child, - ) + ? CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, minSize: 23, child: child) : TextButton( - onPressed: onPressed, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 5.0), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: child, - ); + onPressed: onPressed, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 5.0), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: child, + ); } } @@ -282,7 +277,7 @@ class CupertinoIconButton extends StatelessWidget { child: CupertinoButton( padding: padding, onPressed: onPressed, - child: icon, + child: IconTheme.merge(data: const IconThemeData(size: 24.0), child: icon), ), ); } @@ -291,7 +286,7 @@ class CupertinoIconButton extends StatelessWidget { /// InkWell that adapts to the iOS platform. /// /// Used to create a button that shows a ripple on Android and a highlight on iOS. -class AdaptiveInkWell extends StatefulWidget { +class AdaptiveInkWell extends StatelessWidget { const AdaptiveInkWell({ required this.child, this.onTap, @@ -313,66 +308,23 @@ class AdaptiveInkWell extends StatefulWidget { final BorderRadius? borderRadius; final Color? splashColor; - @override - State createState() => _AdaptiveInkWellState(); -} - -class _AdaptiveInkWellState extends State { - bool _isPressed = false; - @override Widget build(BuildContext context) { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return InkWell( - onTap: widget.onTap, - onTapDown: widget.onTapDown, - onTapUp: widget.onTapUp, - onTapCancel: widget.onTapCancel, - onLongPress: widget.onLongPress, - borderRadius: widget.borderRadius, - splashColor: widget.splashColor, - child: widget.child, - ); - case TargetPlatform.iOS: - return GestureDetector( - onLongPress: widget.onLongPress, - onTap: widget.onTap, - onTapDown: (details) { - widget.onTapDown?.call(details); - if (widget.onTap == null) return; - setState(() => _isPressed = true); - }, - onTapCancel: () { - widget.onTapCancel?.call(); - setState(() => _isPressed = false); - }, - onTapUp: (details) { - widget.onTapUp?.call(details); - Future.delayed(const Duration(milliseconds: 100)).then((_) { - if (mounted) { - setState(() => _isPressed = false); - } - }); - }, - child: Semantics( - button: true, - child: Container( - decoration: BoxDecoration( - borderRadius: widget.borderRadius, - color: _isPressed - ? widget.splashColor ?? - CupertinoColors.systemGrey5.resolveFrom(context) - : null, - ), - child: widget.child, - ), - ), - ); - default: - assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - return const SizedBox.shrink(); - } + final platform = Theme.of(context).platform; + return InkWell( + onTap: onTap, + onTapDown: onTapDown, + onTapUp: onTapUp, + onTapCancel: onTapCancel, + onLongPress: onLongPress, + borderRadius: borderRadius, + splashColor: + platform == TargetPlatform.iOS + ? splashColor ?? CupertinoColors.systemGrey5.resolveFrom(context) + : splashColor, + splashFactory: platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, + child: child, + ); } } @@ -473,10 +425,7 @@ class PlatformIconButton extends StatelessWidget { this.color, this.iconSize, this.padding, - }) : assert( - color == null || !highlighted, - 'Cannot provide both color and highlighted', - ); + }) : assert(color == null || !highlighted, 'Cannot provide both color and highlighted'); final IconData icon; final String semanticsLabel; @@ -505,18 +454,12 @@ class PlatformIconButton extends StatelessWidget { case TargetPlatform.iOS: final themeData = CupertinoTheme.of(context); return CupertinoTheme( - data: themeData.copyWith( - primaryColor: themeData.textTheme.textStyle.color, - ), + data: themeData.copyWith(primaryColor: themeData.textTheme.textStyle.color), child: CupertinoIconButton( onPressed: onTap, semanticsLabel: semanticsLabel, padding: padding, - icon: Icon( - icon, - color: highlighted ? themeData.primaryColor : color, - size: iconSize, - ), + icon: Icon(icon, color: highlighted ? themeData.primaryColor : color, size: iconSize), ), ); default: @@ -525,3 +468,148 @@ class PlatformIconButton extends StatelessWidget { } } } + +const _kMenuWidth = 250.0; +const Color _kBorderColor = CupertinoDynamicColor.withBrightness( + color: Color(0xFFA9A9AF), + darkColor: Color(0xFF57585A), +); + +/// A platform agnostic menu button for the app bar. +class PlatformAppBarMenuButton extends StatelessWidget { + const PlatformAppBarMenuButton({ + required this.icon, + required this.semanticsLabel, + required this.actions, + super.key, + }); + + final Widget icon; + final String semanticsLabel; + final List actions; + + @override + Widget build(BuildContext context) { + if (Theme.of(context).platform == TargetPlatform.iOS) { + final menuActions = + actions.map((action) { + return CupertinoContextMenuAction( + onPressed: () { + if (action.dismissOnPress) { + Navigator.of(context).pop(); + } + action.onPressed(); + }, + trailingIcon: action.icon, + child: Text(action.label), + ); + }).toList(); + return AppBarIconButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: _kMenuWidth, + child: IntrinsicHeight( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(13.0)), + child: ColoredBox( + color: CupertinoDynamicColor.resolve( + CupertinoContextMenu.kBackgroundColor, + context, + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: CupertinoScrollbar( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + menuActions.first, + for (final Widget action in menuActions.skip(1)) + DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: CupertinoDynamicColor.resolve( + _kBorderColor, + context, + ), + width: 0.4, + ), + ), + ), + position: DecorationPosition.foreground, + child: action, + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + arrowWidth: 0.0, + arrowHeight: 0.0, + direction: PopoverDirection.top, + width: _kMenuWidth, + backgroundColor: Colors.transparent, + ); + }, + semanticsLabel: semanticsLabel, + icon: icon, + ); + } + + return MenuAnchor( + menuChildren: + actions.map((action) { + return MenuItemButton( + leadingIcon: Icon(action.icon), + semanticsLabel: action.label, + closeOnActivate: action.dismissOnPress, + onPressed: action.onPressed, + child: Text(action.label), + ); + }).toList(), + builder: (BuildContext context, MenuController controller, Widget? child) { + return AppBarIconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + semanticsLabel: semanticsLabel, + icon: icon, + ); + }, + ); + } +} + +class AppBarMenuAction { + const AppBarMenuAction({ + required this.icon, + required this.label, + required this.onPressed, + this.dismissOnPress = true, + }); + + final IconData icon; + final String label; + final VoidCallback onPressed; + + /// Whether the modal should be dismissed when an action is pressed. + /// + /// Default to true. + final bool dismissOnPress; +} diff --git a/lib/src/widgets/clock.dart b/lib/src/widgets/clock.dart new file mode 100644 index 0000000000..091bb04d49 --- /dev/null +++ b/lib/src/widgets/clock.dart @@ -0,0 +1,302 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; + +const _kClockFontSize = 26.0; +const _kClockTenthFontSize = 20.0; +const _kClockHundredsFontSize = 18.0; + +const _showTenthsThreshold = Duration(seconds: 10); + +/// A stateless widget that displays the time left on the clock. +/// +/// For a clock widget that automatically counts down, see [CountdownClockBuilder]. +class Clock extends StatelessWidget { + const Clock({ + required this.timeLeft, + this.active = false, + this.clockStyle, + this.emergencyThreshold, + this.padLeft = false, + super.key, + }); + + /// The time left to be displayed on the clock. + final Duration timeLeft; + + /// If `true`, [ClockStyle.activeBackgroundColor] will be used, otherwise [ClockStyle.backgroundColor]. + final bool active; + + /// If [timeLeft] is less than [emergencyThreshold], the clock will set + /// its background color to [ClockStyle.emergencyBackgroundColor]. + final Duration? emergencyThreshold; + + /// Clock style to use. + final ClockStyle? clockStyle; + + /// Whether to pad with a leading zero (default is `false`). + final bool padLeft; + + @override + Widget build(BuildContext context) { + final hours = timeLeft.inHours; + final mins = timeLeft.inMinutes.remainder(60); + final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); + final showTenths = timeLeft < _showTenthsThreshold; + final isEmergency = emergencyThreshold != null && timeLeft <= emergencyThreshold!; + final remainingHeight = estimateRemainingHeightLeftBoard(context); + + final hoursDisplay = padLeft ? hours.toString().padLeft(2, '0') : hours.toString(); + final minsDisplay = padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); + + final brightness = Theme.of(context).brightness; + final activeClockStyle = + clockStyle ?? + (brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle); + + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + // TODO improve this + final fontScaleFactor = maxWidth < 90 ? 0.8 : 1.0; + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + color: + active + ? isEmergency + ? activeClockStyle.emergencyBackgroundColor + : activeClockStyle.activeBackgroundColor + : activeClockStyle.backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: kMaxClockTextScaleFactor, + child: RichText( + text: TextSpan( + text: + hours > 0 + ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' + : '$minsDisplay:$secs', + style: TextStyle( + color: + active + ? isEmergency + ? activeClockStyle.emergencyTextColor + : activeClockStyle.activeTextColor + : activeClockStyle.textColor, + fontSize: _kClockFontSize * fontScaleFactor, + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + children: [ + if (showTenths) + TextSpan( + text: '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', + style: TextStyle(fontSize: _kClockTenthFontSize * fontScaleFactor), + ), + if (!active && timeLeft < const Duration(seconds: 1)) + TextSpan( + text: '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', + style: TextStyle(fontSize: _kClockHundredsFontSize * fontScaleFactor), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +@immutable +class ClockStyle { + const ClockStyle({ + required this.textColor, + required this.activeTextColor, + required this.emergencyTextColor, + required this.backgroundColor, + required this.activeBackgroundColor, + required this.emergencyBackgroundColor, + }); + + final Color textColor; + final Color activeTextColor; + final Color emergencyTextColor; + final Color backgroundColor; + final Color activeBackgroundColor; + final Color emergencyBackgroundColor; + + static const darkThemeStyle = ClockStyle( + textColor: Colors.grey, + activeTextColor: Colors.black, + emergencyTextColor: Colors.white, + backgroundColor: Colors.black, + activeBackgroundColor: Color(0xFFDDDDDD), + emergencyBackgroundColor: Color(0xFF673431), + ); + + static const lightThemeStyle = ClockStyle( + textColor: Colors.grey, + activeTextColor: Colors.black, + emergencyTextColor: Colors.black, + backgroundColor: Colors.white, + activeBackgroundColor: Color(0xFFD0E0BD), + emergencyBackgroundColor: Color(0xFFF2CCCC), + ); +} + +typedef ClockWidgetBuilder = Widget Function(BuildContext, Duration); + +/// A widget that automatically starts a countdown from [timeLeft] when [active] is `true`. +/// +/// The clock will update the UI every [tickInterval], which defaults to 100ms, +/// and the [builder] will be called with the new [timeLeft] value. +/// +/// The clock can be synchronized with the time at which the clock event was received from the server +/// by setting the [clockUpdatedAt] parameter. +/// This widget will only update its internal clock when the [clockUpdatedAt] parameter changes. +/// +/// The [delay] parameter can be used to delay the start of the clock. +/// +/// The clock will stop counting down when [active] is set to `false`. +/// +/// The clock will stop counting down when the time left reaches zero. +class CountdownClockBuilder extends StatefulWidget { + const CountdownClockBuilder({ + required this.timeLeft, + required this.active, + required this.builder, + this.delay, + this.tickInterval = const Duration(milliseconds: 100), + this.clockUpdatedAt, + super.key, + }); + + /// The duration left on the clock. + final Duration timeLeft; + + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + + /// The interval at which the clock updates the UI. + final Duration tickInterval; + + /// The time at which the clock was updated. + /// + /// Use this parameter to synchronize the clock with the time at which the clock + /// event was received from the server and to compensate for UI lag. + final DateTime? clockUpdatedAt; + + /// If `true`, the clock starts counting down. + final bool active; + + /// A [ClockWidgetBuilder] that builds the clock on each tick with the new [timeLeft] value. + final ClockWidgetBuilder builder; + + @override + State createState() => _CountdownClockState(); +} + +class _CountdownClockState extends State { + Timer? _timer; + Timer? _delayTimer; + Duration timeLeft = Duration.zero; + + final _stopwatch = clock.stopwatch(); + + void startClock() { + final now = clock.now(); + final delay = widget.delay ?? Duration.zero; + final clockUpdatedAt = widget.clockUpdatedAt ?? now; + // UI lag diff: the elapsed time between the time the clock should have started + // and the time the clock is actually started + final uiLag = now.difference(clockUpdatedAt); + final realDelay = delay - uiLag; + + // real delay is negative, we need to adjust the timeLeft. + if (realDelay < Duration.zero) { + final newTimeLeft = timeLeft + realDelay; + timeLeft = newTimeLeft > Duration.zero ? newTimeLeft : Duration.zero; + } + + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _scheduleTick); + } else { + _scheduleTick(); + } + } + + void _scheduleTick() { + _timer?.cancel(); + _timer = Timer(widget.tickInterval, _tick); + _stopwatch.reset(); + _stopwatch.start(); + } + + void _tick() { + final newTimeLeft = timeLeft - _stopwatch.elapsed; + setState(() { + timeLeft = newTimeLeft; + if (timeLeft <= Duration.zero) { + timeLeft = Duration.zero; + } + }); + if (timeLeft > Duration.zero) { + _scheduleTick(); + } + } + + void stopClock() { + _delayTimer?.cancel(); + _timer?.cancel(); + _stopwatch.stop(); + } + + @override + void initState() { + super.initState(); + timeLeft = widget.timeLeft; + if (widget.active) { + startClock(); + } + } + + @override + void didUpdateWidget(CountdownClockBuilder oldClock) { + super.didUpdateWidget(oldClock); + + if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { + timeLeft = widget.timeLeft; + } + + if (widget.active != oldClock.active) { + if (widget.active) { + startClock(); + } else { + stopClock(); + } + } + } + + @override + void dispose() { + super.dispose(); + _delayTimer?.cancel(); + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary(child: widget.builder(context, timeLeft)); + } +} diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart deleted file mode 100644 index acb2a81948..0000000000 --- a/lib/src/widgets/countdown_clock.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; - -/// A simple countdown clock. -/// -/// The clock starts only when [active] is `true`. -class CountdownClock extends ConsumerStatefulWidget { - /// The duration left on the clock. - final Duration duration; - - /// If [timeLeft] is less than [emergencyThreshold], the clock will change - /// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor - /// If [emergencySoundEnabled] is `true`, the clock will also play a sound. - final Duration? emergencyThreshold; - - final bool emergencySoundEnabled; - - /// If [active] is `true`, the clock starts counting down. - final bool active; - - /// Callback when the clock reaches zero. - final VoidCallback? onFlag; - - /// Callback with the remaining duration when the clock stops - final ValueSetter? onStop; - - /// Custom light color style - final ClockStyle? lightColorStyle; - - /// Custom dark color style - final ClockStyle? darkColorStyle; - - const CountdownClock({ - required this.duration, - required this.active, - this.emergencyThreshold, - this.emergencySoundEnabled = true, - this.onFlag, - this.onStop, - this.lightColorStyle, - this.darkColorStyle, - super.key, - }); - - @override - ConsumerState createState() => _CountdownClockState(); -} - -const _period = Duration(milliseconds: 100); -const _emergencyDelay = Duration(seconds: 20); - -class _CountdownClockState extends ConsumerState { - Timer? _timer; - Duration timeLeft = Duration.zero; - bool _shouldPlayEmergencyFeedback = true; - DateTime? _nextEmergency; - - final _stopwatch = Stopwatch(); - - void startClock() { - _timer?.cancel(); - _stopwatch.reset(); - _stopwatch.start(); - _timer = Timer.periodic(_period, (timer) { - setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; - _stopwatch.reset(); - _playEmergencyFeedback(); - if (timeLeft <= Duration.zero) { - widget.onFlag?.call(); - timeLeft = Duration.zero; - stopClock(); - } - }); - }); - } - - void stopClock() { - _timer?.cancel(); - _stopwatch.stop(); - scheduleMicrotask(() { - widget.onStop?.call(timeLeft); - }); - } - - void _playEmergencyFeedback() { - if (widget.emergencyThreshold != null && - timeLeft <= widget.emergencyThreshold! && - _shouldPlayEmergencyFeedback && - (_nextEmergency == null || _nextEmergency!.isBefore(DateTime.now()))) { - _shouldPlayEmergencyFeedback = false; - _nextEmergency = DateTime.now().add(_emergencyDelay); - if (widget.emergencySoundEnabled) { - ref.read(soundServiceProvider).play(Sound.lowTime); - } - HapticFeedback.heavyImpact(); - } else if (widget.emergencyThreshold != null && - timeLeft > widget.emergencyThreshold! * 1.5) { - _shouldPlayEmergencyFeedback = true; - } - } - - ClockStyle getStyle(Brightness brightness) { - if (brightness == Brightness.dark) { - return widget.darkColorStyle ?? ClockStyle.darkThemeStyle; - } - - return widget.lightColorStyle ?? ClockStyle.lightThemeStyle; - } - - @override - void initState() { - super.initState(); - timeLeft = widget.duration; - if (widget.active) { - startClock(); - } - } - - @override - void didUpdateWidget(CountdownClock oldClock) { - super.didUpdateWidget(oldClock); - if (widget.duration != oldClock.duration) { - timeLeft = widget.duration; - } - - if (widget.active != oldClock.active) { - widget.active ? startClock() : stopClock(); - } - } - - @override - void dispose() { - super.dispose(); - _timer?.cancel(); - } - - @override - Widget build(BuildContext context) { - final hours = timeLeft.inHours; - final mins = timeLeft.inMinutes.remainder(60); - final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); - final showTenths = timeLeft < const Duration(seconds: 10); - final isEmergency = widget.emergencyThreshold != null && - timeLeft <= widget.emergencyThreshold!; - final brightness = ref.watch(currentBrightnessProvider); - final clockStyle = getStyle(brightness); - final remainingHeight = estimateRemainingHeightLeftBoard(context); - - return RepaintBoundary( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: widget.active - ? isEmergency - ? clockStyle.emergencyBackgroundColor - : clockStyle.activeBackgroundColor - : clockStyle.backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), - child: MediaQuery.withClampedTextScaling( - maxScaleFactor: kMaxClockTextScaleFactor, - child: RichText( - text: TextSpan( - text: hours > 0 - ? '$hours:${mins.toString().padLeft(2, '0')}:$secs' - : '$mins:$secs', - style: TextStyle( - color: widget.active - ? isEmergency - ? clockStyle.emergencyTextColor - : clockStyle.activeTextColor - : clockStyle.textColor, - fontSize: 26, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 1.0 - : null, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - children: [ - if (showTenths) - TextSpan( - text: - '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', - style: const TextStyle(fontSize: 20), - ), - if (!widget.active && timeLeft < const Duration(seconds: 1)) - TextSpan( - text: - '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', - style: const TextStyle(fontSize: 18), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -@immutable -class ClockStyle { - const ClockStyle({ - required this.textColor, - required this.activeTextColor, - required this.emergencyTextColor, - required this.backgroundColor, - required this.activeBackgroundColor, - required this.emergencyBackgroundColor, - }); - - static const darkThemeStyle = ClockStyle( - textColor: Colors.grey, - activeTextColor: Colors.black, - emergencyTextColor: Colors.white, - backgroundColor: Colors.black, - activeBackgroundColor: Color(0xFFDDDDDD), - emergencyBackgroundColor: Color(0xFF673431), - ); - - static const lightThemeStyle = ClockStyle( - textColor: Colors.grey, - activeTextColor: Colors.black, - emergencyTextColor: Colors.black, - backgroundColor: Colors.white, - activeBackgroundColor: Color(0xFFD0E0BD), - emergencyBackgroundColor: Color(0xFFF2CCCC), - ); - - final Color textColor; - final Color activeTextColor; - final Color emergencyTextColor; - final Color backgroundColor; - final Color activeBackgroundColor; - final Color emergencyBackgroundColor; -} diff --git a/lib/src/widgets/expanded_section.dart b/lib/src/widgets/expanded_section.dart index 6f8709033f..169d589c12 100644 --- a/lib/src/widgets/expanded_section.dart +++ b/lib/src/widgets/expanded_section.dart @@ -10,8 +10,7 @@ class ExpandedSection extends StatefulWidget { _ExpandedSectionState createState() => _ExpandedSectionState(); } -class _ExpandedSectionState extends State - with SingleTickerProviderStateMixin { +class _ExpandedSectionState extends State with SingleTickerProviderStateMixin { late AnimationController expandController; late Animation animation; @@ -23,10 +22,7 @@ class _ExpandedSectionState extends State value: widget.expand ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), ); - animation = CurvedAnimation( - parent: expandController, - curve: Curves.fastOutSlowIn, - ); + animation = CurvedAnimation(parent: expandController, curve: Curves.fastOutSlowIn); } void _runExpandCheck() { @@ -51,10 +47,6 @@ class _ExpandedSectionState extends State @override Widget build(BuildContext context) { - return SizeTransition( - axisAlignment: 1.0, - sizeFactor: animation, - child: widget.child, - ); + return SizeTransition(axisAlignment: 1.0, sizeFactor: animation, child: widget.child); } } diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index 493d4c3c90..7cb4583903 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -1,10 +1,69 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:signal_strength_indicator/signal_strength_indicator.dart'; + +class LagIndicator extends StatelessWidget { + const LagIndicator({ + required this.lagRating, + this.size = 20.0, + this.showLoadingIndicator = false, + super.key, + }) : assert(lagRating >= 0 && lagRating <= 4); + + /// The lag rating from 0 to 4. + final int lagRating; + + /// Visual size of the indicator. + final double size; + + /// Whether to show a loading indicator when the lag rating is 0. + final bool showLoadingIndicator; + + static const spinKit = SpinKitThreeBounce(color: Colors.grey, size: 15); + + static const cupertinoLevels = { + 0: CupertinoColors.systemRed, + 1: CupertinoColors.systemYellow, + 2: CupertinoColors.systemGreen, + 3: CupertinoColors.systemGreen, + }; + + static const materialLevels = {0: Colors.red, 1: Colors.yellow, 2: Colors.green, 3: Colors.green}; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: size, + child: Stack( + children: [ + SignalStrengthIndicator.bars( + barCount: 4, + minValue: 1, + maxValue: 4, + value: lagRating, + size: size, + inactiveColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey, + context, + ).withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + levels: + Theme.of(context).platform == TargetPlatform.iOS ? cupertinoLevels : materialLevels, + ), + if (showLoadingIndicator && lagRating == 0) spinKit, + ], + ), + ); + } +} class ConnectivityBanner extends ConsumerWidget { const ConnectivityBanner(); @@ -21,9 +80,10 @@ class ConnectivityBanner extends ConsumerWidget { } return Container( height: 45, - color: theme.platform == TargetPlatform.iOS - ? cupertinoTheme.scaffoldBackgroundColor - : theme.colorScheme.surfaceContainer, + color: + theme.platform == TargetPlatform.iOS + ? cupertinoTheme.scaffoldBackgroundColor + : theme.colorScheme.surfaceContainer, child: Padding( padding: Styles.horizontalBodyPadding, child: Row( @@ -36,9 +96,8 @@ class ConnectivityBanner extends ConsumerWidget { maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - color: theme.platform == TargetPlatform.iOS - ? null - : theme.colorScheme.onSurface, + color: + theme.platform == TargetPlatform.iOS ? null : theme.colorScheme.onSurface, ), ), ), @@ -63,9 +122,7 @@ class ButtonLoadingIndicator extends StatelessWidget { return const SizedBox( height: 20, width: 20, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), + child: CircularProgressIndicator.adaptive(strokeWidth: 2), ); } } @@ -76,9 +133,7 @@ class CenterLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); + return const Center(child: CircularProgressIndicator.adaptive()); } } @@ -87,10 +142,7 @@ class CenterLoadingIndicator extends StatelessWidget { /// This widget is intended to be used when a request fails and the user can /// retry it. class FullScreenRetryRequest extends StatelessWidget { - const FullScreenRetryRequest({ - super.key, - required this.onRetry, - }); + const FullScreenRetryRequest({super.key, required this.onRetry}); final VoidCallback onRetry; @@ -102,11 +154,7 @@ class FullScreenRetryRequest extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // TODO translate - Text( - context.l10n.mobileSomethingWentWrong, - style: Styles.sectionTitle, - ), + Text(context.l10n.mobileSomethingWentWrong, style: Styles.sectionTitle), const SizedBox(height: 10), SecondaryButton( onPressed: onRetry, @@ -119,11 +167,7 @@ class FullScreenRetryRequest extends StatelessWidget { } } -enum SnackBarType { - error, - info, - success, -} +enum SnackBarType { error, info, success } void showPlatformSnackbar( BuildContext context, @@ -136,19 +180,13 @@ void showPlatformSnackbar( SnackBar( content: Text( message, - style: type == SnackBarType.error - ? const TextStyle(color: Colors.white) - : null, + style: type == SnackBarType.error ? const TextStyle(color: Colors.white) : null, ), backgroundColor: type == SnackBarType.error ? Colors.red : null, ), ); case TargetPlatform.iOS: - showCupertinoSnackBar( - context: context, - message: message, - type: type, - ); + showCupertinoSnackBar(context: context, message: message, type: type); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); } @@ -162,30 +200,28 @@ void showCupertinoSnackBar({ Duration duration = const Duration(milliseconds: 4000), }) { final overlayEntry = OverlayEntry( - builder: (context) => Positioned( - // default iOS tab bar height + 10 - bottom: 60.0, - left: 8.0, - right: 8.0, - child: _CupertinoSnackBarManager( - snackBar: CupertinoSnackBar( - message: message, - backgroundColor: (type == SnackBarType.error - ? context.lichessColors.error - : type == SnackBarType.success + builder: + (context) => Positioned( + // default iOS tab bar height + 10 + bottom: 60.0, + left: 8.0, + right: 8.0, + child: _CupertinoSnackBarManager( + snackBar: CupertinoSnackBar( + message: message, + backgroundColor: (type == SnackBarType.error + ? context.lichessColors.error + : type == SnackBarType.success ? context.lichessColors.good : CupertinoColors.systemGrey.resolveFrom(context)) - .withOpacity(0.6), - textStyle: const TextStyle(color: Colors.white), + .withValues(alpha: 0.6), + textStyle: const TextStyle(color: Colors.white), + ), + duration: duration, + ), ), - duration: duration, - ), - ), - ); - Future.delayed( - duration + _snackBarAnimationDuration * 2, - overlayEntry.remove, ); + Future.delayed(duration + _snackBarAnimationDuration * 2, overlayEntry.remove); Overlay.of(context).insert(overlayEntry); } @@ -194,11 +230,7 @@ class CupertinoSnackBar extends StatelessWidget { final TextStyle? textStyle; final Color? backgroundColor; - const CupertinoSnackBar({ - required this.message, - this.textStyle, - this.backgroundColor, - }); + const CupertinoSnackBar({required this.message, this.textStyle, this.backgroundColor}); @override Widget build(BuildContext context) { @@ -207,15 +239,8 @@ class CupertinoSnackBar extends StatelessWidget { child: ColoredBox( color: backgroundColor ?? CupertinoColors.systemGrey, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - child: Text( - message, - style: textStyle, - textAlign: TextAlign.center, - ), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Text(message, style: textStyle, textAlign: TextAlign.center), ), ), ); @@ -228,10 +253,7 @@ class _CupertinoSnackBarManager extends StatefulWidget { final CupertinoSnackBar snackBar; final Duration duration; - const _CupertinoSnackBarManager({ - required this.snackBar, - required this.duration, - }); + const _CupertinoSnackBarManager({required this.snackBar, required this.duration}); @override State<_CupertinoSnackBarManager> createState() => _CupertinoSnackBarState(); @@ -244,14 +266,11 @@ class _CupertinoSnackBarState extends State<_CupertinoSnackBarManager> { void initState() { super.initState(); Future.microtask(() => setState(() => _show = true)); - Future.delayed( - widget.duration, - () { - if (mounted) { - setState(() => _show = false); - } - }, - ); + Future.delayed(widget.duration, () { + if (mounted) { + setState(() => _show = false); + } + }); } @override diff --git a/lib/src/widgets/filter.dart b/lib/src/widgets/filter.dart new file mode 100644 index 0000000000..4c204784a5 --- /dev/null +++ b/lib/src/widgets/filter.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +enum FilterType { + /// Only one choice is intended to be selected at a time. Uses [ChoiceChip] to display choices. + singleChoice, + + /// Multiple choices can be selected at the same time. Uses [FilterChip] to display choices. + multipleChoice, +} + +/// Displays a row of choices that can be selected or deselected. +class Filter extends StatelessWidget { + const Filter({ + this.filterName, + required this.filterType, + required this.choices, + this.showCheckmark = true, + required this.choiceSelected, + required this.choiceLabel, + required this.onSelected, + }); + + /// Will be displayed above the choices as a title. + final String? filterName; + + /// Controls how choices in a [Filter] are displayed. + final FilterType filterType; + + /// The choices that will be displayed. + final Iterable choices; + + /// Whether to show a checkmark next to selected choices. + final bool showCheckmark; + + /// Called to determine whether a choice is currently selected. + final bool Function(T choice) choiceSelected; + + /// Determines label to display for the given choice. + final Widget Function(T choice) choiceLabel; + + /// Called when a choice is selected or deselected. + final void Function(T value, bool selected) onSelected; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (filterName != null) ...[ + Text(filterName!, style: const TextStyle(fontSize: 18)), + const SizedBox(height: 10), + ], + SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8.0, + children: choices + .map( + (choice) => switch (filterType) { + FilterType.singleChoice => ChoiceChip( + label: choiceLabel(choice), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + showCheckmark: showCheckmark, + ), + FilterType.multipleChoice => FilterChip( + label: choiceLabel(choice), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + showCheckmark: showCheckmark, + ), + }, + ) + .toList(growable: false), + ), + ), + ], + ); + } +} diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 6ca7f46559..d7f4afe2d1 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -18,26 +18,23 @@ class ListSection extends StatelessWidget { this.dense = false, this.cupertinoAdditionalDividerMargin, this.cupertinoBackgroundColor, + this.cupertinoBorderRadius, this.cupertinoClipBehavior = Clip.hardEdge, }) : _isLoading = false; - ListSection.loading({ - required int itemsNumber, - bool header = false, - this.margin, - }) : children = [ - for (int i = 0; i < itemsNumber; i++) const SizedBox.shrink(), - ], - headerTrailing = null, - header = header ? const SizedBox.shrink() : null, - hasLeading = false, - showDivider = false, - showDividerBetweenTiles = false, - dense = false, - cupertinoAdditionalDividerMargin = null, - cupertinoBackgroundColor = null, - cupertinoClipBehavior = Clip.hardEdge, - _isLoading = true; + ListSection.loading({required int itemsNumber, bool header = false, this.margin}) + : children = [for (int i = 0; i < itemsNumber; i++) const SizedBox.shrink()], + headerTrailing = null, + header = header ? const SizedBox.shrink() : null, + hasLeading = false, + showDivider = false, + showDividerBetweenTiles = false, + dense = false, + cupertinoAdditionalDividerMargin = null, + cupertinoBackgroundColor = null, + cupertinoBorderRadius = null, + cupertinoClipBehavior = Clip.hardEdge, + _isLoading = true; /// Usually a list of [PlatformListTile] widgets final List children; @@ -67,6 +64,8 @@ class ListSection extends StatelessWidget { final Color? cupertinoBackgroundColor; + final BorderRadiusGeometry? cupertinoBorderRadius; + final Clip cupertinoClipBehavior; final bool _isLoading; @@ -77,135 +76,125 @@ class ListSection extends StatelessWidget { case TargetPlatform.android: return _isLoading ? Padding( - padding: margin ?? Styles.bodySectionBottomPadding, - child: Column( - children: [ - if (header != null) - // ignore: avoid-wrapping-in-padding - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Container( - width: double.infinity, - height: 25, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - ), + padding: margin ?? Styles.sectionBottomPadding, + child: Column( + children: [ + if (header != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Container( + width: double.infinity, + height: 25, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), ), ), - for (int i = 0; i < children.length; i++) - // ignore: avoid-wrapping-in-padding - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Container( - width: double.infinity, - height: 50, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), + ), + for (int i = 0; i < children.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Container( + width: double.infinity, + height: 50, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(10)), ), ), - ], - ), - ) + ), + ], + ), + ) : Padding( - padding: margin ?? Styles.sectionBottomPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (header != null) - ListTile( - dense: true, - title: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: header!, - ), - trailing: headerTrailing, - ), - if (showDividerBetweenTiles) - ...ListTile.divideTiles( - context: context, - tiles: children, - ) - else - ...children, - if (showDivider) - const Padding( - padding: EdgeInsets.only(top: 10.0), - child: Divider(thickness: 0), - ), - ], - ), - ); + padding: margin ?? Styles.sectionBottomPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (header != null) + ListTile( + dense: true, + title: DefaultTextStyle.merge(style: Styles.sectionTitle, child: header!), + trailing: headerTrailing, + ), + if (showDividerBetweenTiles) + ...ListTile.divideTiles(context: context, tiles: children) + else + ...children, + if (showDivider) + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: Divider(thickness: 0), + ), + ], + ), + ); case TargetPlatform.iOS: return _isLoading ? Padding( - padding: margin ?? Styles.bodySectionPadding, - child: Column( - children: [ - if (header != null) - // ignore: avoid-wrapping-in-padding - Padding( - padding: const EdgeInsets.only(top: 10.0, bottom: 16.0), - child: Container( - width: double.infinity, - height: 24, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - ), + padding: margin ?? Styles.bodySectionPadding, + child: Column( + children: [ + if (header != null) + // ignore: avoid-wrapping-in-padding + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 16.0), + child: Container( + width: double.infinity, + height: 24, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), ), ), - Container( - width: double.infinity, - height: children.length * 54, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), ), - ], - ), - ) + Container( + width: double.infinity, + height: children.length * 54, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + ], + ), + ) : Padding( - padding: margin ?? Styles.bodySectionPadding, - child: Column( - children: [ - if (header != null) - Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: header!, - ), - if (headerTrailing != null) headerTrailing!, - ], - ), - ), - CupertinoListSection.insetGrouped( - clipBehavior: cupertinoClipBehavior, - backgroundColor: cupertinoBackgroundColor ?? - CupertinoTheme.of(context).scaffoldBackgroundColor, - decoration: BoxDecoration( - color: cupertinoBackgroundColor ?? - Styles.cupertinoCardColor.resolveFrom(context), - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), + padding: margin ?? Styles.bodySectionPadding, + child: Column( + children: [ + if (header != null) + Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DefaultTextStyle.merge(style: Styles.sectionTitle, child: header!), + if (headerTrailing != null) headerTrailing!, + ], ), - separatorColor: - Styles.cupertinoSeparatorColor.resolveFrom(context), - margin: EdgeInsets.zero, - hasLeading: hasLeading, - additionalDividerMargin: cupertinoAdditionalDividerMargin, - children: children, ), - ], - ), - ); + CupertinoListSection.insetGrouped( + clipBehavior: cupertinoClipBehavior, + backgroundColor: + cupertinoBackgroundColor ?? + CupertinoTheme.of(context).scaffoldBackgroundColor, + decoration: BoxDecoration( + color: + cupertinoBackgroundColor ?? + Styles.cupertinoCardColor.resolveFrom(context), + borderRadius: + cupertinoBorderRadius ?? const BorderRadius.all(Radius.circular(10.0)), + ), + separatorColor: Styles.cupertinoSeparatorColor.resolveFrom(context), + margin: EdgeInsets.zero, + hasLeading: hasLeading, + additionalDividerMargin: cupertinoAdditionalDividerMargin, + children: children, + ), + ], + ), + ); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); return const SizedBox.shrink(); @@ -241,21 +230,21 @@ class PlatformDivider extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.android ? Divider( - height: height, - thickness: thickness, - indent: indent, - endIndent: endIndent, - color: color, - ) + height: height, + thickness: thickness, + indent: indent, + endIndent: endIndent, + color: color, + ) : Divider( - height: height, - thickness: thickness ?? 0.0, - // see: - // https://github.com/flutter/flutter/blob/bff6b93683de8be01d53a39b6183f230518541ac/packages/flutter/lib/src/cupertino/list_section.dart#L53 - indent: indent ?? (cupertinoHasLeading ? 14 + 44.0 : 14.0), - endIndent: endIndent, - color: color ?? CupertinoColors.separator.resolveFrom(context), - ); + height: height, + thickness: thickness ?? 0.0, + // see: + // https://github.com/flutter/flutter/blob/bff6b93683de8be01d53a39b6183f230518541ac/packages/flutter/lib/src/cupertino/list_section.dart#L53 + indent: indent ?? (cupertinoHasLeading ? 14 + 44.0 : 14.0), + endIndent: endIndent, + color: color ?? CupertinoColors.separator.resolveFrom(context), + ); } } @@ -278,6 +267,7 @@ class PlatformListTile extends StatelessWidget { this.cupertinoBackgroundColor, this.visualDensity, this.harmonizeCupertinoTitleStyle = false, + super.key, }); final Widget? leading; @@ -295,7 +285,6 @@ class PlatformListTile extends StatelessWidget { /// Useful on some screens where ListTiles with and without subtitle are mixed. final bool harmonizeCupertinoTitleStyle; - // only on android final bool selected; // only on android @@ -320,14 +309,13 @@ class PlatformListTile extends StatelessWidget { leading: leading, title: title, iconColor: Theme.of(context).colorScheme.outline, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, trailing: trailing, dense: dense, visualDensity: visualDensity, @@ -339,26 +327,27 @@ class PlatformListTile extends StatelessWidget { ); case TargetPlatform.iOS: return IconTheme( - data: CupertinoIconThemeData( - color: CupertinoColors.systemGrey.resolveFrom(context), - ), + data: CupertinoIconThemeData(color: CupertinoColors.systemGrey.resolveFrom(context)), child: GestureDetector( onLongPress: onLongPress, child: CupertinoListTile.notched( - backgroundColor: cupertinoBackgroundColor, + backgroundColor: + selected == true + ? CupertinoColors.systemGrey4.resolveFrom(context) + : cupertinoBackgroundColor, leading: leading, - title: harmonizeCupertinoTitleStyle - ? DefaultTextStyle.merge( - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - ), - child: title, - ) - : title, + title: + harmonizeCupertinoTitleStyle + ? DefaultTextStyle.merge( + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), + child: title, + ) + : title, subtitle: subtitle, - trailing: trailing, + trailing: + trailing ?? + (selected == true ? const Icon(CupertinoIcons.check_mark_circled_solid) : null), additionalInfo: additionalInfo, padding: padding, onTap: onTap, @@ -372,3 +361,144 @@ class PlatformListTile extends StatelessWidget { } } } + +/// A [ListTile] that adapts to the platform. +/// +/// Contrary to [PlatformListTile], this widget uses a [ListTile] on both iOS and +/// Android. +/// On Android the list tile will be displayed without modifications. +/// On iOS the list tile will have a custom splash factory to remove the splash +/// effect. +class AdaptiveListTile extends StatelessWidget { + const AdaptiveListTile({ + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + super.key, + }); + + final Widget? leading; + final Widget title; + final Widget? subtitle; + final Widget? trailing; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Theme( + data: Theme.of(context).copyWith( + splashFactory: + Theme.of(context).platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, + ), + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + onTap: onTap, + ), + ), + ); + } +} + +typedef RemovedItemBuilder = + Widget Function(T item, BuildContext context, Animation animation); + +/// Keeps a Dart [List] in sync with an [AnimatedList] or [SliverAnimatedList]. +/// +/// The [insert] and [removeAt] methods apply to both the internal list and +/// the animated list that belongs to [listKey]. +class AnimatedListModel { + AnimatedListModel({ + required this.listKey, + required this.removedItemBuilder, + Iterable? initialItems, + int? itemsOffset, + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; + + final GlobalKey listKey; + final RemovedItemBuilder removedItemBuilder; + final List _items; + final int itemsOffset; + + AnimatedListState? get _animatedList => listKey.currentState; + + void prepend(E item) { + _items.insert(0, item); + _animatedList!.insertItem(itemsOffset); + } + + void insert(int index, E item) { + _items.insert(index - itemsOffset, item); + _animatedList!.insertItem(index); + } + + E removeAt(int index) { + final E removedItem = _items.removeAt(index - itemsOffset); + if (removedItem != null) { + _animatedList!.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); + } + return removedItem; + } + + int get length => _items.length + itemsOffset; + + E operator [](int index) => _items[index - itemsOffset]; + + int indexOf(E item) => _items.indexOf(item) + itemsOffset; +} + +/// Keeps a Dart [List] in sync with a [SliverAnimatedList]. +/// +/// The [insert] and [removeAt] methods apply to both the internal list and +/// the animated list that belongs to [listKey]. +class SliverAnimatedListModel { + SliverAnimatedListModel({ + required this.listKey, + required this.removedItemBuilder, + Iterable? initialItems, + int? itemsOffset, + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; + + final GlobalKey listKey; + final RemovedItemBuilder removedItemBuilder; + final List _items; + final int itemsOffset; + + SliverAnimatedListState? get _animatedList => listKey.currentState; + + void prepend(E item) { + _items.insert(0, item); + _animatedList!.insertItem(itemsOffset); + } + + void insert(int index, E item) { + _items.insert(index - itemsOffset, item); + _animatedList!.insertItem(index); + } + + E removeAt(int index) { + final E removedItem = _items.removeAt(index - itemsOffset); + if (removedItem != null) { + _animatedList!.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); + } + return removedItem; + } + + int get length => _items.length + itemsOffset; + + E operator [](int index) => _items[index - itemsOffset]; + + int indexOf(E item) => _items.indexOf(item) + itemsOffset; +} diff --git a/lib/src/widgets/misc.dart b/lib/src/widgets/misc.dart index fe1e3e6a80..5bce81f586 100644 --- a/lib/src/widgets/misc.dart +++ b/lib/src/widgets/misc.dart @@ -4,11 +4,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:url_launcher/url_launcher.dart'; class LichessMessage extends StatefulWidget { - const LichessMessage({ - super.key, - this.style, - this.textAlign = TextAlign.start, - }); + const LichessMessage({super.key, this.style, this.textAlign = TextAlign.start}); final TextStyle? style; final TextAlign textAlign; @@ -38,10 +34,7 @@ class _LichessMessageState extends State { @override Widget build(BuildContext context) { - final trans = context.l10n.xIsAFreeYLibreOpenSourceChessServer( - 'Lichess', - context.l10n.really, - ); + final trans = context.l10n.xIsAFreeYLibreOpenSourceChessServer('Lichess', context.l10n.really); final regexp = RegExp(r'''^([^(]*\()([^)]*)(\).*)$'''); final match = regexp.firstMatch(trans); final List spans = []; @@ -50,9 +43,7 @@ class _LichessMessageState extends State { spans.add( TextSpan( text: match[i], - style: i == 2 - ? TextStyle(color: Theme.of(context).colorScheme.primary) - : null, + style: i == 2 ? TextStyle(color: Theme.of(context).colorScheme.primary) : null, recognizer: i == 2 ? _recognizer : null, ), ); @@ -61,12 +52,6 @@ class _LichessMessageState extends State { spans.add(TextSpan(text: trans)); } - return Text.rich( - TextSpan( - style: widget.style, - children: spans, - ), - textAlign: widget.textAlign, - ); + return Text.rich(TextSpan(style: widget.style, children: spans), textAlign: widget.textAlign); } } diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart new file mode 100644 index 0000000000..00ef39b310 --- /dev/null +++ b/lib/src/widgets/move_list.dart @@ -0,0 +1,269 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; + +import 'platform.dart'; + +const _scrollAnimationDuration = Duration(milliseconds: 200); +const _moveListOpacity = 0.8; + +const _kMoveListHeight = 40.0; + +enum MoveListType { inline, stacked } + +class MoveList extends ConsumerStatefulWidget { + const MoveList({ + required this.type, + required this.slicedMoves, + required this.currentMoveIndex, + this.inlineColor, + this.inlineDecoration, + this.onSelectMove, + }); + + final MoveListType type; + + final Color? inlineColor; + + final BoxDecoration? inlineDecoration; + + final Iterable>> slicedMoves; + + final int currentMoveIndex; + final void Function(int moveIndex)? onSelectMove; + + @override + ConsumerState createState() => _MoveListState(); +} + +class _MoveListState extends ConsumerState { + final currentMoveKey = GlobalKey(); + final _debounce = Debouncer(const Duration(milliseconds: 100)); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible(currentMoveKey.currentContext!, alignment: 0.5); + } + }); + } + + @override + void dispose() { + _debounce.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant MoveList oldWidget) { + super.didUpdateWidget(oldWidget); + _debounce(() { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + duration: _scrollAnimationDuration, + curve: Curves.easeIn, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); + + return widget.type == MoveListType.inline + ? Container( + decoration: widget.inlineDecoration, + padding: const EdgeInsets.only(left: 5), + height: _kMoveListHeight, + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: widget.slicedMoves + .mapIndexed( + (index, moves) => Container( + margin: const EdgeInsets.only(right: 10), + child: Row( + children: [ + InlineMoveCount( + pieceNotation: pieceNotation, + count: index + 1, + color: widget.inlineColor, + ), + ...moves.map((move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = widget.currentMoveIndex == move.key + 1; + return InlineMoveItem( + key: isCurrentMove ? currentMoveKey : null, + move: move, + color: widget.inlineColor, + pieceNotation: pieceNotation, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ); + }), + ], + ), + ), + ) + .toList(growable: false), + ), + ), + ) + : PlatformCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + children: widget.slicedMoves + .mapIndexed( + (index, moves) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StackedMoveCount(count: index + 1), + Expanded( + child: Row( + children: [ + ...moves.map((move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = widget.currentMoveIndex == move.key + 1; + return Expanded( + child: StackedMoveItem( + key: isCurrentMove ? currentMoveKey : null, + move: move, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ), + ); + }), + ], + ), + ), + ], + ), + ) + .toList(growable: false), + ), + ), + ), + ); + } +} + +class InlineMoveCount extends StatelessWidget { + const InlineMoveCount({required this.count, required this.pieceNotation, this.color}); + + final PieceNotation pieceNotation; + final int count; + + final Color? color; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(right: 3), + child: Text( + '$count.', + style: TextStyle( + fontWeight: FontWeight.w500, + color: color?.withValues(alpha: _moveListOpacity) ?? textShade(context, _moveListOpacity), + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + ), + ), + ); + } +} + +class InlineMoveItem extends StatelessWidget { + const InlineMoveItem({ + required this.move, + required this.pieceNotation, + this.color, + this.current, + this.onSelectMove, + super.key, + }); + + final Color? color; + + final MapEntry move; + final PieceNotation pieceNotation; + final bool? current; + final void Function(int moveIndex)? onSelectMove; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 4), + child: Text( + move.value, + style: TextStyle( + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + fontWeight: current == true ? FontWeight.bold : FontWeight.w500, + color: + current != true + ? color != null + ? color!.withValues(alpha: _moveListOpacity) + : textShade(context, _moveListOpacity) + : Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } +} + +class StackedMoveCount extends StatelessWidget { + const StackedMoveCount({required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 40.0, + child: Text( + '$count.', + style: TextStyle(fontWeight: FontWeight.w600, color: textShade(context, _moveListOpacity)), + ), + ); + } +} + +class StackedMoveItem extends StatelessWidget { + const StackedMoveItem({required this.move, this.current, this.onSelectMove, super.key}); + + final MapEntry move; + final bool? current; + final void Function(int moveIndex)? onSelectMove; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + move.value, + style: TextStyle( + fontWeight: current == true ? FontWeight.bold : null, + color: current != true ? textShade(context, 0.8) : null, + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/non_linear_slider.dart b/lib/src/widgets/non_linear_slider.dart index 31a9f4a335..288f54f716 100644 --- a/lib/src/widgets/non_linear_slider.dart +++ b/lib/src/widgets/non_linear_slider.dart @@ -9,8 +9,8 @@ class NonLinearSlider extends StatefulWidget { this.onChange, this.onChangeEnd, super.key, - }) : assert(values.length > 1), - assert(values.contains(value)); + }) : assert(values.length > 1), + assert(values.contains(value)); final num value; final List values; @@ -46,27 +46,25 @@ class _NonLinearSliderState extends State { @override Widget build(BuildContext context) { return Opacity( - opacity: Theme.of(context).platform != TargetPlatform.iOS || - widget.onChangeEnd != null - ? 1 - : 0.5, + opacity: + Theme.of(context).platform != TargetPlatform.iOS || widget.onChangeEnd != null ? 1 : 0.5, child: Slider.adaptive( value: _index.toDouble(), min: 0, max: widget.values.length.toDouble() - 1, divisions: widget.values.length - 1, - label: widget.labelBuilder?.call(widget.values[_index]) ?? - widget.values[_index].toString(), - onChanged: widget.onChangeEnd != null - ? (double value) { - final newIndex = value.toInt(); - setState(() { - _index = newIndex; - }); + label: widget.labelBuilder?.call(widget.values[_index]) ?? widget.values[_index].toString(), + onChanged: + widget.onChangeEnd != null + ? (double value) { + final newIndex = value.toInt(); + setState(() { + _index = newIndex; + }); - widget.onChange?.call(widget.values[_index]); - } - : null, + widget.onChange?.call(widget.values[_index]); + } + : null, onChangeEnd: (double value) { widget.onChangeEnd?.call(widget.values[_index]); }, diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart new file mode 100644 index 0000000000..2b4b414982 --- /dev/null +++ b/lib/src/widgets/pgn.dart @@ -0,0 +1,1152 @@ +import 'dart:async'; + +import 'package:chessground/chessground.dart'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +const innacuracyColor = LichessColors.cyan; +const mistakeColor = Color(0xFFe69f00); +const blunderColor = Color(0xFFdf5353); + +Color? nagColor(int nag) { + return switch (nag) { + 1 => Colors.lightGreen, + 2 => mistakeColor, + 3 => Colors.teal, + 4 => blunderColor, + 5 => LichessColors.purple, + 6 => LichessColors.cyan, + int() => null, + }; +} + +String moveAnnotationChar(Iterable nags) { + return nags + .map( + (nag) => switch (nag) { + 1 => '!', + 2 => '?', + 3 => '!!', + 4 => '??', + 5 => '!?', + 6 => '?!', + int() => '', + }, + ) + .join(''); +} + +Annotation? makeAnnotation(Iterable? nags) { + final nag = nags?.firstOrNull; + if (nag == null) { + return null; + } + return switch (nag) { + 1 => const Annotation(symbol: '!', color: Colors.lightGreen), + 3 => const Annotation(symbol: '!!', color: Colors.teal), + 5 => const Annotation(symbol: '!?', color: Colors.purple), + 6 => const Annotation(symbol: '?!', color: LichessColors.cyan), + 2 => const Annotation(symbol: '?', color: mistakeColor), + 4 => const Annotation(symbol: '??', color: blunderColor), + int() => null, + }; +} + +// fast replay debounce delay, same as piece animation duration, to avoid piece +// animation jank at the end of the replay +const kFastReplayDebounceDelay = Duration(milliseconds: 150); + +/// Callbacks for interaction with [DebouncedPgnTreeView] +abstract class PgnTreeNotifier { + void expandVariations(UciPath path); + void collapseVariations(UciPath path); + void promoteVariation(UciPath path, bool toMainLine); + void deleteFromHere(UciPath path); + void userJump(UciPath path); +} + +/// Displays a tree-like view of a PGN game's moves. Path changes are debounced to avoid rebuilding the whole tree on every move. +/// +/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: +/// 1. e4 e5 // [_MainLinePart] +/// |- 1... d5 // [_SideLinePart] +/// |- 1... Nc6 // [_SideLinePart] +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline +/// Short sidelines without any branching are displayed inline with their parent line. +/// Longer sidelines are displayed on a new line and indented. +/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. +/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. +class DebouncedPgnTreeView extends ConsumerStatefulWidget { + const DebouncedPgnTreeView({ + required this.root, + required this.currentPath, + this.broadcastLivePath, + required this.pgnRootComments, + required this.notifier, + this.shouldShowComputerVariations = true, + this.shouldShowAnnotations = true, + this.shouldShowComments = true, + }); + + /// Root of the PGN tree to display + final ViewRoot root; + + /// Path to the currently selected move in the tree + final UciPath currentPath; + + /// Path to the last live move in the tree if it is a broadcast game + final UciPath? broadcastLivePath; + + /// Comments associated with the root node + final IList? pgnRootComments; + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move or collapsing variations + final PgnTreeNotifier notifier; + + /// Whether to show analysis variations. + /// + /// Only applied to lichess game analysis. + final bool shouldShowComputerVariations; + + /// Whether to show NAG annotations like '!' and '??'. + final bool shouldShowAnnotations; + + /// Whether to show comments associated with the moves. + final bool shouldShowComments; + + @override + ConsumerState createState() => _DebouncedPgnTreeViewState(); +} + +class _DebouncedPgnTreeViewState extends ConsumerState { + final currentMoveKey = GlobalKey(); + final _debounce = Debouncer(kFastReplayDebounceDelay); + + /// Path to the currently selected move in the tree. When widget.currentPath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every played move. + late UciPath pathToCurrentMove; + + /// Path to the last live move in the tree if it is a broadcast game. When widget.broadcastLivePath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every received move. + late UciPath? pathToBroadcastLiveMove; + + Timer? _scrollTimer; + + @override + void initState() { + super.initState(); + pathToCurrentMove = widget.currentPath; + pathToBroadcastLiveMove = widget.broadcastLivePath; + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollTimer?.cancel(); + _scrollTimer = Timer(const Duration(milliseconds: 500), () { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + } + }); + }); + } + + @override + void dispose() { + _debounce.dispose(); + _scrollTimer?.cancel(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant DebouncedPgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.currentPath != widget.currentPath || + oldWidget.broadcastLivePath != widget.broadcastLivePath) { + // debouncing the current and broadcast live path changes to avoid rebuilding when using + // the fast replay buttons or when receiving a lot of broadcast moves in a short time + _debounce(() { + setState(() { + if (oldWidget.currentPath != widget.currentPath) { + pathToCurrentMove = widget.currentPath; + } + if (oldWidget.broadcastLivePath != widget.broadcastLivePath) { + pathToBroadcastLiveMove = widget.broadcastLivePath; + } + }); + if (oldWidget.currentPath != widget.currentPath) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, + ); + } + }); + } + }); + } + } + + // This is the most expensive part of the pgn tree view because of the tree + // that may be very large. + // Great care must be taken to avoid unnecessary rebuilds. + // This should actually rebuild only when the current path changes or a new node + // is added. + // Debouncing the current path change is necessary to avoid rebuilding when + // using the fast replay buttons. + @override + Widget build(BuildContext context) { + return _PgnTreeView( + root: widget.root, + rootComments: widget.pgnRootComments, + params: ( + shouldShowComputerVariations: widget.shouldShowComputerVariations, + shouldShowAnnotations: widget.shouldShowAnnotations, + shouldShowComments: widget.shouldShowComments, + currentMoveKey: currentMoveKey, + pathToCurrentMove: pathToCurrentMove, + pathToBroadcastLiveMove: pathToBroadcastLiveMove, + notifier: widget.notifier, + ), + ); + } +} + +/// A group of parameters that are passed through various parts of the tree view +/// and ultimately evaluated in the [InlineMove] widget. +/// +/// Grouped in this record to improve readability. +typedef _PgnTreeViewParams = + ({ + /// Path to the currently selected move in the tree. + UciPath pathToCurrentMove, + + /// Path to the last live move in the tree if it is a broadcast game + UciPath? pathToBroadcastLiveMove, + + /// Whether to show analysis variations. + bool shouldShowComputerVariations, + + /// Whether to show NAG annotations like '!' and '??'. + bool shouldShowAnnotations, + + /// Whether to show comments associated with the moves. + bool shouldShowComments, + + /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. + /// Can be used e.g. to ensure that the current move is visible on the screen. + GlobalKey currentMoveKey, + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. + PgnTreeNotifier notifier, + }); + +/// Filter node children when computer analysis is disabled +IList _filteredChildren(ViewNode node, bool shouldShowComputerVariations) { + return node.children + .where((c) => shouldShowComputerVariations || !c.isComputerVariation) + .toIList(); +} + +/// Whether to display the sideline inline. +/// +/// Sidelines are usually rendered on a new line and indented. +/// However sidelines are rendered inline (in parantheses) if the side line has no branching and is less than 6 moves deep. +bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { + if (depth == 6) return false; + if (node.children.isEmpty) return true; + if (node.children.length > 1) return false; + return _displaySideLineAsInline(node.children.first, depth + 1); +} + +/// Returns whether this node has a sideline that should not be displayed inline. +bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { + final children = _filteredChildren(node, params.shouldShowComputerVariations); + return children.length > 2 || (children.length == 2 && !_displaySideLineAsInline(children[1])); +} + +/// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. +/// +/// A part ends when a mainline node has a sideline that should not be displayed inline. +Iterable> _mainlineParts(ViewRoot root, _PgnTreeViewParams params) => + [root, ...root.mainline] + .splitAfter((n) => _hasNonInlineSideLine(n, params)) + .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); + +class _PgnTreeView extends StatefulWidget { + const _PgnTreeView({required this.root, required this.rootComments, required this.params}); + + /// Root of the PGN tree + final ViewRoot root; + + /// Comments associated with the root node + final IList? rootComments; + + final _PgnTreeViewParams params; + + @override + State<_PgnTreeView> createState() => _PgnTreeViewState(); +} + +/// A record that holds the rendered parts of a subtree. +typedef _CachedRenderedSubtree = + ({ + /// The mainline part of the subtree. + _MainLinePart mainLinePart, + + /// The sidelines part of the subtree. + /// + /// This is nullable since the very last mainline part might not have any sidelines. + _IndentedSideLines? sidelines, + + /// Whether the subtree contains the current move. + bool containsCurrentMove, + }); + +class _PgnTreeViewState extends State<_PgnTreeView> { + /// Caches the result of [_mainlineParts], it only needs to be recalculated when the root changes, + /// but not when `params.pathToCurrentMove` changes. + List> mainlineParts = []; + + /// Cache of the top-level subtrees obtained from the last `build()` method. + /// + /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. + /// The framework will skip the `build()` of each subtree since the widget reference is the same. + List<_CachedRenderedSubtree> subtrees = []; + + UciPath _mainlinePartOfCurrentPath() { + var path = UciPath.empty; + for (final node in widget.root.mainline) { + if (!widget.params.pathToCurrentMove.contains(path + node.id)) { + break; + } + path = path + node.id; + } + return path; + } + + List<_CachedRenderedSubtree> _buildChangedSubtrees({required bool fullRebuild}) { + var path = UciPath.empty; + return mainlineParts + .mapIndexed((i, mainlineNodes) { + final mainlineInitialPath = path; + + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + mainlineNodes.take(mainlineNodes.length - 1).map((n) => n.children.first.id), + ), + ); + + path = sidelineInitialPath; + if (mainlineNodes.last.children.isNotEmpty) { + path = path + mainlineNodes.last.children.first.id; + } + + final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); + final containsCurrentMove = + mainlinePartOfCurrentPath.size > mainlineInitialPath.size && + mainlinePartOfCurrentPath.size <= path.size; + + if (fullRebuild || subtrees[i].containsCurrentMove || containsCurrentMove) { + // Skip the first node which is the continuation of the mainline + final sidelineNodes = mainlineNodes.last.children.skip(1); + return ( + mainLinePart: _MainLinePart( + params: widget.params, + initialPath: mainlineInitialPath, + nodes: mainlineNodes, + ), + sidelines: + sidelineNodes.isNotEmpty + ? _IndentedSideLines( + sidelineNodes, + parent: mainlineNodes.last, + params: widget.params, + initialPath: sidelineInitialPath, + nesting: 1, + ) + : null, + containsCurrentMove: containsCurrentMove, + ); + } else { + // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change + return subtrees[i]; + } + }) + .toList(growable: false); + } + + void _updateLines({required bool fullRebuild}) { + setState(() { + if (fullRebuild) { + mainlineParts = _mainlineParts(widget.root, widget.params).toList(growable: false); + } + + subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); + }); + } + + @override + void initState() { + super.initState(); + _updateLines(fullRebuild: true); + } + + @override + void didUpdateWidget(covariant _PgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + _updateLines( + fullRebuild: + oldWidget.root != widget.root || + oldWidget.params.shouldShowComputerVariations != + widget.params.shouldShowComputerVariations || + oldWidget.params.shouldShowComments != widget.params.shouldShowComments || + oldWidget.params.shouldShowAnnotations != widget.params.shouldShowAnnotations, + ); + } + + @override + Widget build(BuildContext context) { + final rootComments = widget.rootComments?.map((c) => c.text).nonNulls ?? []; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // trick to make auto-scroll work when returning to the root position + if (widget.params.pathToCurrentMove.isEmpty) + SizedBox.shrink(key: widget.params.currentMoveKey), + + if (widget.params.shouldShowComments && rootComments.isNotEmpty) + Text.rich(TextSpan(children: _comments(rootComments, textStyle: _baseTextStyle))), + ...subtrees + .map((part) => [part.mainLinePart, if (part.sidelines != null) part.sidelines!]) + .flattened, + ], + ), + ); + } +} + +List _buildInlineSideLine({ + required ViewBranch firstNode, + required ViewNode parent, + required UciPath initialPath, + required TextStyle textStyle, + required bool followsComment, + required _PgnTreeViewParams params, +}) { + textStyle = textStyle.copyWith( + fontSize: textStyle.fontSize != null ? textStyle.fontSize! - 2.0 : null, + ); + + final sidelineNodes = [firstNode, ...firstNode.mainline]; + + var path = initialPath; + return [ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + ...sidelineNodes.mapIndexedAndLast((i, node, last) { + final pathToNode = path; + path = path + node.id; + + return [ + if (i == 0) ...[ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + TextSpan(text: '(', style: textStyle), + ], + ..._moveWithComment( + node, + lineInfo: ( + type: _LineType.inlineSideline, + startLine: i == 0 || (params.shouldShowComments && sidelineNodes[i - 1].hasTextComment), + pathToLine: initialPath, + ), + pathToNode: pathToNode, + textStyle: textStyle, + params: params, + ), + if (last) TextSpan(text: ')', style: textStyle), + ]; + }).flattened, + const WidgetSpan(child: SizedBox(width: 4.0)), + ]; +} + +const _baseTextStyle = TextStyle(fontSize: 16.0, height: 1.5); + +/// The different types of lines (move sequences) that are displayed in the tree view. +enum _LineType { + /// (A part of) the game's main line. + mainline, + + /// A sideline branching off the main line or a parent sideline. + /// + /// Each sideline is rendered on a new line and indented. + sideline, + + /// A short sideline without any branching, displayed in parantheses inline with it's parent line. + inlineSideline, +} + +/// Metadata about a move's role in the tree view. +typedef _LineInfo = ({_LineType type, bool startLine, UciPath pathToLine}); + +List _moveWithComment( + ViewBranch branch, { + required TextStyle textStyle, + required _LineInfo lineInfo, + required UciPath pathToNode, + required _PgnTreeViewParams params, + + /// Optional [GlobalKey] that will be assigned to the [InlineMove] widget. + /// + /// It should only be set if it is the first move of a sideline. + /// We use this to track the position of the first move widget. See [_SideLinePart.firstMoveKey]. + GlobalKey? firstMoveKey, +}) { + return [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: InlineMove( + key: firstMoveKey, + branch: branch, + lineInfo: lineInfo, + path: pathToNode + branch.id, + textStyle: textStyle, + params: params, + ), + ), + if (params.shouldShowComments && branch.hasTextComment) + ..._comments(branch.textComments, textStyle: textStyle), + ]; +} + +/// A widget that renders part of a sideline, where each move is displayed on the same line without branching. +/// +/// Each node in the sideline has only one child (or two children where the second child is rendered as an inline sideline). +class _SideLinePart extends ConsumerWidget { + _SideLinePart( + this.nodes, { + required this.initialPath, + required this.firstMoveKey, + required this.params, + }) : assert(nodes.isNotEmpty); + + final List nodes; + + final UciPath initialPath; + + /// The key that will be assigned to the first move in this sideline. + /// + /// This is needed so that the indent guidelines can be drawn correctly. + final GlobalKey firstMoveKey; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith( + color: _textColor(context, 0.6), + fontSize: _baseTextStyle.fontSize! - 1.0, + ); + + var path = initialPath + nodes.first.id; + final moves = [ + ..._moveWithComment( + nodes.first, + lineInfo: (type: _LineType.sideline, startLine: true, pathToLine: initialPath), + firstMoveKey: firstMoveKey, + pathToNode: initialPath, + textStyle: textStyle, + params: params, + ), + ...nodes.take(nodes.length - 1).map((node) { + final moves = [ + ..._moveWithComment( + node.children.first, + lineInfo: ( + type: _LineType.sideline, + startLine: params.shouldShowComments && node.hasTextComment, + pathToLine: initialPath, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && _displaySideLineAsInline(node.children[1])) + ..._buildInlineSideLine( + followsComment: node.children.first.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ]; + path = path + node.children.first.id; + return moves; + }).flattened, + ]; + + return Text.rich(TextSpan(children: moves)); + } +} + +/// A widget that renders part of the mainline. +/// +/// A part of the mainline is rendered on a single line. See [_mainlineParts]. +/// +/// For example: +/// 1. e4 e5 <-- mainline part +/// |- 1... d5 <-- sideline part +/// |- 1... Nc6 <-- sideline part +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 <-- mainline part +class _MainLinePart extends ConsumerWidget { + const _MainLinePart({required this.initialPath, required this.params, required this.nodes}); + + final UciPath initialPath; + + final List nodes; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith(color: _textColor(context, 0.9)); + + var path = initialPath; + return Text.rich( + TextSpan( + children: nodes + .takeWhile( + (node) => _filteredChildren(node, params.shouldShowComputerVariations).isNotEmpty, + ) + .mapIndexed((i, node) { + final children = _filteredChildren(node, params.shouldShowComputerVariations); + final mainlineNode = children.first; + final moves = [ + _moveWithComment( + mainlineNode, + lineInfo: ( + type: _LineType.mainline, + startLine: + i == 0 || + (params.shouldShowComments && (node as ViewBranch).hasTextComment), + pathToLine: initialPath, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (children.length == 2 && _displaySideLineAsInline(children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }) + .flattened + .toList(growable: false), + ), + ); + } +} + +/// A widget that renders a sideline. +/// +/// The moves are rendered on the same line (see [_SideLinePart]) until further +/// branching is encountered, at which point the children sidelines are rendered +/// on new lines and indented (see [_IndentedSideLines]). +class _SideLine extends StatelessWidget { + const _SideLine({ + required this.firstNode, + required this.parent, + required this.firstMoveKey, + required this.initialPath, + required this.params, + required this.nesting, + }); + + final ViewBranch firstNode; + final ViewNode parent; + final GlobalKey firstMoveKey; + final UciPath initialPath; + final _PgnTreeViewParams params; + final int nesting; + + List _getSidelinePartNodes() { + final sidelineNodes = [firstNode]; + while (sidelineNodes.last.children.isNotEmpty && + !_hasNonInlineSideLine(sidelineNodes.last, params)) { + sidelineNodes.add(sidelineNodes.last.children.first); + } + return sidelineNodes.toList(growable: false); + } + + @override + Widget build(BuildContext context) { + final sidelinePartNodes = _getSidelinePartNodes(); + + final lastNodeChildren = sidelinePartNodes.last.children; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SideLinePart( + sidelinePartNodes, + firstMoveKey: firstMoveKey, + initialPath: initialPath, + params: params, + ), + if (lastNodeChildren.isNotEmpty) + _IndentedSideLines( + lastNodeChildren, + parent: sidelinePartNodes.last, + initialPath: UciPath.join( + initialPath, + UciPath.fromIds(sidelinePartNodes.map((node) => node.id)), + ), + params: params, + nesting: nesting + 1, + ), + ], + ); + } +} + +class _IndentPainter extends CustomPainter { + const _IndentPainter({ + required this.sideLineStartPositions, + required this.color, + required this.padding, + }); + + final List sideLineStartPositions; + + final Color color; + + final double padding; + + @override + void paint(Canvas canvas, Size size) { + if (sideLineStartPositions.isNotEmpty) { + final paint = + Paint() + ..strokeWidth = 1.5 + ..color = color + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + final origin = Offset(-padding, 0); + + final path = Path()..moveTo(origin.dx, origin.dy); + path.lineTo(origin.dx, sideLineStartPositions.last.dy); + for (final position in sideLineStartPositions) { + path.moveTo(origin.dx, position.dy); + path.lineTo(origin.dx + padding / 2, position.dy); + } + canvas.drawPath(path, paint); + } + } + + @override + bool shouldRepaint(_IndentPainter oldDelegate) { + return oldDelegate.sideLineStartPositions != sideLineStartPositions || + oldDelegate.color != color; + } +} + +/// A widget that displays indented sidelines. +/// +/// Will show one ore more sidelines indented on their own line and add indent +/// guides. +/// If there are hidden lines, a "+" button is displayed to expand them. +class _IndentedSideLines extends StatefulWidget { + const _IndentedSideLines( + this.sideLines, { + required this.parent, + required this.initialPath, + required this.params, + required this.nesting, + }); + + final Iterable sideLines; + + final ViewNode parent; + + final UciPath initialPath; + + final _PgnTreeViewParams params; + + final int nesting; + + @override + State<_IndentedSideLines> createState() => _IndentedSideLinesState(); +} + +class _IndentedSideLinesState extends State<_IndentedSideLines> { + /// Keys for the first move of each sideline. + /// + /// Used to calculate the position of the indent guidelines. The keys are + /// assigned to the first move of each sideline. The position of the keys is + /// used to calculate the position of the indent guidelines. A [GlobalKey] is + /// necessary because the exact position of the first move is not known until the + /// widget is rendered, as the vertical space can vary depending on the length of + /// the line, and if the line is wrapped. + late List _sideLinesStartKeys; + + /// The position of the first move of each sideline computed relative to the column and derived from the [GlobalKey] found in [_sideLinesStartKeys]. + List _sideLineStartPositions = []; + + /// The [GlobalKey] for the column that contains the side lines. + final GlobalKey _columnKey = GlobalKey(); + + /// Redraws the indents on demand. + /// + /// Will re-generate the [GlobalKey]s for the first move of each sideline and + /// calculate the position of the indents in a post-frame callback. + void _redrawIndents() { + _sideLinesStartKeys = List.generate( + _expandedSidelines.length + (_hasCollapsedLines ? 1 : 0), + (_) => GlobalKey(), + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + final RenderBox? columnBox = _columnKey.currentContext?.findRenderObject() as RenderBox?; + final Offset rowOffset = columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; + + final positions = _sideLinesStartKeys + .map((key) { + final context = key.currentContext; + final renderBox = context?.findRenderObject() as RenderBox?; + final height = renderBox?.size.height ?? 0; + final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + return Offset(offset.dx, offset.dy + height / 2) - rowOffset; + }) + .toList(growable: false); + + setState(() { + _sideLineStartPositions = positions; + }); + }); + } + + bool get _hasCollapsedLines => widget.sideLines.any((node) => node.isCollapsed); + + Iterable get _expandedSidelines => + widget.sideLines.whereNot((node) => node.isCollapsed); + + @override + void initState() { + super.initState(); + _redrawIndents(); + } + + @override + void didUpdateWidget(covariant _IndentedSideLines oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.sideLines != widget.sideLines) { + _redrawIndents(); + } + } + + @override + Widget build(BuildContext context) { + final sideLineWidgets = _expandedSidelines + .mapIndexed( + (i, firstSidelineNode) => _SideLine( + firstNode: firstSidelineNode, + parent: widget.parent, + firstMoveKey: _sideLinesStartKeys[i], + initialPath: widget.initialPath, + params: widget.params, + nesting: widget.nesting, + ), + ) + .toList(growable: false); + + final padding = widget.nesting < 6 ? 12.0 : 0.0; + + return Padding( + padding: EdgeInsets.only(left: padding), + child: CustomPaint( + painter: _IndentPainter( + sideLineStartPositions: _sideLineStartPositions, + color: _textColor(context, 0.6)!, + padding: padding, + ), + child: Column( + key: _columnKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...sideLineWidgets, + if (_hasCollapsedLines) + GestureDetector( + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + key: _sideLinesStartKeys.last, + size: _baseTextStyle.fontSize! + 5, + ), + onTap: () { + widget.params.notifier.expandVariations(widget.initialPath); + }, + ), + ], + ), + ), + ); + } +} + +Color? _textColor(BuildContext context, double opacity, {int? nag}) { + final defaultColor = + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) + : CupertinoTheme.of(context).textTheme.textStyle.color?.withValues(alpha: opacity); + + return nag != null && nag > 0 ? nagColor(nag) : defaultColor; +} + +/// A widget that displays a single move in the tree view. +/// +/// The move can optionnally be preceded by an index, and followed by a nag annotation. +/// The move is displayed as a clickable button that will jump to the move when pressed. +/// The move is highlighted if it is the current move. +/// A long press on the move will display a context menu with options to promote the move to the main line, collapse variations, etc. +class InlineMove extends ConsumerWidget { + const InlineMove({ + required this.branch, + required this.path, + required this.textStyle, + required this.lineInfo, + required this.params, + super.key, + }); + + final ViewBranch branch; + final UciPath path; + + final TextStyle textStyle; + + final _LineInfo lineInfo; + + final _PgnTreeViewParams params; + + static const borderRadius = BorderRadius.all(Radius.circular(4.0)); + + bool get isCurrentMove => params.pathToCurrentMove == path; + + bool get isBroadcastLiveMove => params.pathToBroadcastLiveMove == path; + + BoxDecoration? _boxDecoration(BuildContext context, bool isCurrentMove, bool isLiveMove) { + return (isCurrentMove || isLiveMove) + ? BoxDecoration( + color: + isCurrentMove + ? Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey3.resolveFrom(context) + : Theme.of(context).focusColor + : null, + shape: BoxShape.rectangle, + borderRadius: borderRadius, + border: isLiveMove ? Border.all(width: 2, color: Colors.orange) : null, + ) + : null; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); + final moveFontFamily = pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; + + final moveTextStyle = textStyle.copyWith( + fontFamily: moveFontFamily, + fontWeight: lineInfo.type == _LineType.inlineSideline ? FontWeight.normal : FontWeight.w600, + ); + + final indexTextStyle = textStyle.copyWith(color: _textColor(context, 0.6)); + + final indexText = + branch.position.ply.isOdd + ? TextSpan(text: '${(branch.position.ply / 2).ceil()}. ', style: indexTextStyle) + : (lineInfo.startLine + ? TextSpan(text: '${(branch.position.ply / 2).ceil()}… ', style: indexTextStyle) + : null); + + final moveWithNag = + branch.sanMove.san + + (branch.nags != null && params.shouldShowAnnotations + ? moveAnnotationChar(branch.nags!) + : ''); + + final nag = params.shouldShowAnnotations ? branch.nags?.firstOrNull : null; + + final ply = branch.position.ply; + return AdaptiveInkWell( + key: isCurrentMove ? params.currentMoveKey : null, + borderRadius: borderRadius, + onTap: () => params.notifier.userJump(path), + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: + (context) => _MoveContextMenu( + notifier: params.notifier, + title: + ply.isOdd + ? '${(ply / 2).ceil()}. $moveWithNag' + : '${(ply / 2).ceil()}... $moveWithNag', + path: path, + branch: branch, + lineInfo: lineInfo, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + decoration: _boxDecoration(context, isCurrentMove, isBroadcastLiveMove), + child: Text.rich( + TextSpan( + children: [ + if (indexText != null) ...[indexText], + TextSpan( + text: moveWithNag, + style: moveTextStyle.copyWith( + color: _textColor(context, isCurrentMove ? 1 : 0.9, nag: nag), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _MoveContextMenu extends ConsumerWidget { + const _MoveContextMenu({ + required this.title, + required this.path, + required this.branch, + required this.lineInfo, + required this.notifier, + }); + + final String title; + final UciPath path; + final ViewBranch branch; + final _LineInfo lineInfo; + final PgnTreeNotifier notifier; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomSheetScrollableContainer( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: Theme.of(context).textTheme.titleLarge), + if (branch.clock != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.punch_clock), + const SizedBox(width: 4.0), + Text( + branch.clock!.toHoursMinutesSeconds( + showTenths: branch.clock! < const Duration(minutes: 1), + ), + ), + ], + ), + if (branch.elapsedMoveTime != null) ...[ + const SizedBox(height: 4.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.hourglass_bottom), + const SizedBox(width: 4.0), + Text(branch.elapsedMoveTime!.toHoursMinutesSeconds(showTenths: true)), + ], + ), + ], + ], + ), + ], + ), + ), + if (branch.hasTextComment) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text(branch.textComments.join(' ')), + ), + const PlatformDivider(indent: 0), + if (lineInfo.type != _LineType.mainline) ...[ + BottomSheetContextMenuAction( + icon: Icons.subtitles_off, + child: Text(context.l10n.collapseVariations), + onPressed: () => notifier.collapseVariations(lineInfo.pathToLine), + ), + BottomSheetContextMenuAction( + icon: Icons.expand_less, + child: Text(context.l10n.promoteVariation), + onPressed: () => notifier.promoteVariation(path, false), + ), + BottomSheetContextMenuAction( + icon: Icons.check, + child: Text(context.l10n.makeMainLine), + onPressed: () => notifier.promoteVariation(path, true), + ), + ], + BottomSheetContextMenuAction( + icon: Icons.delete, + child: Text(context.l10n.deleteFromHere), + onPressed: () => notifier.deleteFromHere(path), + ), + ], + ); + } +} + +List _comments(Iterable comments, {required TextStyle textStyle}) => comments + .map( + (comment) => + TextSpan(text: comment, style: textStyle.copyWith(fontSize: textStyle.fontSize! - 2.0)), + ) + .toList(growable: false); diff --git a/lib/src/widgets/platform.dart b/lib/src/widgets/platform.dart index 483e1c6ff7..87fffb58b6 100644 --- a/lib/src/widgets/platform.dart +++ b/lib/src/widgets/platform.dart @@ -5,11 +5,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; /// A simple widget that builds different things on different platforms. class PlatformWidget extends StatelessWidget { - const PlatformWidget({ - super.key, - required this.androidBuilder, - required this.iosBuilder, - }); + const PlatformWidget({super.key, required this.androidBuilder, required this.iosBuilder}); final WidgetBuilder androidBuilder; final WidgetBuilder iosBuilder; @@ -28,10 +24,7 @@ class PlatformWidget extends StatelessWidget { } } -typedef ConsumerWidgetBuilder = Widget Function( - BuildContext context, - WidgetRef ref, -); +typedef ConsumerWidgetBuilder = Widget Function(BuildContext context, WidgetRef ref); /// A widget that builds different things on different platforms with riverpod. class ConsumerPlatformWidget extends StatelessWidget { @@ -71,6 +64,7 @@ class PlatformCard extends StatelessWidget { this.elevation, this.color, this.shadowColor, + this.clipBehavior, }); final Widget child; @@ -79,6 +73,7 @@ class PlatformCard extends StatelessWidget { final double? elevation; final Color? color; final Color? shadowColor; + final Clip? clipBehavior; /// The empty space that surrounds the card. /// @@ -92,37 +87,34 @@ class PlatformCard extends StatelessWidget { Widget build(BuildContext context) { return MediaQuery.withClampedTextScaling( maxScaleFactor: kCardTextScaleFactor, - child: Theme.of(context).platform == TargetPlatform.iOS - ? Card( - margin: margin ?? EdgeInsets.zero, - elevation: elevation ?? 0, - color: color ?? Styles.cupertinoCardColor.resolveFrom(context), - shadowColor: shadowColor, - shape: borderRadius != null - ? RoundedRectangleBorder( - borderRadius: borderRadius!, - ) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - semanticContainer: semanticContainer, - child: child, - ) - : Card( - shape: borderRadius != null - ? RoundedRectangleBorder( - borderRadius: borderRadius!, - ) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - color: color, - shadowColor: shadowColor, - semanticContainer: semanticContainer, - elevation: elevation, - margin: margin, - child: child, - ), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? Card( + margin: margin ?? EdgeInsets.zero, + elevation: elevation ?? 0, + color: color ?? Styles.cupertinoCardColor.resolveFrom(context), + shadowColor: shadowColor, + shape: + borderRadius != null + ? RoundedRectangleBorder(borderRadius: borderRadius!) + : const RoundedRectangleBorder(borderRadius: kCardBorderRadius), + semanticContainer: semanticContainer, + clipBehavior: clipBehavior, + child: child, + ) + : Card( + shape: + borderRadius != null + ? RoundedRectangleBorder(borderRadius: borderRadius!) + : const RoundedRectangleBorder(borderRadius: kCardBorderRadius), + color: color, + shadowColor: shadowColor, + semanticContainer: semanticContainer, + elevation: elevation, + margin: margin, + clipBehavior: clipBehavior, + child: child, + ), ); } } diff --git a/lib/src/widgets/platform_alert_dialog.dart b/lib/src/widgets/platform_alert_dialog.dart new file mode 100644 index 0000000000..9738f40f27 --- /dev/null +++ b/lib/src/widgets/platform_alert_dialog.dart @@ -0,0 +1,68 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// Displays an [AlertDialog] for Android and a [CupertinoAlertDialog] for iOS. +class PlatformAlertDialog extends StatelessWidget { + const PlatformAlertDialog({ + super.key, + this.title, + this.content, + this.actions = const [], + }); + + /// See [AlertDialog.title] for Android and [CupertinoAlertDialog.title] for iOS. + final Widget? title; + + /// See [AlertDialog.content] for Android and [CupertinoAlertDialog.content] for iOS. + final Widget? content; + + /// See [AlertDialog.actions] for Android and [CupertinoAlertDialog.actions] for iOS. + final List actions; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: (context) => AlertDialog(title: title, content: content, actions: actions), + iosBuilder: + (context) => CupertinoAlertDialog(title: title, content: content, actions: actions), + ); + } +} + +/// To be used with [PlatformAlertDialog.actions]. Displays a [TextButton] for Android and a [CupertinoDialogAction] for iOS. +class PlatformDialogAction extends StatelessWidget { + const PlatformDialogAction({ + super.key, + required this.onPressed, + required this.child, + this.cupertinoIsDefaultAction = false, + this.cupertinoIsDestructiveAction = false, + }); + + /// Callback invoked when the action is pressed. + final VoidCallback? onPressed; + + /// See [TextButton.child] for Android and [CupertinoDialogAction.child] for iOS. + final Widget child; + + /// Passed to [CupertinoDialogAction.isDefaultAction] on iOS, no effect on Android. + final bool cupertinoIsDefaultAction; + + /// Passed to [CupertinoDialogAction.isDestructiveAction] on iOS, no effect on Android. + final bool cupertinoIsDestructiveAction; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: (context) => TextButton(onPressed: onPressed, child: child), + iosBuilder: + (context) => CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: cupertinoIsDefaultAction, + isDestructiveAction: cupertinoIsDestructiveAction, + child: child, + ), + ); + } +} diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart new file mode 100644 index 0000000000..0edfc5d194 --- /dev/null +++ b/lib/src/widgets/platform_scaffold.dart @@ -0,0 +1,154 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +const kCupertinoAppBarWithActionPadding = EdgeInsetsDirectional.only(start: 16.0, end: 8.0); + +/// Displays an [AppBar] for Android and a [CupertinoNavigationBar] for iOS. +/// +/// Intended to be passed to [PlatformScaffold]. +class PlatformAppBar extends StatelessWidget { + const PlatformAppBar({ + super.key, + required this.title, + this.centerTitle = false, + this.leading, + this.actions = const [], + this.androidTitleSpacing, + }); + + /// Widget to place at the start of the navigation bar + /// + /// See [AppBar.leading] and [CupertinoNavigationBar.leading] for details + final Widget? leading; + + /// The title displayed in the middle of the bar. + /// + /// On Android, this is [AppBar.title], on iOS [CupertinoNavigationBar.middle] + final Widget title; + + /// On Android, this is passed directly to [AppBar.centerTitle]. Has no effect on iOS. + final bool centerTitle; + + /// Action widgets to place at the end of the navigation bar. + /// + /// On Android, this is passed directlty to [AppBar.actions]. + /// On iOS, this is wrapped in a [Row] and passed to [CupertinoNavigationBar.trailing] + final List actions; + + /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. + final double? androidTitleSpacing; + + AppBar _androidBuilder(BuildContext context) { + return AppBar( + titleSpacing: androidTitleSpacing, + leading: leading, + title: title, + centerTitle: centerTitle, + actions: actions, + ); + } + + CupertinoNavigationBar _iosBuilder(BuildContext context) { + return CupertinoNavigationBar( + padding: actions.isNotEmpty ? kCupertinoAppBarWithActionPadding : null, + middle: title, + trailing: Row(mainAxisSize: MainAxisSize.min, children: actions), + ); + } + + @override + Widget build(BuildContext context) { + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); + } +} + +/// A platform-aware circular loading indicator to be used in [PlatformAppBar.actions]. +class PlatformAppBarLoadingIndicator extends StatelessWidget { + const PlatformAppBarLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformWidget( + iosBuilder: (_) => const CircularProgressIndicator.adaptive(), + androidBuilder: + (_) => const Padding( + padding: EdgeInsets.only(right: 16), + child: SizedBox( + height: 24, + width: 24, + child: Center(child: CircularProgressIndicator()), + ), + ), + ); + } +} + +class _CupertinoNavBarWrapper extends StatelessWidget implements ObstructingPreferredSizeWidget { + const _CupertinoNavBarWrapper({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) => child; + + @override + Size get preferredSize => const Size.fromHeight(kMinInteractiveDimensionCupertino); + + /// True if the navigation bar's background color has no transparency. + @override + bool shouldFullyObstruct(BuildContext context) { + final Color backgroundColor = CupertinoTheme.of(context).barBackgroundColor; + return backgroundColor.a == 0xFF; + } +} + +/// A screen with a navigation bar and a body that adapts to the platform. +/// +/// On Android, this is a [Scaffold] with an [AppBar], +/// on iOS a [CupertinoPageScaffold] with a [CupertinoNavigationBar]. +/// +/// See [PlatformAppBar] for an app bar that adapts to the platform and needs to be passed to this widget. +class PlatformScaffold extends StatelessWidget { + const PlatformScaffold({ + super.key, + this.appBar, + required this.body, + this.resizeToAvoidBottomInset = true, + }); + + /// Acts as the [AppBar] for Android and as the [CupertinoNavigationBar] for iOS. + /// + /// Usually an instance of [PlatformAppBar]. + final Widget? appBar; + + /// The main content of the screen, displayed below the navigation bar. + final Widget body; + + /// See [Scaffold.resizeToAvoidBottomInset] and [CupertinoPageScaffold.resizeToAvoidBottomInset] + final bool resizeToAvoidBottomInset; + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + appBar: + appBar != null + ? PreferredSize(preferredSize: const Size.fromHeight(kToolbarHeight), child: appBar!) + : null, + body: body, + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + navigationBar: appBar != null ? _CupertinoNavBarWrapper(child: appBar!) : null, + child: body, + ); + } + + @override + Widget build(BuildContext context) { + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); + } +} diff --git a/lib/src/widgets/platform_search_bar.dart b/lib/src/widgets/platform_search_bar.dart new file mode 100644 index 0000000000..bc493bce12 --- /dev/null +++ b/lib/src/widgets/platform_search_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// Displays a [SearchBar] on Android and a [CupertinoSearchTextField] on iOS. +class PlatformSearchBar extends StatelessWidget { + const PlatformSearchBar({ + this.controller, + this.hintText, + this.autoFocus = false, + this.onClear, + this.onTap, + this.onSubmitted, + this.focusNode, + }); + + /// See [SearchBar.controller] and [CupertinoSearchTextField.controller]. + final TextEditingController? controller; + + /// Text that suggests what sort of input the field accepts. + /// + /// Displayed at the same location on the screen where text may be entered + /// when the input is empty. + final String? hintText; + + /// Whether this the search should focus itself if nothing else is already focused. + /// + /// Defaults to false. + final bool autoFocus; + + /// Called when the user taps this search bar. + final GestureTapCallback? onTap; + + /// Callback when the clear button is pressed. + /// + /// Defaults to clearing the text in the [controller]. + final VoidCallback? onClear; + + /// Callback when the search term is submitted. + final void Function(String term)? onSubmitted; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: + (context) => SearchBar( + controller: controller, + leading: const Icon(Icons.search), + trailing: [ + if (controller?.text.isNotEmpty == true) + IconButton( + onPressed: onClear ?? () => controller?.clear(), + tooltip: 'Clear', + icon: const Icon(Icons.close), + ), + ], + onTap: onTap, + focusNode: focusNode, + onSubmitted: onSubmitted, + hintText: hintText, + autoFocus: autoFocus, + ), + iosBuilder: + (context) => CupertinoSearchTextField( + controller: controller, + onTap: onTap, + focusNode: focusNode, + onSuffixTap: onClear, + onSubmitted: onSubmitted, + placeholder: hintText, + autofocus: autoFocus, + ), + ); + } +} diff --git a/lib/src/widgets/progression_widget.dart b/lib/src/widgets/progression_widget.dart new file mode 100644 index 0000000000..64352d2b10 --- /dev/null +++ b/lib/src/widgets/progression_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; + +const _customOpacity = 0.6; + +class ProgressionWidget extends StatelessWidget { + final int progress; + final double fontSize; + + const ProgressionWidget(this.progress, {this.fontSize = 20}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (progress != 0) ...[ + Icon( + progress > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, + size: fontSize, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, + ), + Text( + progress.abs().toString(), + style: TextStyle( + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, + fontSize: fontSize, + ), + ), + ] else + Text( + '0', + style: TextStyle(color: textShade(context, _customOpacity), fontSize: fontSize), + ), + ], + ); + } +} diff --git a/lib/src/widgets/rating.dart b/lib/src/widgets/rating.dart index 69df2388c9..fae2ea6472 100644 --- a/lib/src/widgets/rating.dart +++ b/lib/src/widgets/rating.dart @@ -19,10 +19,8 @@ class RatingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final ratingStr = - rating is double ? rating.toStringAsFixed(2) : rating.toString(); return Text( - '$ratingStr${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', + '${rating.round()}${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', style: style, ); } diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 2100a27873..b553c49b03 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -43,24 +43,21 @@ class SettingsListTile extends StatelessWidget { leading: icon, title: _SettingsTitle(title: settingsLabel), additionalInfo: showCupertinoTrailingValue ? Text(settingsValue) : null, - subtitle: Theme.of(context).platform == TargetPlatform.android - ? Text( - settingsValue, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : explanation != null + subtitle: + Theme.of(context).platform == TargetPlatform.android + ? Text( + settingsValue, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : explanation != null ? Text(explanation!, maxLines: 5) : null, onTap: onTap, - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : explanation != null - ? _SettingsInfoTooltip( - message: explanation!, - child: const Icon(Icons.info_outline), - ) + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : explanation != null + ? _SettingsInfoTooltip(message: explanation!, child: const Icon(Icons.info_outline)) : null, ), ); @@ -74,6 +71,7 @@ class SwitchSettingTile extends StatelessWidget { required this.value, this.onChanged, this.leading, + this.padding, super.key, }); @@ -82,18 +80,16 @@ class SwitchSettingTile extends StatelessWidget { final bool value; final void Function(bool value)? onChanged; final Widget? leading; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return PlatformListTile( + padding: padding, leading: leading, title: _SettingsTitle(title: title), subtitle: subtitle, - trailing: Switch.adaptive( - value: value, - onChanged: onChanged, - applyCupertinoTheme: true, - ), + trailing: Switch.adaptive(value: value, onChanged: onChanged, applyCupertinoTheme: true), ); } } @@ -127,8 +123,7 @@ class _SliderSettingsTileState extends State { min: 0, max: widget.values.length.toDouble() - 1, divisions: widget.values.length - 1, - label: widget.labelBuilder?.call(widget.values[_index]) ?? - widget.values[_index].toString(), + label: widget.labelBuilder?.call(widget.values[_index]) ?? widget.values[_index].toString(), onChanged: (value) { final newIndex = value.toInt(); setState(() { @@ -143,9 +138,10 @@ class _SliderSettingsTileState extends State { return PlatformListTile( leading: widget.icon, title: slider, - trailing: widget.labelBuilder != null - ? Text(widget.labelBuilder!.call(widget.values[_index])) - : null, + trailing: + widget.labelBuilder != null + ? Text(widget.labelBuilder!.call(widget.values[_index])) + : null, ); } } @@ -159,22 +155,20 @@ class SettingsSectionTitle extends StatelessWidget { Widget build(BuildContext context) { return Text( title, - style: Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: CupertinoColors.secondaryLabel.resolveFrom(context), - ) - : null, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: CupertinoColors.secondaryLabel.resolveFrom(context), + ) + : null, ); } } class _SettingsInfoTooltip extends StatelessWidget { - const _SettingsInfoTooltip({ - required this.message, - required this.child, - }); + const _SettingsInfoTooltip({required this.message, required this.child}); final String message; final Widget child; @@ -200,20 +194,13 @@ class _SettingsTitle extends StatelessWidget { Widget build(BuildContext context) { return DefaultTextStyle.merge( // forces iOS default font size - style: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 17.0, - ) - : null, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 17.0) + : null, maxLines: 2, overflow: TextOverflow.ellipsis, - child: Text.rich( - TextSpan( - children: [ - title.textSpan ?? TextSpan(text: title.data), - ], - ), - ), + child: Text.rich(TextSpan(children: [title.textSpan ?? TextSpan(text: title.data)])), ); } } @@ -268,9 +255,7 @@ class ChoicePicker extends StatelessWidget { title: titleBuilder(value), subtitle: subtitleBuilder?.call(value), leading: leadingBuilder?.call(value), - onTap: onSelectedItemChanged != null - ? () => onSelectedItemChanged!(value) - : null, + onTap: onSelectedItemChanged != null ? () => onSelectedItemChanged!(value) : null, ); }); return Opacity( @@ -278,47 +263,45 @@ class ChoicePicker extends StatelessWidget { child: Column( children: [ if (showDividerBetweenTiles) - ...ListTile.divideTiles( - context: context, - tiles: tiles, - ) + ...ListTile.divideTiles(context: context, tiles: tiles) else ...tiles, ], ), ); case TargetPlatform.iOS: - final tileConstructor = - notchedTile ? CupertinoListTile.notched : CupertinoListTile.new; + final tileConstructor = notchedTile ? CupertinoListTile.notched : CupertinoListTile.new; return Padding( padding: margin ?? Styles.bodySectionPadding, child: Opacity( opacity: onSelectedItemChanged != null ? 1.0 : 0.5, child: CupertinoListSection.insetGrouped( - backgroundColor: - CupertinoTheme.of(context).scaffoldBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, decoration: BoxDecoration( color: Styles.cupertinoCardColor.resolveFrom(context), borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), - separatorColor: - Styles.cupertinoSeparatorColor.resolveFrom(context), + separatorColor: Styles.cupertinoSeparatorColor.resolveFrom(context), margin: EdgeInsets.zero, additionalDividerMargin: notchedTile ? null : 6.0, hasLeading: leadingBuilder != null, - children: choices.map((value) { - return tileConstructor( - trailing: selectedItem == value - ? const Icon(CupertinoIcons.check_mark_circled_solid) - : null, - title: titleBuilder(value), - subtitle: subtitleBuilder?.call(value), - leading: leadingBuilder?.call(value), - onTap: onSelectedItemChanged != null - ? () => onSelectedItemChanged!(value) - : null, - ); - }).toList(growable: false), + children: choices + .map((value) { + return tileConstructor( + trailing: + selectedItem == value + ? const Icon(CupertinoIcons.check_mark_circled_solid) + : null, + title: titleBuilder(value), + subtitle: subtitleBuilder?.call(value), + leading: leadingBuilder?.call(value), + onTap: + onSelectedItemChanged != null + ? () => onSelectedItemChanged!(value) + : null, + ); + }) + .toList(growable: false), ), ), ); diff --git a/lib/src/widgets/shimmer.dart b/lib/src/widgets/shimmer.dart index 045a9dd546..b4e86f3be3 100644 --- a/lib/src/widgets/shimmer.dart +++ b/lib/src/widgets/shimmer.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Shimmer extends StatefulWidget { @@ -6,10 +5,7 @@ class Shimmer extends StatefulWidget { return context.findAncestorStateOfType(); } - const Shimmer({ - super.key, - this.child, - }); + const Shimmer({super.key, this.child}); final Widget? child; @@ -21,50 +17,31 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { late AnimationController _shimmerController; LinearGradient get _defaultGradient { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - final brightness = Theme.of(context).brightness; - switch (brightness) { - case Brightness.light: - return androidLightShimmerGradient; - case Brightness.dark: - return androidDarkShimmerGradient; - } - case TargetPlatform.iOS: - final brightness = - CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light; - switch (brightness) { - case Brightness.light: - return iOSLightShimmerGradient; - case Brightness.dark: - return iOSDarkShimmerGradient; - } - default: - throw 'Unexpected platform $Theme.of(context).platform'; + final brightness = Theme.of(context).brightness; + switch (brightness) { + case Brightness.light: + return lightShimmerGradient; + case Brightness.dark: + return darkShimmerGradient; } } LinearGradient get gradient => LinearGradient( - colors: _defaultGradient.colors, - stops: _defaultGradient.stops, - begin: _defaultGradient.begin, - end: _defaultGradient.end, - transform: - _SlidingGradientTransform(slidePercent: _shimmerController.value), - ); + colors: _defaultGradient.colors, + stops: _defaultGradient.stops, + begin: _defaultGradient.begin, + end: _defaultGradient.end, + transform: _SlidingGradientTransform(slidePercent: _shimmerController.value), + ); Listenable get shimmerChanges => _shimmerController; - bool get isSized => - (context.findRenderObject() as RenderBox?)?.hasSize ?? false; + bool get isSized => (context.findRenderObject() as RenderBox?)?.hasSize ?? false; // ignore: cast_nullable_to_non_nullable Size get size => (context.findRenderObject() as RenderBox).size; - Offset getDescendantOffset({ - required RenderBox descendant, - Offset offset = Offset.zero, - }) { + Offset getDescendantOffset({required RenderBox descendant, Offset offset = Offset.zero}) { // ignore: cast_nullable_to_non_nullable final shimmerBox = context.findRenderObject() as RenderBox; return descendant.localToGlobal(offset, ancestor: shimmerBox); @@ -91,11 +68,7 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { } class ShimmerLoading extends StatefulWidget { - const ShimmerLoading({ - super.key, - required this.isLoading, - required this.child, - }); + const ShimmerLoading({super.key, required this.isLoading, required this.child}); final bool isLoading; final Widget child; @@ -145,10 +118,14 @@ class _ShimmerLoadingState extends State { } final shimmerSize = shimmer.size; final gradient = shimmer.gradient; - final offsetWithinShimmer = shimmer.getDescendantOffset( - // ignore: cast_nullable_to_non_nullable - descendant: context.findRenderObject() as RenderBox, - ); + final renderObject = context.findRenderObject(); + final offsetWithinShimmer = + renderObject != null + ? shimmer.getDescendantOffset( + // ignore: cast_nullable_to_non_nullable + descendant: renderObject as RenderBox, + ) + : Offset.zero; return ShaderMask( blendMode: BlendMode.srcATop, @@ -167,74 +144,24 @@ class _ShimmerLoadingState extends State { } } -const iOSLightShimmerGradient = LinearGradient( - colors: [ - Color(0xFFE3E3E6), - Color(0xFFECECEE), - Color(0xFFE3E3E6), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], - begin: Alignment(-1.0, -0.3), - end: Alignment(1.0, 0.3), - tileMode: TileMode.clamp, -); - -const iOSDarkShimmerGradient = LinearGradient( - colors: [ - Color(0xFF111111), - Color(0xFF1a1a1a), - Color(0xFF111111), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], - begin: Alignment(-1.0, -0.3), - end: Alignment(1.0, 0.3), - tileMode: TileMode.clamp, -); - -const androidLightShimmerGradient = LinearGradient( - colors: [ - Color(0xFFE6E6E6), - Color(0xFFEFEFEF), - Color(0xFFE6E6E6), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], +const lightShimmerGradient = LinearGradient( + colors: [Color(0xFFE3E3E6), Color(0xFFECECEE), Color(0xFFE3E3E6)], + stops: [0.1, 0.3, 0.4], begin: Alignment(-1.0, -0.3), end: Alignment(1.0, 0.3), tileMode: TileMode.clamp, ); -const androidDarkShimmerGradient = LinearGradient( - colors: [ - Color(0xFF333333), - Color(0xFF3c3c3c), - Color(0xFF333333), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], +const darkShimmerGradient = LinearGradient( + colors: [Color(0xFF333333), Color(0xFF3c3c3c), Color(0xFF333333)], + stops: [0.1, 0.3, 0.4], begin: Alignment(-1.0, -0.3), end: Alignment(1.0, 0.3), tileMode: TileMode.clamp, ); class _SlidingGradientTransform extends GradientTransform { - const _SlidingGradientTransform({ - required this.slidePercent, - }); + const _SlidingGradientTransform({required this.slidePercent}); final double slidePercent; diff --git a/lib/src/widgets/stat_card.dart b/lib/src/widgets/stat_card.dart index 04527305c3..92c5e3fb18 100644 --- a/lib/src/widgets/stat_card.dart +++ b/lib/src/widgets/stat_card.dart @@ -33,8 +33,7 @@ class StatCard extends StatelessWidget { fontSize: statFontSize ?? _defaultStatFontSize, ); - final defaultValueStyle = - TextStyle(fontSize: valueFontSize ?? _defaultValueFontSize); + final defaultValueStyle = TextStyle(fontSize: valueFontSize ?? _defaultValueFontSize); return Padding( padding: padding ?? EdgeInsets.zero, @@ -48,18 +47,10 @@ class StatCard extends StatelessWidget { FittedBox( alignment: Alignment.center, fit: BoxFit.scaleDown, - child: Text( - stat, - style: defaultStatStyle, - textAlign: TextAlign.center, - ), + child: Text(stat, style: defaultStatStyle, textAlign: TextAlign.center), ), if (value != null) - Text( - value!, - style: defaultValueStyle, - textAlign: TextAlign.center, - ) + Text(value!, style: defaultValueStyle, textAlign: TextAlign.center) else if (child != null) child! else @@ -83,9 +74,7 @@ class StatCardRow extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.stretch, - children: _divideRow(cards) - .map((e) => Expanded(child: e)) - .toList(growable: false), + children: _divideRow(cards).map((e) => Expanded(child: e)).toList(growable: false), ), ); } @@ -100,14 +89,8 @@ Iterable _divideRow(Iterable elements) { } Widget wrapElement(Widget el) { - return Container( - margin: const EdgeInsets.only(right: 8), - child: el, - ); + return Container(margin: const EdgeInsets.only(right: 8), child: el); } - return [ - ...list.take(list.length - 1).map(wrapElement), - list.last, - ]; + return [...list.take(list.length - 1).map(wrapElement), list.last]; } diff --git a/lib/src/widgets/user_full_name.dart b/lib/src/widgets/user_full_name.dart index d5e494d7fe..f869aeedf0 100644 --- a/lib/src/widgets/user_full_name.dart +++ b/lib/src/widgets/user_full_name.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -/// Displays a user name, title, flair with an optional rating. +/// Displays a user name, title, flair (optional) with an optional rating. class UserFullNameWidget extends ConsumerWidget { const UserFullNameWidget({ required this.user, @@ -17,6 +17,7 @@ class UserFullNameWidget extends ConsumerWidget { this.rating, this.provisional, this.shouldShowOnline = false, + this.showFlair = true, this.style, super.key, }); @@ -27,6 +28,7 @@ class UserFullNameWidget extends ConsumerWidget { this.rating, this.provisional, this.shouldShowOnline = false, + this.showFlair = true, this.style, super.key, }); @@ -44,6 +46,9 @@ class UserFullNameWidget extends ConsumerWidget { /// Whether to show the online status. final bool? shouldShowOnline; + /// Whether to show the user's flair. Defaults to `true`. + final bool showFlair; + @override Widget build(BuildContext context, WidgetRef ref) { final provisionalStr = provisional == true ? '?' : ''; @@ -54,12 +59,10 @@ class UserFullNameWidget extends ConsumerWidget { orElse: () => false, ); - final displayName = user?.name ?? + final displayName = + user?.name ?? (aiLevel != null - ? context.l10n.aiNameLevelAiLevel( - 'Stockfish', - aiLevel.toString(), - ) + ? context.l10n.aiNameLevelAiLevel('Stockfish', aiLevel.toString()) : context.l10n.anonymous); return Row( mainAxisSize: MainAxisSize.min, @@ -69,8 +72,7 @@ class UserFullNameWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5), child: Icon( user?.isOnline == true ? Icons.cloud : Icons.cloud_off, - size: style?.fontSize ?? - DefaultTextStyle.of(context).style.fontSize, + size: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, color: user?.isOnline == true ? context.lichessColors.good : null, ), ), @@ -79,8 +81,7 @@ class UserFullNameWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5), child: Icon( LichessIcons.patron, - size: style?.fontSize ?? - DefaultTextStyle.of(context).style.fontSize, + size: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, color: style?.color ?? DefaultTextStyle.of(context).style.color, semanticLabel: context.l10n.patronLichessPatron, ), @@ -89,37 +90,26 @@ class UserFullNameWidget extends ConsumerWidget { Text( user!.title!, style: (style ?? const TextStyle()).copyWith( - color: user?.title == 'BOT' - ? context.lichessColors.fancy - : context.lichessColors.brag, + color: + user?.title == 'BOT' ? context.lichessColors.fancy : context.lichessColors.brag, fontWeight: user?.title == 'BOT' ? null : FontWeight.bold, ), ), const SizedBox(width: 5), ], Flexible( - child: Text( - displayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: style, - ), + child: Text(displayName, maxLines: 1, overflow: TextOverflow.ellipsis, style: style), ), - if (user?.flair != null) ...[ + if (showFlair && user?.flair != null) ...[ const SizedBox(width: 5), CachedNetworkImage( imageUrl: lichessFlairSrc(user!.flair!), errorWidget: (_, __, ___) => kEmptyWidget, - width: - style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, - height: - style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, + width: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, + height: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, ), ], - if (shouldShowRating && ratingStr != null) ...[ - const SizedBox(width: 5), - Text(ratingStr), - ], + if (shouldShowRating && ratingStr != null) ...[const SizedBox(width: 5), Text(ratingStr)], ], ); } diff --git a/lib/src/widgets/user_list_tile.dart b/lib/src/widgets/user_list_tile.dart index 6e6690eb1c..0346e9288f 100644 --- a/lib/src/widgets/user_list_tile.dart +++ b/lib/src/widgets/user_list_tile.dart @@ -21,11 +21,7 @@ class UserListTile extends StatelessWidget { this.userPerfs, ); - factory UserListTile.fromUser( - User user, - bool isOnline, { - VoidCallback? onTap, - }) { + factory UserListTile.fromUser(User user, bool isOnline, {VoidCallback? onTap}) { return UserListTile._( user.username, user.title, @@ -62,9 +58,7 @@ class UserListTile extends StatelessWidget { Widget build(BuildContext context) { return PlatformListTile( onTap: onTap != null ? () => onTap?.call() : null, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.bodyPadding - : null, + padding: Theme.of(context).platform == TargetPlatform.iOS ? Styles.bodyPadding : null, leading: Icon( isOnline == true ? Icons.cloud : Icons.cloud_off, color: isOnline == true ? context.lichessColors.good : null, @@ -74,29 +68,17 @@ class UserListTile extends StatelessWidget { child: Row( children: [ if (isPatron == true) ...[ - Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - ), + Icon(LichessIcons.patron, semanticLabel: context.l10n.patronLichessPatron), const SizedBox(width: 5), ], if (title != null) ...[ Text( title!, - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), const SizedBox(width: 5), ], - Flexible( - child: Text( - maxLines: 1, - overflow: TextOverflow.ellipsis, - username, - ), - ), + Flexible(child: Text(maxLines: 1, overflow: TextOverflow.ellipsis, username)), if (flair != null) ...[ const SizedBox(width: 5), CachedNetworkImage( @@ -121,31 +103,23 @@ class _UserRating extends StatelessWidget { @override Widget build(BuildContext context) { - List userPerfs = Perf.values.where((element) { - final p = perfs[element]; - return p != null && - p.numberOfGamesOrRuns > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + List userPerfs = Perf.values + .where((element) { + final p = perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); if (userPerfs.isEmpty) return const SizedBox.shrink(); userPerfs.sort( - (p1, p2) => perfs[p1]! - .numberOfGamesOrRuns - .compareTo(perfs[p2]!.numberOfGamesOrRuns), + (p1, p2) => perfs[p1]!.numberOfGamesOrRuns.compareTo(perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); final rating = perfs[userPerfs.first]?.rating.toString() ?? '?'; final icon = userPerfs.first.icon; - return Row( - children: [ - Icon(icon, size: 16), - const SizedBox(width: 5), - Text(rating), - ], - ); + return Row(children: [Icon(icon, size: 16), const SizedBox(width: 5), Text(rating)]); } } diff --git a/lib/src/widgets/yes_no_dialog.dart b/lib/src/widgets/yes_no_dialog.dart index 292815c050..ad5053d0fc 100644 --- a/lib/src/widgets/yes_no_dialog.dart +++ b/lib/src/widgets/yes_no_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; class YesNoDialog extends StatelessWidget { const YesNoDialog({ @@ -20,37 +21,13 @@ class YesNoDialog extends StatelessWidget { @override Widget build(BuildContext context) { - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - title: title, - content: content, - actions: [ - CupertinoDialogAction( - onPressed: onNo, - child: Text(context.l10n.no), - ), - CupertinoDialogAction( - onPressed: onYes, - child: Text(context.l10n.yes), - ), - ], - ); - } else { - return AlertDialog( - title: title, - content: content, - alignment: alignment, - actions: [ - TextButton( - onPressed: onNo, - child: Text(context.l10n.no), - ), - TextButton( - onPressed: onYes, - child: Text(context.l10n.yes), - ), - ], - ); - } + return PlatformAlertDialog( + title: title, + content: content, + actions: [ + PlatformDialogAction(onPressed: onNo, child: Text(context.l10n.no)), + PlatformDialogAction(onPressed: onYes, child: Text(context.l10n.yes)), + ], + ); } } diff --git a/patchs/cupertino_transp_appbar.patch b/patchs/cupertino_transp_appbar.patch deleted file mode 100644 index 8b6ca0d244..0000000000 --- a/patchs/cupertino_transp_appbar.patch +++ /dev/null @@ -1,205 +0,0 @@ -diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart -index 1495f55d2d..a3ef08bdb6 100644 ---- a/packages/flutter/lib/src/cupertino/nav_bar.dart -+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart -@@ -31,6 +31,10 @@ const double _kNavBarLargeTitleHeightExtension = 52.0; - /// from the normal navigation bar to a big title below the navigation bar. - const double _kNavBarShowLargeTitleThreshold = 10.0; - -+/// Number of logical pixels scrolled during which the navigation bar's background -+/// fades in or out. -+const _kNavBarScrollUnderAnimationExtent = 10.0; -+ - const double _kNavBarEdgePadding = 16.0; - - const double _kNavBarBottomPadding = 8.0; -@@ -432,17 +436,81 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer - class _CupertinoNavigationBarState extends State { - late _NavigationBarStaticComponentsKeys keys; - -+ ScrollNotificationObserverState? _scrollNotificationObserver; -+ double _scrollAnimationValue = 0.0; -+ -+ @override -+ void didChangeDependencies() { -+ super.didChangeDependencies(); -+ _scrollNotificationObserver?.removeListener(_handleScrollNotification); -+ _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); -+ _scrollNotificationObserver?.addListener(_handleScrollNotification); -+ } -+ -+ @override -+ void dispose() { -+ if (_scrollNotificationObserver != null) { -+ _scrollNotificationObserver!.removeListener(_handleScrollNotification); -+ _scrollNotificationObserver = null; -+ } -+ super.dispose(); -+ } -+ - @override - void initState() { - super.initState(); - keys = _NavigationBarStaticComponentsKeys(); - } - -+ void _handleScrollNotification(ScrollNotification notification) { -+ if (notification is ScrollUpdateNotification && notification.depth == 0) { -+ final ScrollMetrics metrics = notification.metrics; -+ final oldScrollAnimationValue = _scrollAnimationValue; -+ double scrollExtent = 0.0; -+ switch (metrics.axisDirection) { -+ case AxisDirection.up: -+ // Scroll view is reversed -+ scrollExtent = metrics.extentAfter; -+ case AxisDirection.down: -+ scrollExtent = metrics.extentBefore; -+ case AxisDirection.right: -+ case AxisDirection.left: -+ // Scrolled under is only supported in the vertical axis, and should -+ // not be altered based on horizontal notifications of the same -+ // predicate since it could be a 2D scroller. -+ break; -+ } -+ -+ if (scrollExtent >= 0 && scrollExtent < _kNavBarScrollUnderAnimationExtent) { -+ setState(() { -+ _scrollAnimationValue = clampDouble(scrollExtent / _kNavBarScrollUnderAnimationExtent, 0, 1); -+ }); -+ } else if (scrollExtent > _kNavBarScrollUnderAnimationExtent && oldScrollAnimationValue != 1.0) { -+ setState(() { -+ _scrollAnimationValue = 1.0; -+ }); -+ } else if (scrollExtent <= 0 && oldScrollAnimationValue != 0.0) { -+ setState(() { -+ _scrollAnimationValue = 0.0; -+ }); -+ } -+ } -+ } -+ - @override - Widget build(BuildContext context) { - final Color backgroundColor = - CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor; - -+ final Border? effectiveBorder = Border.lerp( -+ const Border(bottom: BorderSide(width: 0.0, color: Color(0x00000000))), -+ widget.border, -+ _scrollAnimationValue, -+ ); -+ -+ final initialBackgroundColor = CupertinoTheme.of(context).scaffoldBackgroundColor; -+ final Color effectiveBackgroundColor = Color.lerp(initialBackgroundColor, backgroundColor, _scrollAnimationValue)!; -+ - final _NavigationBarStaticComponents components = _NavigationBarStaticComponents( - keys: keys, - route: ModalRoute.of(context), -@@ -458,8 +526,8 @@ class _CupertinoNavigationBarState extends State { - ); - - final Widget navBar = _wrapWithBackground( -- border: widget.border, -- backgroundColor: backgroundColor, -+ border: effectiveBorder, -+ backgroundColor: effectiveBackgroundColor, - brightness: widget.brightness, - child: DefaultTextStyle( - style: CupertinoTheme.of(context).textTheme.textStyle, -@@ -488,11 +556,11 @@ class _CupertinoNavigationBarState extends State { - transitionOnUserGestures: true, - child: _TransitionableNavigationBar( - componentsKeys: keys, -- backgroundColor: backgroundColor, -+ backgroundColor: effectiveBackgroundColor, - backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, - titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, - largeTitleTextStyle: null, -- border: widget.border, -+ border: effectiveBorder, - hasUserMiddle: widget.middle != null, - largeExpanded: false, - child: navBar, -@@ -792,7 +860,13 @@ class _LargeTitleNavigationBarSliverDelegate - - @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { -- final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; -+ final double largeTitleThreshold = maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; -+ final bool showLargeTitle = shrinkOffset < largeTitleThreshold; -+ final double shrinkAnimationValue = clampDouble( -+ (shrinkOffset - largeTitleThreshold - _kNavBarScrollUnderAnimationExtent) / _kNavBarScrollUnderAnimationExtent, -+ 0, -+ 1, -+ ); - - final _PersistentNavigationBar persistentNavigationBar = - _PersistentNavigationBar( -@@ -803,9 +877,21 @@ class _LargeTitleNavigationBarSliverDelegate - middleVisible: alwaysShowMiddle ? null : !showLargeTitle, - ); - -+ final Color effectiveBackgroundColor = Color.lerp( -+ CupertinoTheme.of(context).scaffoldBackgroundColor, -+ CupertinoDynamicColor.resolve(backgroundColor, context), -+ shrinkAnimationValue, -+ )!; -+ -+ final Border? effectiveBorder = border == null ? null : Border.lerp( -+ const Border(bottom: BorderSide(width: 0.0, color: Color(0x00000000))), -+ border, -+ shrinkAnimationValue, -+ ); -+ - final Widget navBar = _wrapWithBackground( -- border: border, -- backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context), -+ border: effectiveBorder, -+ backgroundColor: effectiveBackgroundColor, - brightness: brightness, - child: DefaultTextStyle( - style: CupertinoTheme.of(context).textTheme.textStyle, -@@ -875,11 +961,11 @@ class _LargeTitleNavigationBarSliverDelegate - // needs to wrap the top level RenderBox rather than a RenderSliver. - child: _TransitionableNavigationBar( - componentsKeys: keys, -- backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context), -+ backgroundColor: effectiveBackgroundColor, - backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, - titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, - largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, -- border: border, -+ border: effectiveBorder, - hasUserMiddle: userMiddle != null && (alwaysShowMiddle || !showLargeTitle), - largeExpanded: showLargeTitle, - child: navBar, -@@ -1716,6 +1802,7 @@ class _NavigationBarTransition extends StatelessWidget { - AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { -+ - return _wrapWithBackground( - // Don't update the system status bar color mid-flight. - updateSystemUiOverlay: false, -diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart -index 7eec24d908..4bc5fbc87a 100644 ---- a/packages/flutter/lib/src/cupertino/page_scaffold.dart -+++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart -@@ -165,7 +165,7 @@ class _CupertinoPageScaffoldState extends State { - ); - } - -- return DecoratedBox( -+ final content = DecoratedBox( - decoration: BoxDecoration( - color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) - ?? CupertinoTheme.of(context).scaffoldBackgroundColor, -@@ -198,6 +198,8 @@ class _CupertinoPageScaffoldState extends State { - ], - ), - ); -+ -+ return ScrollNotificationObserver(child: content); - } - } - diff --git a/pubspec.lock b/pubspec.lock index 33ac8c55d2..78045e15d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,31 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa + sha256: eae3133cbb06de9205899b822e3897fc6a8bc278ad4c944b4ce612689369694b url: "https://pub.dev" source: hosted - version: "1.3.40" + version: "1.3.47" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" analyzer_plugin: dependency: transitive description: @@ -42,10 +42,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" app_settings: dependency: "direct main" description: @@ -58,82 +58,90 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.1" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: "direct main" description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "3.0.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -154,26 +162,26 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" characters: dependency: transitive description: @@ -194,10 +202,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "44b2f20c8df56d7f42c5d10c68dc8b79f766db65f9f1b4cca45c4d30579d4e57" + sha256: "5cf1a5bcd95c2c043ebfb1c88775b3d05b3332908ae4bbd528f32a8003129850" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "6.2.3" ci: dependency: transitive description: @@ -210,42 +218,42 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: - dependency: transitive + dependency: "direct main" description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: "3e7d1d9dbae40ae82cbe6c23c518f0c4ffe32764ee9749b9a99d32cbac8734f6" + sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d url: "https://pub.dev" source: hosted - version: "6.0.4" + version: "6.1.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -258,10 +266,10 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cronet_http: dependency: "direct main" description: @@ -282,26 +290,26 @@ packages: dependency: "direct main" description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_http: dependency: "direct main" description: name: cupertino_http - sha256: "7e75c45a27cc13a886ab0a1e4d8570078397057bd612de9d24fe5df0d9387717" + sha256: "3de09415040ad1def7a1b1d52cde3b07d6b29837bf0e4f7b822ca5c805e2d872" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "2.0.1" cupertino_icons: dependency: "direct main" description: @@ -314,42 +322,50 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.7.0" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.7.0" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5" url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.0" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9 + url: "https://pub.dev" + source: hosted + version: "1.0.0+6.11.0" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartchess: dependency: "direct main" description: name: dartchess - sha256: "047ee9973f2546744f3d24eeee8c01d563714d3f10a73b0b0799218dc00c65c1" + sha256: "6ddc173288cb63444edf0d766986c283bb1ae6d8ede2ac1fc9de857f0f84d051" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.9.2" dbus: dependency: transitive description: @@ -362,26 +378,26 @@ packages: dependency: "direct main" description: name: deep_pick - sha256: ba19f93cae54e5d601c0d2c11f873bf0038fc6207cc7ab23115f4a5873bb917f + sha256: "79d96b94a9c9ca2e823f05f72ec1a4efcc5ad5fd3875f46d1a8fe61ebbd9f359" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "93429694c9253d2871b3af80cf11b3cbb5c65660d402ed7bf69854ce4a089f82" + sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "11.2.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" dynamic_color: dependency: "direct main" description: @@ -394,18 +410,18 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: - dependency: transitive + dependency: "direct dev" description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" fast_immutable_collections: dependency: "direct main" description: @@ -418,98 +434,98 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" + sha256: fef81a53ba1ca618def1f8bef4361df07968434e62cb204c1fb90bb880a03da2 url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.8.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" + sha256: b94b217e3ad745e784960603d33d99471621ecca151c99c670869b76e50ad2a6 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e + sha256: "9e69806bb3d905aeec3c1242e0e1475de6ea6d48f456af29d598fb229a2b4e5e" url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.18.2" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "30260e1b8ad1464b41ca4531b44ce63d752daaf2f12c92ca6cdcd82b270abecc" + sha256: e235c8452d5622fc271404592388fde179e4b62c50e777ad3c8c3369296104ed url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.2.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: a75e1826d92ea4e86e4a753c7b5d64b844a362676fa653185f1581c859186d18 + sha256: "4ddadf44ed0a202f3acad053f12c083877940fa8cc1a9f747ae09e1ef4372160" url: "https://pub.dev" source: hosted - version: "3.6.40" + version: "3.7.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "1b0a4f9ecbaf9007771bac152afad738ddfacc4b8431a7591c00829480d99553" + sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf" url: "https://pub.dev" source: hosted - version: "15.0.4" + version: "15.1.6" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: c5a6443e66ae064fe186901d740ee7ce648ca2a6fd0484b8c5e963849ac0fc28 + sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d url: "https://pub.dev" source: hosted - version: "4.5.42" + version: "4.5.49" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "232ef63b986467ae5b5577a09c2502b26e2e2aebab5b85e6c966a5ca9b038b89" + sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f url: "https://pub.dev" source: hosted - version: "3.8.12" + version: "3.9.5" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" fl_chart: dependency: "direct main" description: name: fl_chart - sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" url: "https://pub.dev" source: hosted - version: "0.68.0" + version: "0.69.2" flutter: dependency: "direct main" description: flutter @@ -519,26 +535,26 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: f2696d4cf437f627fa09bc4864afdd8c80273f2e293fde544b18202a627754b1 + sha256: "0aa449d8991f70e7847d55b8bff0890fb41dc62c1d8526337e4073e806813bcb" url: "https://pub.dev" source: hosted - version: "6.0.6" + version: "8.0.3" flutter_appauth_platform_interface: dependency: transitive description: name: flutter_appauth_platform_interface - sha256: "44feaa7058191b5d3cd7c9ff195262725773643121bcada172d49c2ddcff71cb" + sha256: ccf5e1d8c40dd35b297290b33cc1896648b4b92a2ec3f62a436c62a8eef9a9db url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "8.0.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" flutter_displaymode: dependency: "direct main" description: @@ -563,27 +579,59 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + url: "https://pub.dev" + source: hosted + version: "18.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + url: "https://pub.dev" + source: hosted + version: "8.0.0" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" + url: "https://pub.dev" + source: hosted + version: "0.7.4+3" flutter_native_splash: dependency: "direct main" description: name: flutter_native_splash - sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.3" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.1" flutter_secure_storage: dependency: "direct main" description: @@ -635,10 +683,11 @@ packages: flutter_slidable: dependency: "direct main" description: - name: flutter_slidable - sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" - url: "https://pub.dev" - source: hosted + path: "." + ref: "89b8384667d3b6c1c2967a8ff10846bcf0a170c7" + resolved-ref: "89b8384667d3b6c1c2967a8ff10846bcf0a170c7" + url: "https://github.com/veloce/flutter_slidable.git" + source: git version: "3.1.1" flutter_spinkit: dependency: "direct main" @@ -648,14 +697,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.1" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" - url: "https://pub.dev" - source: hosted - version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -718,10 +759,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: "direct main" description: @@ -734,18 +775,18 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.1" http_profile: dependency: transitive description: @@ -758,10 +799,10 @@ packages: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "20842a5ad1555be624c314b0c0cc0566e8ece412f61e859a42efeb6d4101a26c" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.5.0" intl: dependency: "direct main" description: @@ -774,10 +815,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" jni: dependency: transitive description: @@ -806,26 +847,26 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -854,18 +895,26 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" macros: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "7.2.2" matcher: dependency: transitive description: @@ -894,10 +943,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mockito: dependency: transitive description: @@ -915,7 +964,7 @@ packages: source: hosted version: "1.0.4" network_image_mock: - dependency: "direct main" + dependency: "direct dev" description: name: network_image_mock sha256: "855cdd01d42440e0cffee0d6c2370909fc31b3bcba308a59829f24f64be42db7" @@ -930,6 +979,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: c8467f3b067a3867c4876210b2fa65a57d117b5667ac158df5991804b0912673 + url: "https://pub.dev" + source: hosted + version: "3.0.0" octo_image: dependency: transitive description: @@ -942,66 +999,58 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "1.9.1" path_provider: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1038,10 +1087,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1062,18 +1111,26 @@ packages: dependency: "direct main" description: name: popover - sha256: "5cba40e04115cbbf15c35e00767b91e8bf3f769763a34beb2f8a1b9e8b5fc876" + sha256: "0606f3e10f92fc0459f5c52fd917738c29e7552323b28694d50c2d3312d0e1a2" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a url: "https://pub.dev" source: hosted - version: "0.3.0+1" + version: "6.0.1" pub_semver: dependency: "direct main" description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: @@ -1086,10 +1143,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" result_extensions: dependency: "direct main" description: @@ -1102,98 +1159,98 @@ packages: dependency: transitive description: name: riverpod - sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d + sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.6.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" + sha256: "63546d70952015f0981361636bf8f356d9cfd9d7f6f0815e3c07789a41233188" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.3" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 + sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" url: "https://pub.dev" source: hosted - version: "2.3.12" + version: "2.6.3" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.1.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: @@ -1206,34 +1263,34 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" signal_strength_indicator: dependency: "direct main" description: @@ -1246,7 +1303,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" sound_effect: dependency: "direct main" description: @@ -1291,42 +1348,66 @@ packages: dependency: "direct main" description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+6" sqflite_common_ffi: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4+4" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqlite3: dependency: transitive description: name: sqlite3 - sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb + sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" state_notifier: dependency: transitive description: @@ -1339,11 +1420,11 @@ packages: dependency: "direct main" description: path: "." - ref: "0965a99ea143db00ec495eecbd54dfe10acf70ea" - resolved-ref: "0965a99ea143db00ec495eecbd54dfe10acf70ea" + ref: "0b4d8f20f72beb43c853230dd18063bee242487d" + resolved-ref: "0b4d8f20f72beb43c853230dd18063bee242487d" url: "https://github.com/lichess-org/dart-stockfish.git" source: git - version: "1.6.1" + version: "1.6.2" stream_channel: dependency: "direct dev" description: @@ -1356,26 +1437,26 @@ packages: dependency: "direct main" description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1388,34 +1469,34 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" - timeago: - dependency: "direct main" + version: "0.7.3" + timezone: + dependency: transitive description: - name: timeago - sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" + name: timezone + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "0.10.0" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -1428,42 +1509,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.8" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1476,50 +1557,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" + version: "4.5.1" vector_math: dependency: transitive description: @@ -1540,18 +1597,18 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.1" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5" + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.2.8" wakelock_plus_platform_interface: dependency: transitive description: @@ -1564,18 +1621,18 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1596,26 +1653,26 @@ packages: dependency: transitive description: name: win32 - sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.9.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.1.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1633,5 +1690,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0-259.0.dev <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0-209.1.beta <4.0.0" + flutter: ">=3.28.0-0.1.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 8d1440b2f3..11fbb27c78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,45 +2,53 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.9.6+000906 # see README.md for details about versioning +version: 0.13.12+001312 # See README.md for details about versioning environment: - sdk: ">=3.3.0 <4.0.0" + sdk: '^3.7.0-209.1.beta' + # We're using the beta channel for the flutter version + flutter: "3.28.0-0.1.pre" dependencies: app_settings: ^5.1.1 async: ^2.10.0 + auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: ^3.2.0 + chessground: ^6.2.3 + clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 crypto: ^3.0.3 - cupertino_http: ^1.1.0 + cupertino_http: ^2.0.0 cupertino_icons: ^1.0.2 - dartchess: ^0.7.0 + dartchess: ^0.9.0 deep_pick: ^1.0.0 - device_info_plus: ^10.1.0 + device_info_plus: ^11.0.0 dynamic_color: ^1.6.9 fast_immutable_collections: ^10.0.0 firebase_core: ^3.0.0 firebase_crashlytics: ^4.0.0 firebase_messaging: ^15.0.0 - fl_chart: ^0.68.0 + fl_chart: ^0.69.0 flutter: sdk: flutter - flutter_appauth: ^6.0.0 + flutter_appauth: ^8.0.0+1 flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 + flutter_local_notifications: ^18.0.1 flutter_localizations: sdk: flutter + flutter_markdown: ^0.7.3+1 flutter_native_splash: ^2.3.5 flutter_riverpod: ^2.3.4 flutter_secure_storage: ^9.2.0 - flutter_slidable: ^3.0.0 + flutter_slidable: + git: + url: https://github.com/veloce/flutter_slidable.git + ref: 89b8384667d3b6c1c2967a8ff10846bcf0a170c7 flutter_spinkit: ^5.2.0 - flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 http: ^1.1.0 intl: ^0.19.0 @@ -49,14 +57,13 @@ dependencies: logging: ^1.1.0 material_color_utilities: ^0.11.1 meta: ^1.8.0 - network_image_mock: ^2.1.1 package_info_plus: ^8.0.0 path: ^1.8.2 popover: ^0.3.0 pub_semver: ^2.1.4 result_extensions: ^0.1.0 riverpod_annotation: ^2.3.0 - share_plus: ^9.0.0 + share_plus: ^10.0.0 shared_preferences: ^2.1.0 signal_strength_indicator: ^0.4.1 sound_effect: ^0.0.2 @@ -64,9 +71,8 @@ dependencies: stockfish: git: url: https://github.com/lichess-org/dart-stockfish.git - ref: 0965a99ea143db00ec495eecbd54dfe10acf70ea + ref: 0b4d8f20f72beb43c853230dd18063bee242487d stream_transform: ^2.1.0 - timeago: ^3.6.0 url_launcher: ^6.1.9 visibility_detector: ^0.4.0 wakelock_plus: ^1.1.1 @@ -74,13 +80,15 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 - custom_lint: ^0.6.0 + custom_lint: ^0.7.0 + fake_async: ^1.3.1 flutter_test: sdk: flutter freezed: ^2.3.4 json_serializable: ^6.5.4 lint: ^2.0.1 mocktail: ^1.0.0 + network_image_mock: ^2.1.1 riverpod_generator: ^2.1.0 riverpod_lint: ^2.3.3 sqflite_common_ffi: ^2.2.3 @@ -96,6 +104,7 @@ flutter: - assets/chess_openings.db - assets/images/ - assets/images/stockfish/ + - assets/images/fide-fed/ - assets/sounds/futuristic/ - assets/sounds/lisp/ - assets/sounds/nes/ diff --git a/scripts/update-arb-from-crowdin.mjs b/scripts/update-arb-from-crowdin.mjs index 07d0e37ad7..8efec3bb1d 100755 --- a/scripts/update-arb-from-crowdin.mjs +++ b/scripts/update-arb-from-crowdin.mjs @@ -4,7 +4,6 @@ import { readFileSync, createWriteStream, writeFileSync, mkdirSync, existsSync } import { readdir, unlink } from 'node:fs/promises'; import { pipeline } from 'stream' import { promisify } from 'util' -import { exec } from 'child_process' import colors from 'colors/safe.js' import { parseStringPromise } from 'xml2js' import fetch from 'node-fetch' @@ -23,8 +22,6 @@ const lilaTranslationsPath = `${tmpDir}/[lichess-org.lila] master/translation/de const mobileSourcePath = `${__dirname}/../translation/source` const mobileTranslationsPath = `${tmpDir}/[lichess-org.mobile] main/translation/dest` -const unzipMaxBufferSize = 1024 * 1024 * 10 // Set maxbuffer to 10MB to avoid errors when default 1MB used - // selection of lila translation modules to include const modules = [ 'mobile', // mobile is not a module in crowdin, but another source of translations, we'll treat it as a module here for simplicity @@ -43,6 +40,7 @@ const modules = [ 'storm', 'streamer', 'study', + 'timeago', ] // list of keys (per module) to include in the ARB file @@ -52,11 +50,8 @@ const whiteLists = { 'contact': ['contact', 'contactLichess'], 'search': ['search'], 'streamer': ['lichessStreamers'], - 'study': ['start', 'shareAndExport'], - 'broadcast': ['broadcasts', 'liveBroadcasts'], } - // Order of locales with variants matters: the fallback must always be first // eg: 'pt-PT' is before 'pt-BR' // Note that 'en-GB' is omitted here on purpose because it is the locale used in template ARB. @@ -76,10 +71,9 @@ main() // -- async function generateLilaTranslationARBs() { - // Download translations zip from crowdin - const zipFile = createWriteStream(`${tmpDir}/out.zip`) - await downloadTranslationsTo(zipFile) - await unzipTranslations(`${tmpDir}/out.zip`) + // Download zip doesn't work anymore, we need another way to get the translations + // This is tracked here: https://github.com/lichess-org/mobile/issues/945 + // for now we need to manually download the translations and put them in the tmp/translations folder // load all translations into a single object const translations = {} @@ -147,28 +141,6 @@ async function generateTemplateARB() { console.log(colors.green(' Template file successfully written.')) } -async function downloadTranslationsTo(zipFile) { - console.log(colors.blue('Downloading translations...')) - const streamPipeline = promisify(pipeline) - const response = await fetch('https://crowdin.com/backend/download/project/lichess.zip') - if (!response.ok) throw new Error(`unexpected response ${response.statusText}`) - - await streamPipeline(response.body, zipFile) - console.log(colors.green(' Download complete.')) -} - -async function unzipTranslations(zipFilePath) { - console.log(colors.blue('Unzipping translations...')) - return new Promise((resolve, reject) => { - exec(`unzip -o ${zipFilePath} -d ${tmpDir}`, {maxBuffer: unzipMaxBufferSize}, (err) => { - if (err) { - return reject('Unzip failed.') - } - resolve() - }) - }) -} - async function downloadLilaSourcesTo(dir) { console.log(colors.blue('Downloading lila source translations...')) const response = await octokitRequest('GET /repos/{owner}/{repo}/contents/{path}', { diff --git a/test/app_test.dart b/test/app_test.dart new file mode 100644 index 0000000000..916147efbb --- /dev/null +++ b/test/app_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/app.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/home/home_tab_screen.dart'; + +import 'model/auth/fake_session_storage.dart'; +import 'network/fake_http_client_factory.dart'; +import 'test_helpers.dart'; +import 'test_provider_scope.dart'; + +void main() { + testWidgets('App loads', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + + expect(find.byType(MaterialApp), findsOneWidget); + }); + + testWidgets('App loads with system theme, which defaults to light', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + + expect(Theme.of(tester.element(find.byType(MaterialApp))).brightness, Brightness.light); + }); + + testWidgets('App will delete a stored session on startup if one request return 401', ( + tester, + ) async { + int tokenTestRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/token/test') { + tokenTestRequests++; + return mockResponse(''' +{ + "${fakeSession.token}": null +} + ''', 200); + } else if (request.url.path == '/api/account') { + return mockResponse('{"error": "Unauthorized"}', 401); + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(HomeTabScreen), findsOneWidget); + + // wait for the startup requests and animations to complete + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + // should see welcome message + expect( + find.text( + 'Lichess is a free (really), libre, no-ads, open source chess server.', + findRichText: true, + ), + findsOneWidget, + ); + + // should have made a request to test the token + expect(tokenTestRequests, 1); + + // session is not active anymore + expect(find.text('Sign in'), findsOneWidget); + }); + + testWidgets('Bottom navigation', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + + expect(find.byType(BottomNavScaffold), findsOneWidget); + + if (defaultTargetPlatform == TargetPlatform.iOS) { + expect(find.byType(BottomNavigationBarItem), findsNWidgets(5)); + } else { + expect(find.byType(NavigationDestination), findsNWidgets(5)); + } + + expect(find.text('Home'), findsOneWidget); + expect(find.text('Puzzles'), findsOneWidget); + expect(find.text('Tools'), findsOneWidget); + expect(find.text('Watch'), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + }); +} diff --git a/test/binding.dart b/test/binding.dart new file mode 100644 index 0000000000..6462d79266 --- /dev/null +++ b/test/binding.dart @@ -0,0 +1,328 @@ +import 'dart:async'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:logging/logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// The binding instance used in tests. +TestLichessBinding get testBinding => TestLichessBinding.instance; + +/// Lichess binding for testing. +class TestLichessBinding extends LichessBinding { + TestLichessBinding() { + Logger.root.level = Level.FINE; + Logger.root.onRecord.listen((record) { + if (record.level >= Level.WARNING) { + // ignore: avoid_print + print( + '${DateFormat('H:m:s.S').format(record.time)} [${record.level}] ${record.loggerName}: ${record.message}', + ); + } + }); + } + + /// Initialize the binding if necessary, and ensure it is a [TestLichessBinding]. + /// + /// If there is an existing binding but it is not a [TestLichessBinding], + /// this method throws an error. + factory TestLichessBinding.ensureInitialized() { + if (_instance == null) { + TestLichessBinding(); + } + return instance; + } + + /// The single instance of the binding. + static TestLichessBinding get instance => LichessBinding.checkInstance(_instance); + static TestLichessBinding? _instance; + + @override + void initInstance() { + super.initInstance(); + _instance = this; + } + + /// Set the initial values for shared preferences. + Future setInitialSharedPreferencesValues(Map values) async { + for (final entry in values.entries) { + if (entry.value is String) { + await sharedPreferences.setString(entry.key, entry.value as String); + } else if (entry.value is bool) { + await sharedPreferences.setBool(entry.key, entry.value as bool); + } else if (entry.value is double) { + await sharedPreferences.setDouble(entry.key, entry.value as double); + } else if (entry.value is int) { + await sharedPreferences.setInt(entry.key, entry.value as int); + } else if (entry.value is List) { + await sharedPreferences.setStringList(entry.key, entry.value as List); + } else { + throw ArgumentError.value( + entry.value, + 'values', + 'Unsupported value type: ${entry.value.runtimeType}', + ); + } + } + } + + FakeSharedPreferences? _sharedPreferences; + + @override + FakeSharedPreferences get sharedPreferences { + return _sharedPreferences ??= FakeSharedPreferences(); + } + + /// Reset the binding instance. + /// + /// Should be called using [addTearDown] in tests. + void reset() { + _firebaseMessaging = null; + _sharedPreferences = null; + } + + FakeFirebaseMessaging? _firebaseMessaging; + + @override + Future initializeFirebase() async {} + + @override + FakeFirebaseMessaging get firebaseMessaging { + return _firebaseMessaging ??= FakeFirebaseMessaging(); + } + + @override + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler) { + firebaseMessaging.onBackgroundMessage.stream.listen(handler); + } + + @override + Stream get firebaseMessagingOnMessage => firebaseMessaging.onMessage.stream; + + @override + Stream get firebaseMessagingOnMessageOpenedApp => + firebaseMessaging.onMessageOpenedApp.stream; +} + +class FakeSharedPreferences implements SharedPreferencesWithCache { + final Map _values = {}; + + @override + Future remove(String key) async { + _values.remove(key); + return true; + } + + @override + Future clear({Set? allowList}) async { + _values.clear(); + } + + @override + bool containsKey(String key) { + return _values.containsKey(key); + } + + @override + String? getString(String key) { + return _values[key] as String?; + } + + @override + bool? getBool(String key) { + return _values[key] as bool?; + } + + @override + double? getDouble(String key) { + return _values[key] as double?; + } + + @override + int? getInt(String key) { + return _values[key] as int?; + } + + @override + List? getStringList(String key) { + return _values[key] as List?; + } + + @override + Future setString(String key, String value) async { + _values[key] = value; + return true; + } + + @override + Future setBool(String key, bool value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setDouble(String key, double value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setInt(String key, int value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setStringList(String key, List value) { + _values[key] = value; + return Future.value(); + } + + @override + Object? get(String key) { + return _values[key]; + } + + @override + Set get keys => _values.keys.toSet(); + + @override + Future reloadCache() { + return Future.value(); + } +} + +typedef FirebaseMessagingRequestPermissionCall = + ({ + bool alert, + bool announcement, + bool badge, + bool carPlay, + bool criticalAlert, + bool provisional, + bool sound, + }); + +class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { + /// Whether [requestPermission] will grant permission. + bool _willGrantPermission = true; + + /// Set whether [requestPermission] will grant permission. + // ignore: avoid_setters_without_getters + set willGrantPermission(bool value) { + _willGrantPermission = value; + } + + List verifyRequestPermissionCalls() { + final result = _requestPermissionCalls; + _requestPermissionCalls = []; + return result; + } + + List _requestPermissionCalls = []; + + NotificationSettings _notificationSettings = const NotificationSettings( + alert: AppleNotificationSetting.disabled, + announcement: AppleNotificationSetting.disabled, + authorizationStatus: AuthorizationStatus.notDetermined, + badge: AppleNotificationSetting.disabled, + carPlay: AppleNotificationSetting.disabled, + lockScreen: AppleNotificationSetting.disabled, + notificationCenter: AppleNotificationSetting.disabled, + showPreviews: AppleShowPreviewSetting.always, + timeSensitive: AppleNotificationSetting.disabled, + criticalAlert: AppleNotificationSetting.disabled, + sound: AppleNotificationSetting.disabled, + ); + + @override + Future requestPermission({ + bool alert = true, + bool announcement = false, + bool badge = true, + bool carPlay = false, + bool criticalAlert = false, + bool provisional = false, + bool sound = true, + }) async { + _requestPermissionCalls.add(( + alert: alert, + announcement: announcement, + badge: badge, + carPlay: carPlay, + criticalAlert: criticalAlert, + provisional: provisional, + sound: sound, + )); + return _notificationSettings = NotificationSettings( + alert: alert ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + announcement: + announcement ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + authorizationStatus: + _willGrantPermission ? AuthorizationStatus.authorized : AuthorizationStatus.denied, + badge: badge ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + carPlay: carPlay ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + lockScreen: AppleNotificationSetting.enabled, + notificationCenter: AppleNotificationSetting.enabled, + showPreviews: AppleShowPreviewSetting.whenAuthenticated, + timeSensitive: AppleNotificationSetting.disabled, + criticalAlert: + criticalAlert ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + sound: sound ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + ); + } + + @override + Future getNotificationSettings() { + return Future.value(_notificationSettings); + } + + @override + Future getInitialMessage() async { + return null; + } + + // assume the token is initially available + String? _token = 'test-token'; + + void setToken(String token) { + _token = token; + _tokenController.add(token); + } + + final StreamController _tokenController = StreamController.broadcast(); + + @override + Future getToken({String? vapidKey}) async { + assert(vapidKey == null); + return _token; + } + + @override + Future getAPNSToken() { + return Future.value('test-apns-token'); + } + + @override + Stream get onTokenRefresh => _tokenController.stream; + + /// Controller for [onMessage]. + /// + /// Call [StreamController.add] to simulate a message received from FCM while + /// the application is in foreground. + StreamController onMessage = StreamController.broadcast(); + + /// Controller for [onMessageOpenedApp]. + /// + /// Call [StreamController.add] to simulate a user press on a notification message + /// sent by FCM. + StreamController onMessageOpenedApp = StreamController.broadcast(); + + /// Controller for [onBackgroundMessage]. + /// + /// Call [StreamController.add] to simulate a message received from FCM while + /// the application is in background. + StreamController onBackgroundMessage = StreamController.broadcast(); +} diff --git a/test/example_data.dart b/test/example_data.dart new file mode 100644 index 0000000000..51d5b681bc --- /dev/null +++ b/test/example_data.dart @@ -0,0 +1,87 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/material_diff.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; + +List generateArchivedGames({int count = 100, String? username}) { + return List.generate(count, (index) { + final id = GameId('game${index.toString().padLeft(4, '0')}'); + final whitePlayer = Player( + user: + username != null && index.isEven + ? LightUser(id: UserId.fromUserName(username), name: username) + : username != null + ? const LightUser(id: UserId('whiteId'), name: 'White') + : null, + rating: username != null ? 1500 : null, + ); + final blackPlayer = Player( + user: + username != null && index.isOdd + ? LightUser(id: UserId.fromUserName(username), name: username) + : username != null + ? const LightUser(id: UserId('blackId'), name: 'Black') + : null, + rating: username != null ? 1500 : null, + ); + return ArchivedGame( + id: id, + meta: GameMeta( + createdAt: DateTime(2021, 1, 1), + rated: true, + perf: Perf.correspondence, + speed: Speed.correspondence, + variant: Variant.standard, + ), + source: GameSource.lobby, + data: LightArchivedGame( + id: id, + variant: Variant.standard, + lastMoveAt: DateTime(2021, 1, 1), + createdAt: DateTime(2021, 1, 1), + perf: Perf.blitz, + speed: Speed.blitz, + rated: true, + status: GameStatus.started, + white: whitePlayer, + black: blackPlayer, + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), + ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), + status: GameStatus.started, + white: whitePlayer, + black: blackPlayer, + youAre: + username != null + ? index.isEven + ? Side.white + : Side.black + : null, + ); + }); +} + +IList _makeSteps(String pgn) { + Position position = Chess.initial; + final steps = [GameStep(position: position)]; + for (final san in pgn.split(' ')) { + final move = position.parseSan(san)!; + position = position.play(move); + steps.add( + GameStep( + position: position, + sanMove: SanMove(san, move), + diff: MaterialDiff.fromBoard(position.board), + ), + ); + } + return steps.toIList(); +} diff --git a/test/fake_crashlytics.dart b/test/fake_crashlytics.dart index dff3795c76..9821735fc5 100644 --- a/test/fake_crashlytics.dart +++ b/test/fake_crashlytics.dart @@ -57,9 +57,7 @@ class FakeCrashlytics implements FirebaseCrashlytics { }) async {} @override - Future recordFlutterFatalError( - FlutterErrorDetails flutterErrorDetails, - ) async {} + Future recordFlutterFatalError(FlutterErrorDetails flutterErrorDetails) async {} @override Future sendUnsentReports() async {} diff --git a/test/fake_notification_service.dart b/test/fake_notification_service.dart deleted file mode 100644 index 0c4387a2d3..0000000000 --- a/test/fake_notification_service.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; - -class FakeNotificationService implements NotificationService { - @override - Future processDataMessage(RemoteMessage message) async {} - - @override - NotificationServiceRef get ref => throw UnimplementedError(); - - @override - Future registerToken(String token) async {} - - @override - Future registerDevice() async {} - - @override - Future unregister() async {} -} diff --git a/test/mock_server_responses.dart b/test/mock_server_responses.dart new file mode 100644 index 0000000000..cc58a2b913 --- /dev/null +++ b/test/mock_server_responses.dart @@ -0,0 +1,46 @@ +/// Mock server response for /api/account endpoint. +String mockApiAccountResponse(String username) => ''' +{ + "id": "${username.toLowerCase()}", + "username": "$username", + "createdAt": 1290415680000, + "seenAt": 1290415680000, + "title": "GM", + "patron": true, + "perfs": { + "blitz": { + "games": 2340, + "rating": 1681, + "rd": 30, + "prog": 10 + }, + "rapid": { + "games": 2340, + "rating": 1677, + "rd": 30, + "prog": 10 + }, + "classical": { + "games": 2340, + "rating": 1618, + "rd": 30, + "prog": 10 + } + }, + "profile": { + "country": "France", + "location": "Lille", + "bio": "test bio", + "firstName": "John", + "lastName": "Doe", + "fideRating": 1800, + "links": "http://test.com" + } +} +'''; + +String mockUserRecentGameResponse(String username) => ''' +{"id":"Huk88k3D","rated":false,"variant":"fromPosition","speed":"blitz","perf":"blitz","createdAt":1673716450321,"lastMoveAt":1673716450321,"status":"noStart","players":{"white":{"user":{"name":"MightyNanook","id":"mightynanook"},"rating":1116,"provisional":true},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1772}},"initialFen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1","winner":"black","tournament":"ZZQ9tunK","clock":{"initial":300,"increment":0,"totalTime":300},"lastFen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"} +{"id":"g2bzFol8","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553626465,"lastMoveAt":1673553936657,"status":"resign","players":{"white":{"user":{"name":"SchallUndRausch","id":"schallundrausch"},"rating":1751,"ratingDiff":-5},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1767,"ratingDiff":5}},"winner":"black","clock":{"initial":180,"increment":2,"totalTime":260},"lastFen":"r7/pppk4/4p1B1/3pP3/6Pp/q1P1P1nP/P1QK1r2/R5R1 w - - 1 1"} +{"id":"9WLmxmiB","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553299064,"lastMoveAt":1673553615438,"status":"resign","players":{"white":{"user":{"name":"Dr-Alaakour","id":"dr-alaakour"},"rating":1806,"ratingDiff":5},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1772,"ratingDiff":-5}},"winner":"white","clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"2b1Q1k1/p1r4p/1p2p1p1/3pN3/2qP4/P4R2/1P3PPP/4R1K1 b - - 0 1"} +'''; diff --git a/test/model/account/account_repository_test.dart b/test/model/account/account_repository_test.dart index d2bfcc9c55..efb69915ae 100644 --- a/test/model/account/account_repository_test.dart +++ b/test/model/account/account_repository_test.dart @@ -2,10 +2,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('AccountRepository', () { diff --git a/test/model/analysis/fake_opening_service.dart b/test/model/analysis/fake_opening_service.dart new file mode 100644 index 0000000000..ffe1aa6cc1 --- /dev/null +++ b/test/model/analysis/fake_opening_service.dart @@ -0,0 +1,11 @@ +import 'package:dartchess/src/models.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; + +class FakeOpeningService implements OpeningService { + @override + Future fetchFromMoves(Iterable moves) { + // TODO: implement fetchFromMoves when needed + return Future.value(null); + } +} diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index a7603671c0..5dd222ea93 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -6,13 +6,15 @@ import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:mocktail/mocktail.dart'; +import '../../mock_server_responses.dart'; +import '../../network/fake_http_client_factory.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; class MockFlutterAppAuth extends Mock implements FlutterAppAuth {} @@ -22,31 +24,28 @@ class Listener extends Mock { void call(T? previous, T value); } -final client = MockClient((request) { - if (request.url.path == '/api/account') { - return mockResponse(testAccountResponse, 200); - } else if (request.method == 'DELETE' && request.url.path == '/api/token') { - return mockResponse('ok', 200); - } - return mockResponse('', 404); -}); - void main() { final mockSessionStorage = MockSessionStorage(); final mockFlutterAppAuth = MockFlutterAppAuth(); const testUserSession = AuthSessionState( token: 'testToken', - user: LightUser( - id: UserId('test'), - name: 'test', - title: 'GM', - isPatron: true, - ), + user: LightUser(id: UserId('test'), name: 'test', title: 'GM', isPatron: true), ); const loading = AsyncLoading(); const nullData = AsyncData(null); + final client = MockClient((request) { + if (request.url.path == '/api/account') { + return mockResponse(mockApiAccountResponse(testUserSession.user.name), 200); + } else if (request.method == 'DELETE' && request.url.path == '/api/token') { + return mockResponse('ok', 200); + } else if (request.method == 'POST' && request.url.path == '/mobile/unregister') { + return mockResponse('ok', 200); + } + return mockResponse('', 404); + }); + setUpAll(() { registerFallbackValue( AuthorizationTokenRequest( @@ -67,20 +66,17 @@ void main() { group('AuthController', () { test('sign in', () async { - when(() => mockSessionStorage.read()) - .thenAnswer((_) => delayedAnswer(null)); - when(() => mockFlutterAppAuth.authorizeAndExchangeCode(any())) - .thenAnswer((_) => delayedAnswer(signInResponse)); + when(() => mockSessionStorage.read()).thenAnswer((_) => Future.value(null)); when( - () => mockSessionStorage.write(any()), - ).thenAnswer((_) => delayedAnswer(null)); + () => mockFlutterAppAuth.authorizeAndExchangeCode(any()), + ).thenAnswer((_) => Future.value(signInResponse)); + when(() => mockSessionStorage.write(any())).thenAnswer((_) => Future.value(null)); final container = await makeContainer( overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + httpClientFactoryProvider.overrideWith((_) => FakeHttpClientFactory(() => client)), ], ); @@ -106,25 +102,34 @@ void main() { verifyNoMoreInteractions(listener); // it should successfully write the session - verify( - () => mockSessionStorage.write(testUserSession), - ).called(1); + verify(() => mockSessionStorage.write(testUserSession)).called(1); }); test('sign out', () async { - when(() => mockSessionStorage.read()) - .thenAnswer((_) => delayedAnswer(testUserSession)); - when( - () => mockSessionStorage.delete(), - ).thenAnswer((_) => delayedAnswer(null)); + when(() => mockSessionStorage.read()).thenAnswer((_) => Future.value(testUserSession)); + when(() => mockSessionStorage.delete()).thenAnswer((_) => Future.value(null)); + + int tokenDeleteCount = 0; + int unregisterCount = 0; + + final client = MockClient((request) { + if (request.method == 'DELETE' && request.url.path == '/api/token') { + tokenDeleteCount++; + return mockResponse('ok', 200); + } else if (request.method == 'POST' && request.url.path == '/mobile/unregister') { + unregisterCount++; + return mockResponse('ok', 200); + } + return mockResponse('', 404); + }); final container = await makeContainer( overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + httpClientFactoryProvider.overrideWith((_) => FakeHttpClientFactory(() => client)), ], + userSession: testUserSession, ); final listener = Listener>(); @@ -148,54 +153,15 @@ void main() { ]); verifyNoMoreInteractions(listener); + expect(tokenDeleteCount, 1, reason: 'token should be deleted'); + expect(unregisterCount, 1, reason: 'device should be unregistered'); + // session should be deleted - verify( - () => mockSessionStorage.delete(), - ).called(1); + verify(() => mockSessionStorage.delete()).called(1); }); }); } -const testAccountResponse = ''' -{ - "id": "test", - "username": "test", - "createdAt": 1290415680000, - "seenAt": 1290415680000, - "title": "GM", - "patron": true, - "perfs": { - "blitz": { - "games": 2340, - "rating": 1681, - "rd": 30, - "prog": 10 - }, - "rapid": { - "games": 2340, - "rating": 1677, - "rd": 30, - "prog": 10 - }, - "classical": { - "games": 2340, - "rating": 1618, - "rd": 30, - "prog": 10 - } - }, - "profile": { - "country": "France", - "location": "Lille", - "bio": "test bio", - "firstName": "John", - "lastName": "Doe", - "fideRating": 1800, - "links": "http://test.com" - } -} -'''; - final signInResponse = AuthorizationTokenResponse( 'testToken', null, diff --git a/test/model/auth/fake_auth_repository.dart b/test/model/auth/fake_auth_repository.dart index c82b5b48da..4bcbe9eac8 100644 --- a/test/model/auth/fake_auth_repository.dart +++ b/test/model/auth/fake_auth_repository.dart @@ -28,9 +28,4 @@ final fakeUser = User( }), ); -const _fakePerf = UserPerf( - rating: 1500, - ratingDeviation: 0, - progression: 0, - games: 0, -); +const _fakePerf = UserPerf(rating: 1500, ratingDeviation: 0, progression: 0, games: 0); diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index 2cfd055e69..e07216c35e 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -2,11 +2,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('BroadcastRepository', () { @@ -18,7 +18,7 @@ void main() { if (request.url.path == '/api/broadcast/top') { return mockResponse( r''' -{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

\n

Time control is 15 minutes for the entire game with a 10-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

\n

If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

\n

If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

\n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
The player must have reached or reach the required age during the year of competition.
There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

\n

The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

\n

Prize Fund: 10,000 EUR

\n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move one.

\n

GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

\n

Official Website | Standings

\n
\n

El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

\n

El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

\n

Web Oficial | Chess-Results

\n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 12 2024

\n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

Group 1: Boards 1-5
Group 2: Boards 6-10

\n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

\n

The winner will be crowned Belgian Chess Champion 2024.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

\n
    \n
  1. FM Manon, Reja Neer 2445
  2. \n
  3. IM Mohammad Fahad, Rahman 2437
  4. \n
  5. GM Rahman, Ziaur 2423
  6. \n
  7. GM Hossain, Enamul 2365
  8. \n
  9. GM Murshed, Niaz 2317
  10. \n
\n

The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

\n

Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

\n

The top 5 players qualify for the Olympiad team.

\n

Here are useful links:

\n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

\n

Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Photo by Nestoras Argiris on Unsplash

\n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n

Official Website

\n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

\n

Time control is 75 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

\n

Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

\n

Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

\n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"upcoming":[{"tour":{"id":"wXto4wTE","name":"Warsaw Chess Festival 2024 | Najdorf Memorial","slug":"warsaw-chess-festival-2024--najdorf-memorial","info":{"format":"9-round Swiss","tc":"Classical","players":"Kazakouski, Krasenkiw, Markowski, Rozentalis"},"createdAt":1720377119509,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/wXto4wTE","tier":3,"dates":[1720538100000,1721204100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=alefzero:relay:wXto4wTE:rdHCIp4Q.jpg&w=800&sig=8e8dad9d2c0ec3c494b4226173d1c14ca1fd0040","markup":"

The Najdorf Memorial 2024 is a 9-round Swiss, held from the 9th to the 17th of July in Warsaw, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

23rd international open tournament to commemorate GM Miguel (Mieczysław) Najdorf

\n"},"round":{"id":"ENq91IQo","name":"Round 1","slug":"round-1","createdAt":1720377162688,"startsAt":1720538100000,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/round-1/ENq91IQo"},"group":"Warsaw Chess Festival 2024"},{"tour":{"id":"lb23JlWD","name":"2024 Australian University Chess League","slug":"2024-australian-university-chess-league","info":{"format":"6-team round-robin","tc":"Classical"},"createdAt":1718194400018,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/lb23JlWD","tier":3,"dates":[1720775700000,1720934100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:lb23JlWD:vBwmZg4h.jpg&w=800&sig=f6ed72005b1790b9bfc351908acad119b5c369ba","markup":"

The 2024 Australian University Chess League is a 6-team round-robin, held from the 12th to the 14th of July in Sydney, Australia. There are four players per team.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n","teamTable":true},"round":{"id":"yAulT9F2","name":"Round 1","slug":"round-1","createdAt":1718194461669,"startsAt":1720775700000,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/round-1/yAulT9F2"}},{"tour":{"id":"GboZ8j0F","name":"IV Open Internacional Vila del Vendrell","slug":"iv-open-internacional-vila-del-vendrell","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719974387311,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/GboZ8j0F","tier":3,"dates":[1720855800000,1720889100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:GboZ8j0F:BSyQN0Ff.jpg&w=800&sig=a39542ad349425ce3be423a1b5a1d15ef326e1f9","markup":"

The IV Open Internacional Vila del Vendrell is a 9-round Swiss, held on 13th of July in El Vendrell, Tarragona, Catalonia, Spain.

\n

Time control is 15 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"LBvFee4s","name":"Round 1","slug":"round-1","createdAt":1719974485751,"startsAt":1720855800000,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/round-1/LBvFee4s"}},{"tour":{"id":"Es02AbFN","name":"Swiss Individual Championships 2024 | Open","slug":"swiss-individual-championships-2024--open","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1717543242414,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/Es02AbFN","tier":3,"dates":[1720871100000,1721546100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:Es02AbFN:Yz0Im12b.jpg&w=800&sig=8e0f71606055775d5ea2006c6f09f2851872823a","markup":"

The Swiss Individual Championships 2024 | Open is a 10-player round-robin, held from the 13th to the 21st of July in Flims, Switzerland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"vhvpniLr","name":"Round 1","slug":"round-1","createdAt":1717543458521,"startsAt":1720871100000,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/round-1/vhvpniLr"},"group":"Swiss Individual Championships 2024"},{"tour":{"id":"op7Dy2aB","name":"Zadar Chess Festival 2024","slug":"zadar-chess-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720373669999,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/op7Dy2aB","tier":3,"dates":[1721142000000,1721721600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=coach_goran:relay:op7Dy2aB:SVKZikJd.jpg&w=800&sig=70198997a1e6ce8a8f9ecdce566d2c86cf1bcc2b","markup":"

The Zadar Chess Festival 2024 Tournament-A is a 9-round Swiss, held from the 16th to the 23rd of July in Zadar, Croatia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

The Zadar Chess Festival brings chess players from all continents to Zadar. The tournament is divided into two categories, Tournament A and Tournament B. Tournament A is a grandmaster-level event where players compete for master norms, while Tournament B is for amateurs. Attendance is expected to be on par with last year when around 300 players participated. As part of the festival, a blitz championship will also be held, which this year coincides with World Chess Day.

\n

Zadar is a beautiful and extremely popular seaside destination visited by numerous tourists from all over the world. Besides its unique beauty, Zadar is a place of culture and heritage, fantastic gastronomy, a well-known sports city, and a city where everyone can find everything for a perfect summer vacation.

\n

Zadar Tourist Board

\n"},"round":{"id":"Lx0mdzh1","name":"Round 1","slug":"round-1","createdAt":1720437555322,"startsAt":1721142000000,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/round-1/Lx0mdzh1"}},{"tour":{"id":"ilou0biG","name":"3rd Father Cup | Masters","slug":"3rd-father-cup--masters","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1717535598646,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/ilou0biG","tier":3,"dates":[1722169800000,1722666600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ali9fazeli:relay:ilou0biG:ni99wVnt.jpg&w=800&sig=795312938ba49ed18f08120aee2efa1eed924764","markup":"

The 3rd Father Cup | Masters is a 9-round Swiss, held from 28th of July to 3th of August in Rasht, Iran.

\n

Time control is 90 minutes for 40 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Results

\n"},"round":{"id":"jcUBMfjn","name":"Round 1","slug":"round-1","createdAt":1717535725866,"startsAt":1722169800000,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/round-1/jcUBMfjn"},"group":"3rd Father Cup"},{"tour":{"id":"nMDYY8rH","name":"28th Créon International 2024 | Main","slug":"28th-creon-international-2024--main","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720108038852,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/nMDYY8rH","tier":3,"dates":[1722254400000,1722754800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:nMDYY8rH:kljC7xWa.jpg&w=800&sig=b06f8d53b209c0e4eadd232c24700ced2535b02f","markup":"

The 28th Créon International 2024 | Main is a 9-round Swiss, held from the 29th of July to the 4th of August in Créon, France.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"4ONAFGnx","name":"Round 1","slug":"round-1","createdAt":1720108110532,"startsAt":1722254400000,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/round-1/4ONAFGnx"},"group":"28th Créon International 2024"}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

\n

Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

\n

Official Website | Results

\n
\n

Title image photo by Paul Frendach

\n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

\n

SE

\n

Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

\n

Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

\n

Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

\n

EN

\n

Official Website | Video commentary | Results and Pairings | Livechess PGN

\n

Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

\n

Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

\n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

Title image photo by Dario Daniel Silva on Unsplash

\n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Rapido Sub 10 2024

\n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Offiical Website | Standings

\n
\n

Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

\n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
Torredembarra, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Del 29 de juny al 7 de juliol de 2024
ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
(Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

\n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Standings

\n
\n

Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

\n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 10 2024

\n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Board 1-9 Elite
Board 10 - 28 Senior 65+

\n

Official Website | Results

\n
\n

Landsturneringen 2024

\n

Eliteklassen og Senior 65+

\n

Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

\n

Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

\n

Bord 1-9 Eliteklassen
Bord 10 - 28 Senior 65+

\n

Clono partier:
Mikroputt
\nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
Miniputt
https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
Lilleputt
https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
Junior B
https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
Klasse 1
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
Klasse 2
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
Klasse 3
https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
Klasse 4
https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
Klasse 5
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
Klasse Mester
https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

\n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

\n

Time control is 120 minutes for the entire game, plus a 30-second increment per move.

\n

Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

\n

Official Website | Results

\n
\n

In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

\n
\n

Grand Chess Tour | Tour Standings
2024 Superbet Poland Rapid & Blitz
2024 Superbet Romania Chess Classic
2024 Superunited Croatia Rapid & Blitz

\n
\n

Title image photo by Arvid Olson from Pixabay

\n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

\n

Chief Arbiter: IA Gregory Vaserstein

\n

Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

\n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} +{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

\n

Time control is 15 minutes for the entire game with a 10-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

\n

If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

\n

If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

\n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
The player must have reached or reach the required age during the year of competition.
There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

\n

The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

\n

Prize Fund: 10,000 EUR

\n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move one.

\n

GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

\n

Official Website | Standings

\n
\n

El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

\n

El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

\n

Web Oficial | Chess-Results

\n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 12 2024

\n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

Group 1: Boards 1-5
Group 2: Boards 6-10

\n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

\n

The winner will be crowned Belgian Chess Champion 2024.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

\n
    \n
  1. FM Manon, Reja Neer 2445
  2. \n
  3. IM Mohammad Fahad, Rahman 2437
  4. \n
  5. GM Rahman, Ziaur 2423
  6. \n
  7. GM Hossain, Enamul 2365
  8. \n
  9. GM Murshed, Niaz 2317
  10. \n
\n

The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

\n

Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

\n

The top 5 players qualify for the Olympiad team.

\n

Here are useful links:

\n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

\n

Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Photo by Nestoras Argiris on Unsplash

\n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n

Official Website

\n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

\n

Time control is 75 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

\n

Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

\n

Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

\n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

\n

Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

\n

Official Website | Results

\n
\n

Title image photo by Paul Frendach

\n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

\n

SE

\n

Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

\n

Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

\n

Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

\n

EN

\n

Official Website | Video commentary | Results and Pairings | Livechess PGN

\n

Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

\n

Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

\n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

Title image photo by Dario Daniel Silva on Unsplash

\n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Rapido Sub 10 2024

\n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Offiical Website | Standings

\n
\n

Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

\n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
Torredembarra, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Del 29 de juny al 7 de juliol de 2024
ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
(Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

\n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Standings

\n
\n

Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

\n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 10 2024

\n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Board 1-9 Elite
Board 10 - 28 Senior 65+

\n

Official Website | Results

\n
\n

Landsturneringen 2024

\n

Eliteklassen og Senior 65+

\n

Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

\n

Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

\n

Bord 1-9 Eliteklassen
Bord 10 - 28 Senior 65+

\n

Clono partier:
Mikroputt
\nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
Miniputt
https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
Lilleputt
https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
Junior B
https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
Klasse 1
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
Klasse 2
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
Klasse 3
https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
Klasse 4
https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
Klasse 5
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
Klasse Mester
https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

\n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

\n

Time control is 120 minutes for the entire game, plus a 30-second increment per move.

\n

Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

\n

Official Website | Results

\n
\n

In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

\n
\n

Grand Chess Tour | Tour Standings
2024 Superbet Poland Rapid & Blitz
2024 Superbet Romania Chess Classic
2024 Superunited Croatia Rapid & Blitz

\n
\n

Title image photo by Arvid Olson from Pixabay

\n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

\n

Chief Arbiter: IA Gregory Vaserstein

\n

Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

\n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} ''', 200, headers: {'content-type': 'application/json; charset=utf-8'}, @@ -33,9 +33,8 @@ void main() { final response = await repo.getBroadcasts(); - expect(response, isA()); + expect(response, isA()); expect(response.active.isNotEmpty, true); - expect(response.upcoming.isNotEmpty, true); expect(response.past.isNotEmpty, true); }); @@ -60,8 +59,8 @@ void main() { final response = await repo.getRound(const BroadcastRoundId(roundId)); - expect(response, isA()); - expect(response.length, 5); + expect(response, isA()); + expect(response.games.length, 5); }); }); } diff --git a/test/model/challenge/challenge_repository_test.dart b/test/model/challenge/challenge_repository_test.dart index c0f2fa7ed4..21532543c9 100644 --- a/test/model/challenge/challenge_repository_test.dart +++ b/test/model/challenge/challenge_repository_test.dart @@ -3,21 +3,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('ChallengeRepository', () { test('list', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/challenge') { - return mockResponse( - challengesList, - 200, - ); + return mockResponse(challengesList, 200); } return mockResponse('', 404); }); @@ -36,10 +33,7 @@ void main() { test('show', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/challenge/H9fIRZUk/show') { - return mockResponse( - challenge, - 200, - ); + return mockResponse(challenge, 200); } return mockResponse('', 404); }); diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart new file mode 100644 index 0000000000..b882a278e5 --- /dev/null +++ b/test/model/challenge/challenge_service_test.dart @@ -0,0 +1,220 @@ +import 'package:fake_async/fake_async.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../network/fake_websocket_channel.dart'; +import '../../network/socket_test.dart'; +import '../../test_container.dart'; +import '../auth/fake_session_storage.dart'; + +class NotificationDisplayMock extends Mock implements FlutterLocalNotificationsPlugin {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final notificationDisplayMock = NotificationDisplayMock(); + + tearDown(() { + reset(notificationDisplayMock); + }); + + test('exposes a challenges stream', () async { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + await socketClient.connect(); + await socketClient.firstConnection; + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''', + ]); + + await expectLater( + ChallengeService.stream, + emitsInOrder([ + const ( + inward: IListConst([ + Challenge( + socketVersion: 0, + id: ChallengeId('H9fIRZUk'), + status: ChallengeStatus.created, + challenger: ( + user: LightUser(id: UserId('bot1'), name: 'Bot1', title: 'BOT', isOnline: true), + rating: 1500, + provisionalRating: true, + lagRating: 4, + ), + destUser: ( + user: LightUser(id: UserId('bobby'), name: 'Bobby', title: 'GM', isOnline: true), + rating: 1635, + provisionalRating: true, + lagRating: 4, + ), + variant: Variant.standard, + rated: true, + speed: Speed.rapid, + timeControl: ChallengeTimeControlType.clock, + clock: (time: Duration(seconds: 600), increment: Duration.zero), + sideChoice: SideChoice.random, + direction: ChallengeDirection.inward, + ), + ]), + outward: IListConst([]), + ), + ]), + ); + + socketClient.close(); + }); + + test('Listen to socket and show a notification for any new challenge', () async { + when( + () => + notificationDisplayMock.show(any(), any(), any(), any(), payload: any(named: 'payload')), + ).thenAnswer((_) => Future.value()); + + final container = await makeContainer( + userSession: fakeSession, + overrides: [notificationDisplayProvider.overrideWithValue(notificationDisplayMock)], + ); + + final notificationService = container.read(notificationServiceProvider); + final challengeService = container.read(challengeServiceProvider); + + fakeAsync((async) { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + socketClient.connect(); + notificationService.start(); + challengeService.start(); + + // wait for the socket to connect + async.elapse(const Duration(milliseconds: 100)); + async.flushMicrotasks(); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''', + ]); + + async.flushMicrotasks(); + + final result = verify( + () => notificationDisplayMock.show( + const ChallengeId('H9fIRZUk').hashCode, + 'Bot1 challenges you!', + 'Random side • Rated • 10+0', + captureAny(), + payload: any(named: 'payload'), + ), + ); + + expectLater(result.callCount, 1); + expectLater( + result.captured[0], + isA() + .having((details) => details.android?.channelId, 'channelId', 'challenge') + .having((d) => d.android?.importance, 'importance', Importance.max) + .having((d) => d.android?.priority, 'priority', Priority.high), + ); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''', + ]); + + async.flushMicrotasks(); + + // same notification should not be shown again + verifyNever( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ); + + // closing the socket client to be able to flush the timers + socketClient.close(); + async.flushTimers(); + }); + }); + + test('Cancels the notification for any missing challenge', () async { + when( + () => + notificationDisplayMock.show(any(), any(), any(), any(), payload: any(named: 'payload')), + ).thenAnswer((_) => Future.value()); + + when(() => notificationDisplayMock.cancel(any())).thenAnswer((_) => Future.value()); + + final container = await makeContainer( + userSession: fakeSession, + overrides: [notificationDisplayProvider.overrideWithValue(notificationDisplayMock)], + ); + + final notificationService = container.read(notificationServiceProvider); + final challengeService = container.read(challengeServiceProvider); + + fakeAsync((async) { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + socketClient.connect(); + notificationService.start(); + challengeService.start(); + + // wait for the socket to connect + async.elapse(const Duration(milliseconds: 100)); + async.flushMicrotasks(); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''', + ]); + + async.flushMicrotasks(); + + verify( + () => notificationDisplayMock.show( + any(), + any(), + any(), + captureAny(), + payload: any(named: 'payload'), + ), + ); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [] }, "v": 0 } +''', + ]); + + async.flushMicrotasks(); + + verify( + () => notificationDisplayMock.cancel(const ChallengeId('H9fIRZUk').hashCode), + ).called(1); + + // closing the socket client to be able to flush the timers + socketClient.close(); + async.flushTimers(); + }); + }); +} diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart new file mode 100644 index 0000000000..f0c2a45870 --- /dev/null +++ b/test/model/clock/chess_clock_test.dart @@ -0,0 +1,231 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; + +void main() { + test('make clock', () { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + expect(clock.isRunning, false); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + + test('start clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + }); + }); + + test('clock ticking', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('stop clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.stop(); + expect(clock.isRunning, false); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('start side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, null); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start side (running clock)', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start side (running clock, same side)', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.startSide(Side.white); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('start with delay', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(delay: const Duration(milliseconds: 20)); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + // the start delay is reached, but clock not updated yet since tick delay is 100ms + async.elapse(const Duration(milliseconds: 100)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(milliseconds: 4900)); + final thinkTime = clock.stop(); + expect(thinkTime, const Duration(milliseconds: 100)); + }); + }); + + test('increment times', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(whiteInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(blackInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('increment specific side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.white, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.black, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('flag', () { + fakeAsync((async) { + int flagCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + onFlag: () { + flagCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 1); + expect(clock.whiteTime.value, Duration.zero); + expect(clock.blackTime.value, const Duration(seconds: 5)); + + // continue ticking and calling onFlag + async.elapse(const Duration(milliseconds: 200)); + expect(flagCount, 3); + clock.stop(); + + // no more onFlag calls + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 3); + }); + }); + + test('onEmergency', () { + fakeAsync((async) { + int onEmergencyCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + emergencyThreshold: const Duration(seconds: 2), + onEmergency: (_) { + onEmergencyCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 2)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(onEmergencyCount, 1); + async.elapse(const Duration(milliseconds: 100)); + expect(onEmergencyCount, 1); + }); + }); +} diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index f0ee966732..c8897014f4 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,16 +14,13 @@ void main() { expect(root.position, equals(Chess.initial)); expect(root.children.length, equals(1)); final child = root.children.first; - expect(child.id, equals(UciCharPair.fromMove(Move.fromUci('e2e4')!))); - expect(child.sanMove, equals(SanMove('e4', Move.fromUci('e2e4')!))); + expect(child.id, equals(UciCharPair.fromMove(Move.parse('e2e4')!))); + expect(child.sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); expect( child.position.fen, equals('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'), ); - expect( - child.position, - equals(Chess.initial.playUnchecked(Move.fromUci('e2e4')!)), - ); + expect(child.position, equals(Chess.initial.playUnchecked(Move.parse('e2e4')!))); }); test('Root.fromPgnGame, flat', () { @@ -39,8 +37,7 @@ void main() { expect(root.position, equals(Chess.initial)); expect(root.children.length, equals(1)); expect(root.mainline.length, equals(5)); - final nodeWithVariation = - root.nodeAt(UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5'])); + final nodeWithVariation = root.nodeAt(UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5'])); expect(nodeWithVariation.children.length, 2); expect(nodeWithVariation.children[1].sanMove.san, equals('Nf6')); expect(nodeWithVariation.children[1].children.length, 2); @@ -52,10 +49,7 @@ void main() { final nodeList = root.nodesOn(path).toList(); expect(nodeList.length, equals(2)); expect(nodeList[0], equals(root)); - expect( - nodeList[1], - equals(root.nodeAt(path) as Branch), - ); + expect(nodeList[1], equals(root.nodeAt(path) as Branch)); }); test('branchesOn, simple', () { @@ -63,29 +57,21 @@ void main() { final path = UciPath.fromId(UciCharPair.fromUci('e2e4')); final nodeList = root.branchesOn(path); expect(nodeList.length, equals(1)); - expect( - nodeList.first, - equals(root.nodeAt(path) as Branch), - ); + expect(nodeList.first, equals(root.nodeAt(path) as Branch)); }); test('branchesOn, with variation', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - final move = Move.fromUci('b1c3')!; + final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( - UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ), + UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock), move, ); final newNode = root.nodeAt(newPath!); // mainline has not changed expect(root.mainline.length, equals(3)); - expect( - root.mainline.last, - equals(root.nodeAt(root.mainlinePath) as Branch), - ); + expect(root.mainline.last, equals(root.nodeAt(root.mainlinePath) as Branch)); final nodeList = root.branchesOn(newPath); expect(nodeList.length, equals(3)); @@ -98,8 +84,8 @@ void main() { expect(mainline.length, equals(2)); final list = mainline.toList(); - expect(list[0].sanMove, equals(SanMove('e4', Move.fromUci('e2e4')!))); - expect(list[1].sanMove, equals(SanMove('e5', Move.fromUci('e7e5')!))); + expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); + expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); }); test('isOnMainline', () { @@ -107,11 +93,9 @@ void main() { final path = UciPath.fromId(UciCharPair.fromUci('e2e4')); expect(root.isOnMainline(path), isTrue); - final move = Move.fromUci('b1c3')!; + final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( - UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ), + UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock), move, ); @@ -119,13 +103,8 @@ void main() { }); test('add child', () { - final root = Root( - position: Chess.initial, - ); - final child = Branch( - sanMove: SanMove('e4', Move.fromUci('e2e4')!), - position: Chess.initial, - ); + final root = Root(position: Chess.initial); + final child = Branch(sanMove: SanMove('e4', Move.parse('e2e4')!), position: Chess.initial); root.addChild(child); expect(root.children.length, equals(1)); expect(root.children.first, equals(child)); @@ -133,10 +112,7 @@ void main() { test('prepend child', () { final root = Root.fromPgnMoves('e4 e5'); - final child = Branch( - sanMove: SanMove('d4', Move.fromUci('d2d4')!), - position: Chess.initial, - ); + final child = Branch(sanMove: SanMove('d4', Move.parse('d2d4')!), position: Chess.initial); root.prependChild(child); expect(root.children.length, equals(2)); expect(root.children.first, equals(child)); @@ -151,12 +127,10 @@ void main() { test('nodeAtOrNull', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = - root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('e2e4'))); + final branch = root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(branch, equals(root.children.first)); - final branch2 = - root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('b1c3'))); + final branch2 = root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('b1c3'))); expect(branch2, isNull); }); @@ -169,32 +143,22 @@ void main() { test('branchAt from branch', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); final branch = root.branchAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); - final branch2 = - branch!.branchAt(UciPath.fromId(UciCharPair.fromUci('e7e5'))); + final branch2 = branch!.branchAt(UciPath.fromId(UciCharPair.fromUci('e7e5'))); expect(branch2, equals(branch.children.first)); }); test('updateAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), - position: Chess.initial, - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); final fromPath = UciPath.fromId(UciCharPair.fromUci('e2e4')); final (nodePath, _) = root.addNodeAt(fromPath, branch); - expect( - root.branchesOn(nodePath!), - equals([ - root.children.first, - branch, - ]), - ); + expect(root.branchesOn(nodePath!), equals([root.children.first, branch])); final eval = ClientEval( position: branch.position, - maxDepth: 20, + searchTime: const Duration(seconds: 10), cp: 100, depth: 10, nodes: 1000, @@ -208,26 +172,17 @@ void main() { node.eval = eval; }); - expect( - root.branchesOn(nodePath), - equals([ - root.children.first, - newNode!, - ]), - ); + expect(root.branchesOn(nodePath), equals([root.children.first, newNode!])); }); test('updateAll', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - expect( - root.mainline.map((n) => n.eval), - equals([null, null, null]), - ); + expect(root.mainline.map((n) => n.eval), equals([null, null, null])); final eval = ClientEval( position: root.position, - maxDepth: 20, + searchTime: const Duration(seconds: 10), cp: 100, depth: 10, nodes: 1000, @@ -241,28 +196,20 @@ void main() { node.eval = eval; }); - expect( - root.mainline.map((n) => n.eval), - equals([eval, eval, eval]), - ); + expect(root.mainline.map((n) => n.eval), equals([eval, eval, eval])); }); test('addNodeAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), - position: Chess.initial, + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + final (newPath, isNewNode) = root.addNodeAt( + UciPath.fromId(UciCharPair.fromUci('e2e4')), + branch, ); - final (newPath, isNewNode) = - root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch); expect( newPath, - equals( - UciPath.fromIds( - IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('b8c6')]), - ), - ), + equals(UciPath.fromIds(IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('b8c6')]))), ); expect(isNewNode, isTrue); @@ -274,15 +221,8 @@ void main() { test('addNodeAt, prepend', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), - position: Chess.initial, - ); - root.addNodeAt( - UciPath.fromId(UciCharPair.fromUci('e2e4')), - branch, - prepend: true, - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch, prepend: true); final testNode = root.nodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(testNode.children.length, equals(2)); @@ -291,20 +231,15 @@ void main() { test('addNodeAt, with an existing node at path', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('e5', Move.fromUci('e7e5')!), - position: Chess.initial, + final branch = Branch(sanMove: SanMove('e5', Move.parse('e7e5')!), position: Chess.initial); + final (newPath, isNewNode) = root.addNodeAt( + UciPath.fromId(UciCharPair.fromUci('e2e4')), + branch, ); - final (newPath, isNewNode) = - root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch); expect( newPath, - equals( - UciPath.fromIds( - IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]), - ), - ), + equals(UciPath.fromIds(IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]))), ); expect(isNewNode, isFalse); @@ -318,18 +253,9 @@ void main() { test('addNodesAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), - position: Chess.initial, - ); - final branch2 = Branch( - sanMove: SanMove('Na6', Move.fromUci('b8a6')!), - position: Chess.initial, - ); - root.addNodesAt( - UciPath.fromId(UciCharPair.fromUci('e2e4')), - [branch, branch2], - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + final branch2 = Branch(sanMove: SanMove('Na6', Move.parse('b8a6')!), position: Chess.initial); + root.addNodesAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), [branch, branch2]); final testNode = root.nodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(testNode.children.length, equals(2)); @@ -339,24 +265,17 @@ void main() { test('addMoveAt', () { final root = Root.fromPgnMoves('e4 e5'); - final move = Move.fromUci('b1c3')!; - final path = UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ); + final move = Move.parse('b1c3')!; + final path = UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock); final currentPath = root.mainlinePath; final (newPath, _) = root.addMoveAt(path, move); - expect( - newPath, - equals(currentPath + UciCharPair.fromMove(move)), - ); + expect(newPath, equals(currentPath + UciCharPair.fromMove(move))); final newNode = root.branchAt(newPath!); expect(newNode?.position.ply, equals(3)); expect(newNode?.sanMove, equals(SanMove('Nc3', move))); expect( newNode?.position.fen, - equals( - 'rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2', - ), + equals('rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2'), ); final testNode = root.nodeAt(path); @@ -364,17 +283,13 @@ void main() { expect(testNode.children.first.sanMove, equals(SanMove('Nc3', move))); expect( testNode.children.first.position.fen, - equals( - 'rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2', - ), + equals('rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2'), ); }); test('deleteAt', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - final path = UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')], - ); + final path = UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]); root.deleteAt(path); expect(root.mainline.length, equals(1)); expect(root.mainline.last, equals(root.children.first)); @@ -395,44 +310,27 @@ void main() { root.promoteAt(path, toMainline: false); expect( root.mainline.map((n) => n.sanMove.san).toList(), - equals([ - 'e4', - 'd5', - 'exd5', - 'Nf6', - 'c4', - ]), + equals(['e4', 'd5', 'exd5', 'Nf6', 'c4']), ); expect( root.makePgn(), - equals( - '1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. c4 ( 3. Nc3 ) *\n', - ), + equals('1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. c4 ( 3. Nc3 ) *\n'), ); }); test('promoteAt, to mainline', () { const pgn = '1. e4 d5 2. exd5 Qxd5 (2... Nf6 3. c4 (3. Nc3)) 3. Nc3'; final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); - final path = - UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5', 'g8f6', 'b1c3']); + final path = UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5', 'g8f6', 'b1c3']); expect(root.nodeAt(path), isNotNull); root.promoteAt(path, toMainline: true); expect( root.mainline.map((n) => n.sanMove.san).toList(), - equals([ - 'e4', - 'd5', - 'exd5', - 'Nf6', - 'Nc3', - ]), + equals(['e4', 'd5', 'exd5', 'Nf6', 'Nc3']), ); expect( root.makePgn(), - equals( - '1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. Nc3 ( 3. c4 ) *\n', - ), + equals('1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. Nc3 ( 3. c4 ) *\n'), ); }); @@ -442,29 +340,115 @@ void main() { final path = UciPath.fromUciMoves(['d2d4']); expect(root.nodeAt(path), isNotNull); root.promoteAt(path, toMainline: false); - expect( - root.mainline.map((n) => n.sanMove.san).toList(), - equals(['d4']), - ); - expect( - root.makePgn(), - equals( - '1. d4 ( 1. e4 ) *\n', - ), - ); + expect(root.mainline.map((n) => n.sanMove.san).toList(), equals(['d4'])); + expect(root.makePgn(), equals('1. d4 ( 1. e4 ) *\n')); + }); + + group('merge', () { + test('add moves', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } * +'''; + + const pgn2 = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } * +'''; + + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(48)); + + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(50)); + + root2.merge(root); + expect(root2.mainline.length, equals(50)); + expect(root2.mainline.last.sanMove.san, equals('Rc2')); + + for (final nodes in IterableZip([root.mainline, root2.mainline])) { + final [node1, node2] = nodes; + expect(node1.sanMove, equals(node2.sanMove)); + expect(node1.position.fen, equals(node2.position.fen)); + expect(node1.clock, equals(node2.clock)); + } + }); + + test('preserve variations', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } ( 26... Qe7 27. Rc1 ) * +'''; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(52)); + + const pgn2 = ''' + 1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * + '''; + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(59)); + + root2.merge(root); + expect(root2.mainline.length, equals(59)); + expect(root2.makePgn(), ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } ( 26... Qe7 27. Rc1 ) 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * +'''); + }); + + test('preserve evals', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * + '''; + + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(59)); + final clientEval = ClientEval( + position: Chess.initial, + depth: 22, + nodes: 100000, + pvs: IList(), + millis: 1230900, + searchTime: const Duration(milliseconds: 1230900), + cp: 23, + ); + root.mainline.last.eval = clientEval; + + const pgn2 = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } Bd7 { [%clk 0:32:05] } 31. fxe4 { [%clk 0:29:38] } R8c4 { [%clk 0:31:11] } 32. Rxa7 { [%clk 0:27:46] } Bc6 { [%clk 0:30:37] } 33. Bxf5 { [%clk 0:27:20] } Re2 { [%clk 0:29:32] } 34. b5 { [%clk 0:26:56] } Rb4+ { [%clk 0:29:02] } 35. Ka1 { [%clk 0:25:59] } Rxb5 { [%clk 0:28:13] } 36. Rb1 { [%clk 0:25:17] } Rc5 { [%clk 0:27:47] } 37. h5 { [%clk 0:23:42] } Rh2 { [%clk 0:27:22] } 38. g4 { [%clk 0:22:55] } Kf6 { [%clk 0:26:59] } 39. Ra3 { [%clk 0:22:10] } Rc4 { [%clk 0:26:36] } 40. Re1 { [%eval 1.17,33] [%clk 0:19:30] } * +'''; + + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(79)); + + root2.merge(root); + expect(root2.mainline.length, equals(79)); + + for (final nodes in IterableZip([root.mainline, root2.mainline])) { + final [node1, node2] = nodes; + expect(node1.sanMove, equals(node2.sanMove)); + expect(node1.position.fen, equals(node2.position.fen)); + expect(node1.clock, equals(node2.clock)); + } + // one new external eval + expect(root2.mainline.where((n) => n.externalEval != null).length, equals(1)); + // one old client eval preseved + expect(root2.mainline.where((node) => node.eval != null).length, equals(1)); + expect( + root2.mainline.firstWhereOrNull((node) => node.eval != null)?.eval, + equals(clientEval), + ); + }); }); + group('convert alternative castling move', () { void makeTestAltCastlingMove(String pgn, String alt1, String alt2) { final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); final initialPath = root.mainlinePath; final initialPng = root.makePgn(); - final move = Move.fromUci(alt1); + final move = Move.parse(alt1); expect(move, isNotNull); final newMove = root.convertAltCastlingMove(move!); expect(newMove, isNotNull); - expect(newMove, Move.fromUci(alt2)); + expect(newMove, Move.parse(alt2)); expect(root.mainline.last.sanMove.move, newMove); final previousUciPath = root.mainlinePath.penultimate; @@ -475,19 +459,11 @@ void main() { } test('e1g1 -> e1h1', () { - makeTestAltCastlingMove( - '1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O', - 'e1g1', - 'e1h1', - ); + makeTestAltCastlingMove('1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O', 'e1g1', 'e1h1'); }); test('e8g8 -> e8h8', () { - makeTestAltCastlingMove( - '1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O O-O', - 'e8g8', - 'e8h8', - ); + makeTestAltCastlingMove('1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O O-O', 'e8g8', 'e8h8'); }); test('e1c1 -> e1a1', () { @@ -505,16 +481,53 @@ void main() { 'e8a8', ); }); + test('only convert king moves in altCastlingMove', () { + const pgn = '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + final initialPng = root.makePgn(); + final previousUciPath = root.mainlinePath.penultimate; + final move = Move.parse('e1g1'); + root.addMoveAt(previousUciPath, move!); + expect(root.makePgn(), isNot(initialPng)); + }); + + test('do not convert castling move if rook is on the alternative castling square', () { + const pgn = '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + final initialPng = root.makePgn(); + final previousUciPath = root.mainlinePath.penultimate; + final move = Move.parse('e1c1'); + root.addMoveAt(previousUciPath, move!); + expect(root.makePgn(), isNot(initialPng)); + expect(root.mainline.last.sanMove.move, move); + }); }); - test('only convert king moves in altCastlingMove', () { - const pgn = - '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; + }); + + group('ViewNode', () { + test('mainline', () { + const pgn = '1. e4 e5 (1... d5 2. a4) 2. a4'; final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); - final initialPng = root.makePgn(); - final previousUciPath = root.mainlinePath.penultimate; - final move = Move.fromUci('e1g1'); - root.addMoveAt(previousUciPath, move!); - expect(root.makePgn(), isNot(initialPng)); + final viewRoot = root.view; + + { + final mainline = viewRoot.mainline; + + expect(mainline.length, equals(3)); + final list = mainline.toList(); + expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); + expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + expect(list[2].sanMove, equals(SanMove('a4', Move.parse('a2a4')!))); + } + + { + final childMainline = viewRoot.children.first.mainline; + + expect(childMainline.length, equals(2)); + final list = childMainline.toList(); + expect(list[0].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + expect(list[1].sanMove, equals(SanMove('a4', Move.parse('a2a4')!))); + } }); }); } diff --git a/test/model/common/service/fake_sound_service.dart b/test/model/common/service/fake_sound_service.dart index 3e765d133d..d6b8ccafe0 100644 --- a/test/model/common/service/fake_sound_service.dart +++ b/test/model/common/service/fake_sound_service.dart @@ -1,18 +1,12 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; class FakeSoundService implements SoundService { @override Future play(Sound sound) async {} @override - Future changeTheme( - SoundTheme theme, { - bool playSound = false, - }) async {} - - @override - Future initialize(SoundTheme theme) async {} + Future changeTheme(SoundTheme theme, {bool playSound = false}) async {} @override Future release() async {} diff --git a/test/model/common/time_increment_test.dart b/test/model/common/time_increment_test.dart index 84839b3904..022c615a08 100644 --- a/test/model/common/time_increment_test.dart +++ b/test/model/common/time_increment_test.dart @@ -30,10 +30,7 @@ void main() { }); test('Estimated Duration', () { - expect( - const TimeIncrement(300, 5).estimatedDuration, - const Duration(seconds: 300 + 5 * 40), - ); + expect(const TimeIncrement(300, 5).estimatedDuration, const Duration(seconds: 300 + 5 * 40)); expect(const TimeIncrement(0, 0).estimatedDuration, Duration.zero); }); diff --git a/test/model/common/uci_test.dart b/test/model/common/uci_test.dart index 1fb157bbd4..3ee7d7bd8e 100644 --- a/test/model/common/uci_test.dart +++ b/test/model/common/uci_test.dart @@ -5,18 +5,18 @@ import 'package:lichess_mobile/src/model/common/uci.dart'; void main() { test('UciCharPair', () { // regular moves - expect(UciCharPair.fromMove(Move.fromUci('a1b1')!).toString(), '#\$'); - expect(UciCharPair.fromMove(Move.fromUci('a1a2')!).toString(), '#+'); - expect(UciCharPair.fromMove(Move.fromUci('h7h8')!).toString(), 'Zb'); + expect(UciCharPair.fromMove(Move.parse('a1b1')!).toString(), '#\$'); + expect(UciCharPair.fromMove(Move.parse('a1a2')!).toString(), '#+'); + expect(UciCharPair.fromMove(Move.parse('h7h8')!).toString(), 'Zb'); // promotions - expect(UciCharPair.fromMove(Move.fromUci('b7b8q')!).toString(), 'Td'); - expect(UciCharPair.fromMove(Move.fromUci('b7c8q')!).toString(), 'Te'); - expect(UciCharPair.fromMove(Move.fromUci('b7c8n')!).toString(), 'T}'); + expect(UciCharPair.fromMove(Move.parse('b7b8q')!).toString(), 'Td'); + expect(UciCharPair.fromMove(Move.parse('b7c8q')!).toString(), 'Te'); + expect(UciCharPair.fromMove(Move.parse('b7c8n')!).toString(), 'T}'); // drops - expect(UciCharPair.fromMove(Move.fromUci('P@a1')!).toString(), '#\x8f'); - expect(UciCharPair.fromMove(Move.fromUci('Q@h8')!).toString(), 'b\x8b'); + expect(UciCharPair.fromMove(Move.parse('P@a1')!).toString(), '#\x8f'); + expect(UciCharPair.fromMove(Move.parse('Q@h8')!).toString(), 'b\x8b'); }); group('UciPath', () { diff --git a/test/model/correspondence/correspondence_game_storage_test.dart b/test/model/correspondence/correspondence_game_storage_test.dart index 400e843d62..9e5d10a527 100644 --- a/test/model/correspondence/correspondence_game_storage_test.dart +++ b/test/model/correspondence/correspondence_game_storage_test.dart @@ -1,7 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -13,36 +12,18 @@ import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('CorrespondenceGameStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); - final storage = container.read(correspondenceGameStorageProvider); + final storage = await container.read(correspondenceGameStorageProvider.future); await storage.save(corresGame); - expect( - storage.fetch( - gameId: gameId, - ), - completion(equals(corresGame)), - ); + expect(storage.fetch(gameId: gameId), completion(equals(corresGame))); }); }); } @@ -75,9 +56,7 @@ final corresGame = OfflineCorrespondenceGame( variant: Variant.standard, ), fullId: const GameFullId('g2bzFol8fgty'), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', - ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), clock: const CorrespondenceClockData( white: Duration(days: 2, hours: 23, minutes: 59), black: Duration(days: 3), @@ -87,20 +66,8 @@ final corresGame = OfflineCorrespondenceGame( variant: Variant.standard, speed: Speed.correspondence, perf: Perf.classical, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, daysPerTurn: 3, ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 1d5311e424..5db4f3c474 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -2,13 +2,13 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('GameRepository.getRecentGames', () { @@ -46,11 +46,7 @@ void main() { {"id":"9WLmxmiB","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553299064,"lastMoveAt":1673553615438,"status":"resign","players":{"white":{"user":{"name":"Dr-Alaakour","id":"dr-alaakour"},"rating":1806,"ratingDiff":5},"black":{"user":{"name":"Thibault","patron":true,"id":"thibault"},"rating":1772,"ratingDiff":-5}},"winner":"white","clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"2b1Q1k1/p1r4p/1p2p1p1/3pN3/2qP4/P4R2/1P3PPP/4R1K1 b - - 0 1"} '''; - final ids = ISet(const { - GameId('Huk88k3D'), - GameId('g2bzFol8'), - GameId('9WLmxmiB'), - }); + final ids = ISet(const {GameId('Huk88k3D'), GameId('g2bzFol8'), GameId('9WLmxmiB')}); final mockClient = MockClient((request) { if (request.url.path == '/api/games/export/_ids') { @@ -73,7 +69,7 @@ void main() { group('GameRepository.getGame', () { test('game with clocks', () async { const testResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"division":{"middle":18,"end":42}} +{"id":"qVChCOTc","rated":false,"source":"lobby","variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"division":{"middle":18,"end":42}} '''; final mockClient = MockClient((request) { @@ -96,7 +92,7 @@ void main() { test('threeCheck game', () async { const testResponse = ''' -{"id":"1vdsvmxp","rated":true,"variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60},"division":{"middle":18,"end":42}} +{"id":"1vdsvmxp","rated":true,"source":"lobby","variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60},"division":{"middle":18,"end":42}} '''; final mockClient = MockClient((request) { diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 12d4ee32a4..a61db74930 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -14,19 +14,14 @@ void main() { final fullEvent = GameFullEvent.fromJson(json); final game = fullEvent.game; expect(game.id, const GameId('nV3DaALy')); - expect( - game.clock, - const PlayableClockData( - running: true, - white: Duration(seconds: 149, milliseconds: 50), - black: Duration(seconds: 775, milliseconds: 940), - ), - ); + expect(game.clock?.running, true); + expect(game.clock?.white, const Duration(seconds: 149, milliseconds: 50)); + + expect(game.clock?.black, const Duration(seconds: 775, milliseconds: 940)); expect( game.meta, GameMeta( - createdAt: - DateTime.fromMillisecondsSinceEpoch(1685698678928, isUtc: true), + createdAt: DateTime.fromMillisecondsSinceEpoch(1685698678928), rated: false, variant: Variant.standard, speed: Speed.classical, diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart new file mode 100644 index 0000000000..272907e58f --- /dev/null +++ b/test/model/game/game_socket_example_data.dart @@ -0,0 +1,126 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +typedef FullEventTestClock = + ({ + bool running, + Duration initial, + Duration increment, + Duration? emerg, + Duration white, + Duration black, + }); + +typedef FullEventTestCorrespondenceClock = ({Duration white, Duration black, int daysPerTurn}); + +String makeFullEvent( + GameId id, + String pgn, { + required String whiteUserName, + required String blackUserName, + int socketVersion = 0, + Side? youAre, + FullEventTestClock? clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + emerg: Duration(seconds: 30), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + ), + FullEventTestCorrespondenceClock? correspondenceClock, +}) { + final youAreStr = youAre != null ? '"youAre": "${youAre.name}",' : ''; + final clockStr = + clock != null + ? ''' + "clock": { + "running": ${clock.running}, + "initial": ${clock.initial.inSeconds}, + "increment": ${clock.increment.inSeconds}, + "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, + "emerg": 30, + "moretime": 15 + }, +''' + : ''; + + final correspondenceClockStr = + correspondenceClock != null + ? ''' + "correspondence": { + "daysPerTurn": ${correspondenceClock.daysPerTurn}, + "white": ${(correspondenceClock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(correspondenceClock.black.inMilliseconds / 1000).toStringAsFixed(2)} + }, +''' + : ''; + + return ''' +{ + "t": "full", + "d": { + "game": { + "id": "$id", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "speed": "${clock != null ? 'blitz' : 'correspondence'}", + "perf": "${clock != null ? 'blitz' : 'correspondence'}", + "rated": false, + "source": "lobby", + "status": { + "id": 20, + "name": "started" + }, + "createdAt": 1685698678928, + "pgn": "$pgn" + }, + "white": { + "user": { + "name": "$whiteUserName", + "patron": true, + "id": "${whiteUserName.toLowerCase()}" + }, + "rating": 1806, + "provisional": true, + "onGame": true + }, + "black": { + "user": { + "name": "$blackUserName", + "patron": true, + "id": "${blackUserName.toLowerCase()}" + }, + "onGame": true + }, + $clockStr + $correspondenceClockStr + $youAreStr + "socket": $socketVersion, + "expiration": { + "idleMillis": 245, + "millisToMove": 30000 + }, + "chat": { + "lines": [ + { + "u": "Zidrox", + "t": "Good luck", + "f": "people.man-singer" + }, + { + "u": "lichess", + "t": "Takeback accepted", + "f": "activity.lichess" + } + ] + } + }, + "v": $socketVersion +} +'''; +} diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index ac49917849..201dd428b1 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -1,7 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -13,51 +12,24 @@ import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('GameStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); - final storage = container.read(gameStorageProvider); + final storage = await container.read(gameStorageProvider.future); await storage.save(game); - expect( - storage.fetch( - gameId: gameId, - ), - completion(equals(game)), - ); + expect(storage.fetch(gameId: gameId), completion(equals(game))); }); test('paginate games', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); - final storage = container.read(gameStorageProvider); + final storage = await container.read(gameStorageProvider.future); for (final game in games) { await storage.save(game); @@ -69,11 +41,7 @@ void main() { expect(page1.length, 10); expect(page1.last.game.id, const GameId('game0090')); - final page2 = await storage.page( - userId: userId, - max: 10, - until: page1.last.lastModified, - ); + final page2 = await storage.page(userId: userId, max: 10, until: page1.last.lastModified); expect(page2.length, 10); expect(page2.last.game.id, const GameId('game0080')); }); @@ -107,6 +75,7 @@ final game = ArchivedGame( speed: Speed.correspondence, variant: Variant.standard, ), + source: GameSource.lobby, data: LightArchivedGame( id: gameId, variant: Variant.standard, @@ -116,43 +85,14 @@ final game = ArchivedGame( speed: Speed.blitz, rated: true, status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), - clock: ( - initial: const Duration(minutes: 2), - increment: const Duration(seconds: 3), - ), - ), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, ); @@ -167,6 +107,7 @@ final games = List.generate(100, (index) { speed: Speed.correspondence, variant: Variant.standard, ), + source: GameSource.lobby, data: LightArchivedGame( id: id, variant: Variant.standard, @@ -176,43 +117,14 @@ final games = List.generate(100, (index) { speed: Speed.blitz, rated: true, status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), - clock: ( - initial: const Duration(minutes: 2), - increment: const Duration(seconds: 3), - ), - ), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, ); }); diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index a00b06145d..6fdd980b27 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -8,14 +8,12 @@ void main() { group('PlayableGame', () { test('makePgn, unfinished game', () { final game = PlayableGame.fromServerJson( - jsonDecode(_unfishinedPlayableGameJson) as Map, + jsonDecode(_unfinishedGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/Fn9UvVKF"] +[Site "https://lichess.dev/Fn9UvVKF"] [Date "2024.01.25"] [White "chabrot"] [Black "veloce"] @@ -26,19 +24,17 @@ void main() { [TimeControl "120+1"] * -''', - ); +'''); }); + test('makePgn, finished game', () { final game = PlayableGame.fromServerJson( jsonDecode(_playableGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] @@ -51,8 +47,50 @@ void main() { [TimeControl "120+1"] 1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. b4 Bxb4 5. c3 Ba5 6. d4 Bb6 7. Ba3 Nf6 8. Qb3 d6 9. Bxf7+ Kf8 10. O-O Qe7 11. Nxe5 Nxe5 12. dxe5 Be6 13. Bxe6 Nxe4 14. Re1 Nc5 15. Bxc5 Bxc5 16. Qxb7 Re8 17. Bh3 dxe5 18. Qf3+ Kg8 19. Nd2 Rf8 20. Qd5+ Rf7 21. Be6 Qxe6 22. Qxe6 1-0 -''', - ); +'''); + }); + + test('toArchivedGame', () { + for (final game in [_playableGameJson, _playable960GameJson]) { + final playableGame = PlayableGame.fromServerJson(jsonDecode(game) as Map); + final now = DateTime.now(); + final archivedGame = playableGame.toArchivedGame(finishedAt: now); + + expect(archivedGame.id, playableGame.id); + expect(archivedGame.meta, playableGame.meta); + expect(archivedGame.source, playableGame.source); + expect(archivedGame.data.id, playableGame.id); + expect(archivedGame.data.lastMoveAt, now); + expect(archivedGame.data.createdAt, playableGame.meta.createdAt); + expect(archivedGame.data.lastFen, playableGame.lastPosition.fen); + expect(archivedGame.data.variant, playableGame.meta.variant); + expect(archivedGame.data.perf, playableGame.meta.perf); + expect(archivedGame.data.speed, playableGame.meta.speed); + expect(archivedGame.data.rated, playableGame.meta.rated); + expect(archivedGame.data.winner, playableGame.winner); + expect(archivedGame.data.white, playableGame.white); + expect(archivedGame.data.black, playableGame.black); + expect(archivedGame.data.opening, playableGame.meta.opening); + expect( + archivedGame.data.clock, + playableGame.meta.clock != null + ? ( + initial: playableGame.meta.clock!.initial, + increment: playableGame.meta.clock!.increment, + ) + : null, + ); + expect(archivedGame.initialFen, playableGame.initialFen); + expect(archivedGame.isThreefoldRepetition, playableGame.isThreefoldRepetition); + expect(archivedGame.status, playableGame.status); + expect(archivedGame.winner, playableGame.winner); + expect(archivedGame.white, playableGame.white); + expect(archivedGame.black, playableGame.black); + expect(archivedGame.steps, playableGame.steps); + expect(archivedGame.clocks, playableGame.clocks); + expect(archivedGame.evals, playableGame.evals); + expect(archivedGame.youAre, playableGame.youAre); + } }); }); @@ -61,11 +99,9 @@ void main() { final game = ArchivedGame.fromServerJson( jsonDecode(_archivedGameJsonNoEvals) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] @@ -80,19 +116,16 @@ void main() { [Opening "Italian Game: Evans Gambit, Main Line"] 1. e4 { [%clk 0:02:00.03] } e5 { [%clk 0:02:00.03] } 2. Nf3 { [%emt 0:00:02.2] [%clk 0:01:58.83] } Nc6 { [%emt 0:00:02.92] [%clk 0:01:58.11] } 3. Bc4 { [%emt 0:00:03] [%clk 0:01:56.83] } Bc5 { [%emt 0:00:05.32] [%clk 0:01:53.79] } 4. b4 { [%emt 0:00:04.76] [%clk 0:01:53.07] } Bxb4 { [%emt 0:00:03.16] [%clk 0:01:51.63] } 5. c3 { [%emt 0:00:03.64] [%clk 0:01:50.43] } Ba5 { [%emt 0:00:02.2] [%clk 0:01:50.43] } 6. d4 { [%emt 0:00:02.44] [%clk 0:01:48.99] } Bb6 { [%emt 0:00:04.36] [%clk 0:01:47.07] } 7. Ba3 { [%emt 0:00:08.44] [%clk 0:01:41.55] } Nf6 { [%emt 0:00:03.24] [%clk 0:01:44.83] } 8. Qb3 { [%emt 0:00:02.36] [%clk 0:01:40.19] } d6 { [%emt 0:00:05.88] [%clk 0:01:39.95] } 9. Bxf7+ { [%emt 0:00:04.84] [%clk 0:01:36.35] } Kf8 { [%emt 0:00:01.72] [%clk 0:01:39.23] } 10. O-O { [%emt 0:00:07.72] [%clk 0:01:29.63] } Qe7 { [%emt 0:00:14.2] [%clk 0:01:26.03] } 11. Nxe5 { [%emt 0:00:11.48] [%clk 0:01:19.15] } Nxe5 { [%emt 0:00:04.2] [%clk 0:01:22.83] } 12. dxe5 { [%emt 0:00:02.52] [%clk 0:01:17.63] } Be6 { [%emt 0:00:09.24] [%clk 0:01:14.59] } 13. Bxe6 { [%emt 0:00:04.84] [%clk 0:01:13.79] } Nxe4 { [%emt 0:00:14.76] [%clk 0:01:00.83] } 14. Re1 { [%emt 0:00:08.92] [%clk 0:01:05.87] } Nc5 { [%emt 0:00:03.64] [%clk 0:00:58.19] } 15. Bxc5 { [%emt 0:00:03.24] [%clk 0:01:03.63] } Bxc5 { [%emt 0:00:02.68] [%clk 0:00:56.51] } 16. Qxb7 { [%emt 0:00:03.88] [%clk 0:01:00.75] } Re8 { [%emt 0:00:02.44] [%clk 0:00:55.07] } 17. Bh3 { [%emt 0:00:05] [%clk 0:00:56.75] } dxe5 { [%emt 0:00:08.04] [%clk 0:00:48.03] } 18. Qf3+ { [%emt 0:00:07.16] [%clk 0:00:50.59] } Kg8 { [%emt 0:00:03.88] [%clk 0:00:45.15] } 19. Nd2 { [%emt 0:00:06.12] [%clk 0:00:45.47] } Rf8 { [%emt 0:00:10.6] [%clk 0:00:35.55] } 20. Qd5+ { [%emt 0:00:06.76] [%clk 0:00:39.71] } Rf7 { [%emt 0:00:02.44] [%clk 0:00:34.11] } 21. Be6 { [%emt 0:00:08.36] [%clk 0:00:32.35] } Qxe6 { [%emt 0:00:03.88] [%clk 0:00:31.23] } 22. Qxe6 { [%emt 0:00:02.15] [%clk 0:00:31.2] } 1-0 -''', - ); +'''); }); test('makePgn, with evals and clocks', () { final game = ArchivedGame.fromServerJson( jsonDecode(_archivedGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] @@ -107,13 +140,12 @@ void main() { [Opening "Italian Game: Evans Gambit, Main Line"] 1. e4 { [%eval 0.32] [%clk 0:02:00.03] } e5 { [%eval 0.41] [%clk 0:02:00.03] } 2. Nf3 { [%eval 0.39] [%emt 0:00:02.2] [%clk 0:01:58.83] } Nc6 { [%eval 0.20] [%emt 0:00:02.92] [%clk 0:01:58.11] } 3. Bc4 { [%eval 0.17] [%emt 0:00:03] [%clk 0:01:56.83] } Bc5 { [%eval 0.21] [%emt 0:00:05.32] [%clk 0:01:53.79] } 4. b4 { [%eval -0.21] [%emt 0:00:04.76] [%clk 0:01:53.07] } Bxb4 { [%eval -0.14] [%emt 0:00:03.16] [%clk 0:01:51.63] } 5. c3 { [%eval -0.23] [%emt 0:00:03.64] [%clk 0:01:50.43] } Ba5 { [%eval -0.24] [%emt 0:00:02.2] [%clk 0:01:50.43] } 6. d4 { [%eval -0.24] [%emt 0:00:02.44] [%clk 0:01:48.99] } Bb6 \$6 { Inaccuracy. d6 was best. [%eval 0.52] [%emt 0:00:04.36] [%clk 0:01:47.07] } ( 6... d6 ) 7. Ba3 \$6 { Inaccuracy. Nxe5 was best. [%eval -0.56] [%emt 0:00:08.44] [%clk 0:01:41.55] } ( 7. Nxe5 ) 7... Nf6 \$4 { Blunder. d6 was best. [%eval 1.77] [%emt 0:00:03.24] [%clk 0:01:44.83] } ( 7... d6 ) 8. Qb3 \$4 { Blunder. dxe5 was best. [%eval -0.19] [%emt 0:00:02.36] [%clk 0:01:40.19] } ( 8. dxe5 Ng4 9. Qd5 Nh6 10. Nbd2 Ne7 11. Qd3 O-O 12. h3 d6 13. g4 Kh8 14. exd6 cxd6 ) 8... d6 { [%eval -0.16] [%emt 0:00:05.88] [%clk 0:01:39.95] } 9. Bxf7+ { [%eval -0.20] [%emt 0:00:04.84] [%clk 0:01:36.35] } Kf8 { [%eval -0.12] [%emt 0:00:01.72] [%clk 0:01:39.23] } 10. O-O \$2 { Mistake. Bd5 was best. [%eval -1.45] [%emt 0:00:07.72] [%clk 0:01:29.63] } ( 10. Bd5 Nxd5 ) 10... Qe7 \$4 { Blunder. Na5 was best. [%eval 0.72] [%emt 0:00:14.2] [%clk 0:01:26.03] } ( 10... Na5 11. Qd1 Kxf7 12. dxe5 dxe5 13. Nxe5+ Ke8 14. Nd2 Be6 15. Qa4+ Bd7 16. Qd1 Nc6 17. Ndc4 ) 11. Nxe5 \$6 { Inaccuracy. Bd5 was best. [%eval -0.36] [%emt 0:00:11.48] [%clk 0:01:19.15] } ( 11. Bd5 Nxd5 12. exd5 Na5 13. Qb4 exd4 14. cxd4 Kg8 15. Re1 Qf7 16. Ng5 Qg6 17. Nc3 h6 ) 11... Nxe5 { [%eval -0.41] [%emt 0:00:04.2] [%clk 0:01:22.83] } 12. dxe5 { [%eval -0.42] [%emt 0:00:02.52] [%clk 0:01:17.63] } Be6 \$4 { Blunder. Qxf7 was best. [%eval 5.93] [%emt 0:00:09.24] [%clk 0:01:14.59] } ( 12... Qxf7 13. exf6 gxf6 14. c4 Rg8 15. Nd2 Qh5 16. c5 Bh3 17. g3 Bxc5 18. Bxc5 dxc5 19. Rfe1 ) 13. Bxe6 { [%eval 5.89] [%emt 0:00:04.84] [%clk 0:01:13.79] } Nxe4 { [%eval 6.30] [%emt 0:00:14.76] [%clk 0:01:00.83] } 14. Re1 \$4 { Blunder. exd6 was best. [%eval -0.32] [%emt 0:00:08.92] [%clk 0:01:05.87] } ( 14. exd6 cxd6 15. Bd5 Nxf2 16. Nd2 g5 17. Nc4 Kg7 18. Nxb6 Qe3 19. Rxf2 Rhf8 20. Bf3 axb6 ) 14... Nc5 \$4 { Blunder. Bxf2+ was best. [%eval 6.02] [%emt 0:00:03.64] [%clk 0:00:58.19] } ( 14... Bxf2+ ) 15. Bxc5 { [%eval 5.81] [%emt 0:00:03.24] [%clk 0:01:03.63] } Bxc5 { [%eval 6.56] [%emt 0:00:02.68] [%clk 0:00:56.51] } 16. Qxb7 { [%eval 6.62] [%emt 0:00:03.88] [%clk 0:01:00.75] } Re8 \$4 { Checkmate is now unavoidable. g6 was best. [%eval #15] [%emt 0:00:02.44] [%clk 0:00:55.07] } ( 16... g6 17. Qxa8+ Kg7 18. Qd5 c6 19. Qb3 Rf8 20. Re2 Qh4 21. Qb7+ Kh6 22. Qb2 dxe5 23. Nd2 ) 17. Bh3 \$4 { Lost forced checkmate sequence. Qf3+ was best. [%eval 5.66] [%emt 0:00:05] [%clk 0:00:56.75] } ( 17. Qf3+ Qf6 18. exf6 g6 19. f7 Kg7 20. fxe8=Q Rxe8 21. Qf7+ Kh6 22. Qxe8 d5 23. g4 c6 ) 17... dxe5 { [%eval 5.74] [%emt 0:00:08.04] [%clk 0:00:48.03] } 18. Qf3+ { [%eval 5.66] [%emt 0:00:07.16] [%clk 0:00:50.59] } Kg8 { [%eval 5.80] [%emt 0:00:03.88] [%clk 0:00:45.15] } 19. Nd2 { [%eval 5.69] [%emt 0:00:06.12] [%clk 0:00:45.47] } Rf8 \$6 { Inaccuracy. g6 was best. [%eval 7.74] [%emt 0:00:10.6] [%clk 0:00:35.55] } ( 19... g6 20. Ne4 Kg7 21. Qe2 Rd8 22. a4 h5 23. Rad1 Rxd1 24. Rxd1 Bb6 25. Rd7 Qxd7 26. Bxd7 ) 20. Qd5+ { [%eval 7.39] [%emt 0:00:06.76] [%clk 0:00:39.71] } Rf7 { [%eval 7.43] [%emt 0:00:02.44] [%clk 0:00:34.11] } 21. Be6 { [%eval 6.15] [%emt 0:00:08.36] [%clk 0:00:32.35] } Qxe6 \$6 { Inaccuracy. Bxf2+ was best. [%eval 9.34] [%emt 0:00:03.88] [%clk 0:00:31.23] } ( 21... Bxf2+ 22. Kh1 Bxe1 23. Rxe1 g6 24. Rf1 Kg7 25. Rxf7+ Qxf7 26. Bxf7 Rf8 27. Be6 e4 28. Qxe4 ) 22. Qxe6 { [%eval 8.61] [%emt 0:00:02.15] [%clk 0:00:31.2] } 1-0 -''', - ); +'''); }); }); } -const _unfishinedPlayableGameJson = ''' +const _unfinishedGameJson = ''' {"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} '''; @@ -121,10 +153,14 @@ const _playableGameJson = ''' {"game":{"id":"CCW6EEru","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"6kr/p1p2rpp/4Q3/2b1p3/8/2P5/P2N1PPP/R3R1K1 b - - 0 22","turns":43,"source":"lobby","status":{"id":31,"name":"resign"},"createdAt":1706185945680,"winner":"white","pgn":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6"},"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9},"socket":0,"clock":{"running":false,"initial":120,"increment":1,"white":31.2,"black":27.42,"emerg":15,"moretime":15},"takebackable":true,"youAre":"white","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} '''; +const _playable960GameJson = ''' +{"game":{"id":"sfqnC9ZK","variant":{"key":"chess960","name":"Chess960","short":"960"},"speed":"blitz","perf":"chess960","rated":false,"fen":"1k2rb1n/pp4pp/1n3p2/2Npr3/5N2/5PP1/1PPBP2P/q1KRR1Q1 w - - 1 15","turns":28,"source":"lobby","status":{"id":30,"name":"mate"},"createdAt":1686125895867,"initialFen":"nrbkrbqn/pppppppp/8/8/8/8/PPPPPPPP/NRBKRBQN w KQkq - 0 1","winner":"black","pgn":"f3 Nb6 d3 d6 Be3 c5 d4 Bf5 O-O-O O-O-O Nf2 e5 dxe5 Rxe5 g3 Kb8 Bh3 Bxh3 Nxh3 f6 Nf4 Qxa2 Nb3 Rde8 Bd2 d5 Nxc5 Qa1#"},"white":{"user":{"name":"MinBorn","id":"minborn"},"rating":1500,"provisional":true},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1292,"provisional":true,"onGame":true},"socket":0,"clock":{"running":false,"initial":180,"increment":2,"white":145.34,"black":118.8,"emerg":22,"moretime":15},"youAre":"black","prefs":{"autoQueen":2,"zen":0,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} +'''; + const _archivedGameJson = ''' -{"id":"CCW6EEru","rated":true,"variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9,"analysis":{"inaccuracy":2,"mistake":1,"blunder":3,"acpl":90}},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9,"analysis":{"inaccuracy":3,"mistake":0,"blunder":5,"acpl":135}}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"analysis":[{"eval":32},{"eval":41},{"eval":39},{"eval":20},{"eval":17},{"eval":21},{"eval":-21},{"eval":-14},{"eval":-23},{"eval":-24},{"eval":-24},{"eval":52,"best":"d7d6","variation":"d6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d6 was best."}},{"eval":-56,"best":"f3e5","variation":"Nxe5","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nxe5 was best."}},{"eval":177,"best":"d7d6","variation":"d6","judgment":{"name":"Blunder","comment":"Blunder. d6 was best."}},{"eval":-19,"best":"d4e5","variation":"dxe5 Ng4 Qd5 Nh6 Nbd2 Ne7 Qd3 O-O h3 d6 g4 Kh8 exd6 cxd6","judgment":{"name":"Blunder","comment":"Blunder. dxe5 was best."}},{"eval":-16},{"eval":-20},{"eval":-12},{"eval":-145,"best":"f7d5","variation":"Bd5 Nxd5","judgment":{"name":"Mistake","comment":"Mistake. Bd5 was best."}},{"eval":72,"best":"c6a5","variation":"Na5 Qd1 Kxf7 dxe5 dxe5 Nxe5+ Ke8 Nd2 Be6 Qa4+ Bd7 Qd1 Nc6 Ndc4","judgment":{"name":"Blunder","comment":"Blunder. Na5 was best."}},{"eval":-36,"best":"f7d5","variation":"Bd5 Nxd5 exd5 Na5 Qb4 exd4 cxd4 Kg8 Re1 Qf7 Ng5 Qg6 Nc3 h6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bd5 was best."}},{"eval":-41},{"eval":-42},{"eval":593,"best":"e7f7","variation":"Qxf7 exf6 gxf6 c4 Rg8 Nd2 Qh5 c5 Bh3 g3 Bxc5 Bxc5 dxc5 Rfe1","judgment":{"name":"Blunder","comment":"Blunder. Qxf7 was best."}},{"eval":589},{"eval":630},{"eval":-32,"best":"e5d6","variation":"exd6 cxd6 Bd5 Nxf2 Nd2 g5 Nc4 Kg7 Nxb6 Qe3 Rxf2 Rhf8 Bf3 axb6","judgment":{"name":"Blunder","comment":"Blunder. exd6 was best."}},{"eval":602,"best":"b6f2","variation":"Bxf2+","judgment":{"name":"Blunder","comment":"Blunder. Bxf2+ was best."}},{"eval":581},{"eval":656},{"eval":662},{"mate":15,"best":"g7g6","variation":"g6 Qxa8+ Kg7 Qd5 c6 Qb3 Rf8 Re2 Qh4 Qb7+ Kh6 Qb2 dxe5 Nd2","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. g6 was best."}},{"eval":566,"best":"b7f3","variation":"Qf3+ Qf6 exf6 g6 f7 Kg7 fxe8=Q Rxe8 Qf7+ Kh6 Qxe8 d5 g4 c6","judgment":{"name":"Blunder","comment":"Lost forced checkmate sequence. Qf3+ was best."}},{"eval":574},{"eval":566},{"eval":580},{"eval":569},{"eval":774,"best":"g7g6","variation":"g6 Ne4 Kg7 Qe2 Rd8 a4 h5 Rad1 Rxd1 Rxd1 Bb6 Rd7 Qxd7 Bxd7","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. g6 was best."}},{"eval":739},{"eval":743},{"eval":615},{"eval":934,"best":"c5f2","variation":"Bxf2+ Kh1 Bxe1 Rxe1 g6 Rf1 Kg7 Rxf7+ Qxf7 Bxf7 Rf8 Be6 e4 Qxe4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bxf2+ was best."}},{"eval":861}],"clock":{"initial":120,"increment":1,"totalTime":160}} +{"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9,"analysis":{"inaccuracy":2,"mistake":1,"blunder":3,"acpl":90}},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9,"analysis":{"inaccuracy":3,"mistake":0,"blunder":5,"acpl":135}}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"analysis":[{"eval":32},{"eval":41},{"eval":39},{"eval":20},{"eval":17},{"eval":21},{"eval":-21},{"eval":-14},{"eval":-23},{"eval":-24},{"eval":-24},{"eval":52,"best":"d7d6","variation":"d6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d6 was best."}},{"eval":-56,"best":"f3e5","variation":"Nxe5","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nxe5 was best."}},{"eval":177,"best":"d7d6","variation":"d6","judgment":{"name":"Blunder","comment":"Blunder. d6 was best."}},{"eval":-19,"best":"d4e5","variation":"dxe5 Ng4 Qd5 Nh6 Nbd2 Ne7 Qd3 O-O h3 d6 g4 Kh8 exd6 cxd6","judgment":{"name":"Blunder","comment":"Blunder. dxe5 was best."}},{"eval":-16},{"eval":-20},{"eval":-12},{"eval":-145,"best":"f7d5","variation":"Bd5 Nxd5","judgment":{"name":"Mistake","comment":"Mistake. Bd5 was best."}},{"eval":72,"best":"c6a5","variation":"Na5 Qd1 Kxf7 dxe5 dxe5 Nxe5+ Ke8 Nd2 Be6 Qa4+ Bd7 Qd1 Nc6 Ndc4","judgment":{"name":"Blunder","comment":"Blunder. Na5 was best."}},{"eval":-36,"best":"f7d5","variation":"Bd5 Nxd5 exd5 Na5 Qb4 exd4 cxd4 Kg8 Re1 Qf7 Ng5 Qg6 Nc3 h6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bd5 was best."}},{"eval":-41},{"eval":-42},{"eval":593,"best":"e7f7","variation":"Qxf7 exf6 gxf6 c4 Rg8 Nd2 Qh5 c5 Bh3 g3 Bxc5 Bxc5 dxc5 Rfe1","judgment":{"name":"Blunder","comment":"Blunder. Qxf7 was best."}},{"eval":589},{"eval":630},{"eval":-32,"best":"e5d6","variation":"exd6 cxd6 Bd5 Nxf2 Nd2 g5 Nc4 Kg7 Nxb6 Qe3 Rxf2 Rhf8 Bf3 axb6","judgment":{"name":"Blunder","comment":"Blunder. exd6 was best."}},{"eval":602,"best":"b6f2","variation":"Bxf2+","judgment":{"name":"Blunder","comment":"Blunder. Bxf2+ was best."}},{"eval":581},{"eval":656},{"eval":662},{"mate":15,"best":"g7g6","variation":"g6 Qxa8+ Kg7 Qd5 c6 Qb3 Rf8 Re2 Qh4 Qb7+ Kh6 Qb2 dxe5 Nd2","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. g6 was best."}},{"eval":566,"best":"b7f3","variation":"Qf3+ Qf6 exf6 g6 f7 Kg7 fxe8=Q Rxe8 Qf7+ Kh6 Qxe8 d5 g4 c6","judgment":{"name":"Blunder","comment":"Lost forced checkmate sequence. Qf3+ was best."}},{"eval":574},{"eval":566},{"eval":580},{"eval":569},{"eval":774,"best":"g7g6","variation":"g6 Ne4 Kg7 Qe2 Rd8 a4 h5 Rad1 Rxd1 Rxd1 Bb6 Rd7 Qxd7 Bxd7","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. g6 was best."}},{"eval":739},{"eval":743},{"eval":615},{"eval":934,"best":"c5f2","variation":"Bxf2+ Kh1 Bxe1 Rxe1 g6 Rf1 Kg7 Rxf7+ Qxf7 Bxf7 Rf8 Be6 e4 Qxe4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bxf2+ was best."}},{"eval":861}],"clock":{"initial":120,"increment":1,"totalTime":160}} '''; const _archivedGameJsonNoEvals = ''' -{"id":"CCW6EEru","rated":true,"variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"clock":{"initial":120,"increment":1,"totalTime":160}} +{"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"clock":{"initial":120,"increment":1,"totalTime":160}} '''; diff --git a/test/model/game/material_diff_test.dart b/test/model/game/material_diff_test.dart index 7a1e7c6a79..264f25b582 100644 --- a/test/model/game/material_diff_test.dart +++ b/test/model/game/material_diff_test.dart @@ -6,8 +6,7 @@ import 'package:lichess_mobile/src/model/game/material_diff.dart'; void main() { group('GameMaterialDiff', () { test('generation from board', () { - final Board board = - Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'); + final Board board = Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'); final MaterialDiff diff = MaterialDiff.fromBoard(board); expect(diff.bySide(Side.black).score, equals(-10)); @@ -38,6 +37,32 @@ void main() { }), ), ); + expect( + diff.bySide(Side.black).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 0, + Role.rook: 0, + Role.bishop: 2, + Role.knight: 2, + Role.pawn: 4, + }), + ), + ); + expect( + diff.bySide(Side.white).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 1, + Role.rook: 1, + Role.bishop: 1, + Role.knight: 2, + Role.pawn: 3, + }), + ), + ); }); }); } diff --git a/test/model/game/mock_game_storage.dart b/test/model/game/mock_game_storage.dart deleted file mode 100644 index 0ed719063f..0000000000 --- a/test/model/game/mock_game_storage.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; - -class MockGameStorage implements GameStorage { - @override - Future delete(GameId gameId) { - return Future.value(); - } - - @override - Future fetch({required GameId gameId}) { - return Future.value(null); - } - - @override - Future> page({ - UserId? userId, - DateTime? until, - int max = 10, - }) { - return Future.value(IList()); - } - - @override - Future save(ArchivedGame game) { - return Future.value(); - } - - @override - Future count({UserId? userId}) { - return Future.value(0); - } -} diff --git a/test/model/lobby/lobby_repository_test.dart b/test/model/lobby/lobby_repository_test.dart index e790bae327..e18f00f7c0 100644 --- a/test/model/lobby/lobby_repository_test.dart +++ b/test/model/lobby/lobby_repository_test.dart @@ -1,13 +1,13 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { final mockClient = MockClient((request) { diff --git a/test/model/notifications/fake_notification_display.dart b/test/model/notifications/fake_notification_display.dart new file mode 100644 index 0000000000..b4332ca865 --- /dev/null +++ b/test/model/notifications/fake_notification_display.dart @@ -0,0 +1,34 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeNotificationDisplay extends Fake implements FlutterLocalNotificationsPlugin { + final Map _activeNotifications = {}; + + @override + Future show( + int id, + String? title, + String? body, + NotificationDetails? notificationDetails, { + String? payload, + }) { + _activeNotifications[id] = ActiveNotification( + id: id, + title: title, + body: body, + payload: payload, + ); + return Future.value(); + } + + @override + Future cancel(int id, {String? tag}) { + _activeNotifications.remove(id); + return Future.value(); + } + + @override + Future> getActiveNotifications() { + return Future.value(_activeNotifications.values.toList()); + } +} diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart new file mode 100644 index 0000000000..776d059fbe --- /dev/null +++ b/test/model/notifications/notification_service_test.dart @@ -0,0 +1,338 @@ +import 'dart:convert'; + +import 'package:fake_async/fake_async.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../binding.dart'; +import '../../test_container.dart'; +import '../../test_helpers.dart'; +import '../auth/fake_session_storage.dart'; + +class NotificationDisplayMock extends Mock implements FlutterLocalNotificationsPlugin {} + +class CorrespondenceServiceMock extends Mock implements CorrespondenceService {} + +class FakePlayableGame extends Fake implements PlayableGame {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final notificationDisplayMock = NotificationDisplayMock(); + final correspondenceServiceMock = CorrespondenceServiceMock(); + + int registerDeviceCalls = 0; + + setUpAll(() { + registerFallbackValue(FakePlayableGame()); + }); + + tearDown(() { + registerDeviceCalls = 0; + reset(notificationDisplayMock); + reset(correspondenceServiceMock); + }); + + final registerMockClient = MockClient((request) { + if (request.url.path == '/mobile/register/firebase/test-token') { + registerDeviceCalls++; + return mockResponse('{"ok": true}', 200); + } + return mockResponse('', 404); + }); + + group('Start service:', () { + test('request permissions', () async { + final container = await makeContainer(); + + final notificationService = container.read(notificationServiceProvider); + + await notificationService.start(); + + final calls = testBinding.firebaseMessaging.verifyRequestPermissionCalls(); + expect(calls, hasLength(1)); + expect( + calls.first, + equals(( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + )), + ); + }); + + test( + 'register device when online, token exists and permissions are granted and a session exists', + () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 1); + }); + }, + ); + + test("don't try to register device when permissions are not granted", () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + testBinding.firebaseMessaging.willGrantPermission = false; + + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 0); + }); + }); + + test("don't try to register device when user is not logged in", () async { + final container = await makeContainer( + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 0); + }); + }); + }); + + group('Correspondence game update notifications', () { + test('FCM message with associated notification will show it in foreground', () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + const fullId = GameFullId('9wlmxmibr9gh'); + + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: {'lichess.type': 'gameMove', 'lichess.fullId': '9wlmxmibr9gh'}, + notification: RemoteNotification( + title: 'It is your turn!', + body: 'Dr-Alaakour played a move', + ), + ), + ); + + async.flushMicrotasks(); + + const expectedNotif = CorresGameUpdateNotification( + fullId, + 'It is your turn!', + 'Dr-Alaakour played a move', + ); + + final result = verify( + () => notificationDisplayMock.show( + fullId.hashCode, + 'It is your turn!', + 'Dr-Alaakour played a move', + captureAny(), + payload: jsonEncode(expectedNotif.payload), + ), + ); + + result.called(1); + expect( + result.captured[0], + isA() + .having((d) => d.android?.importance, 'importance', Importance.high) + .having((d) => d.android?.priority, 'priority', Priority.defaultPriority), + ); + }); + }); + + test('FCM game data message will update the game', () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), + correspondenceServiceProvider.overrideWith((_) => correspondenceServiceMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + const fullId = GameFullId('Fn9UvVKFsopx'); + + when( + () => correspondenceServiceMock.onServerUpdateEvent( + fullId, + any(that: isA()), + fromBackground: false, + ), + ).thenAnswer((_) => Future.value()); + + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: { + 'lichess.type': 'gameMove', + 'lichess.fullId': 'Fn9UvVKFsopx', + 'lichess.round': + '{"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}}', + }, + notification: RemoteNotification( + title: 'It is your turn!', + body: 'Dr-Alaakour played a move', + ), + ), + ); + + async.flushMicrotasks(); + + verify( + () => correspondenceServiceMock.onServerUpdateEvent( + fullId, + any(that: isA()), + fromBackground: false, + ), + ).called(1); + + verify( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).called(1); + }); + }); + + test('FCM game data message without notification', () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), + correspondenceServiceProvider.overrideWith((_) => correspondenceServiceMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + when( + () => correspondenceServiceMock.onServerUpdateEvent( + any(that: isA()), + any(that: isA()), + fromBackground: false, + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: { + 'lichess.type': 'gameMove', + 'lichess.fullId': 'Fn9UvVKFsopx', + 'lichess.round': + '{"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}}', + }, + ), + ); + + async.flushMicrotasks(); + + verify( + () => correspondenceServiceMock.onServerUpdateEvent( + any(that: isA()), + any(that: isA()), + fromBackground: false, + ), + ).called(1); + + verifyNever( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ); + }); + }); + }); +} diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart new file mode 100644 index 0000000000..ffdaa4b93e --- /dev/null +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -0,0 +1,236 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; + +import '../../test_container.dart'; +import '../../test_helpers.dart'; + +void main() { + group('OpeningExplorerRepository.getMasterDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 834333, + "draws": 1085272, + "black": 600303, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 2399, + "white": 372266, + "draws": 486092, + "black": 280238, + "game": null + }, + { + "uci": "d2d4", + "san": "d4", + "averageRating": 2414, + "white": 302160, + "draws": 397224, + "black": 209077, + "game": null + } + ], + "topGames": [ + { + "uci": "d2d4", + "id": "QR5UbqUY", + "winner": null, + "black": { + "name": "Caruana, F.", + "rating": 2818 + }, + "white": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "year": 2019, + "month": "2019-08" + }, + { + "uci": "e2e4", + "id": "Sxov6E94", + "winner": "white", + "black": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "white": { + "name": "Caruana, F.", + "rating": 2818 + }, + "year": 2019, + "month": "2019-08" + } + ], + "opening": null +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/masters') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final result = await repo.getMasterDatabase('fen'); + expect(result, isA()); + expect(result.moves.length, 2); + expect(result.topGames, isNotNull); + expect(result.topGames!.length, 2); + }); + }); + + group('OpeningExplorerRepository.getLichessDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 2848672002, + "draws": 225287646, + "black": 2649860106, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 1604, + "white": 1661457614, + "draws": 129433754, + "black": 1565161663, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "rapid", + "mode": "rated", + "black": { + "name": "Jcats1", + "rating": 1548 + }, + "white": { + "name": "carlosrivero32", + "rating": 1690 + }, + "year": 2024, + "month": "2024-06" + } + ], + "topGames": [], + "opening": null +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/lichess') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final result = await repo.getLichessDatabase( + 'fen', + speeds: const ISetConst({Speed.rapid}), + ratings: const ISetConst({1000, 1200}), + ); + expect(result, isA()); + expect(result.moves.length, 1); + expect(result.recentGames, isNotNull); + expect(result.recentGames!.length, 1); + expect(result.topGames, isNotNull); + expect(result.topGames!.length, 0); + }); + }); + + group('OpeningExplorerRepository.getPlayerDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 1713, + "draws": 119, + "black": 1459, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageOpponentRating": 1767, + "performance": 1796, + "white": 1691, + "draws": 116, + "black": 1432, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "bullet", + "mode": "rated", + "black": { + "name": "foo", + "rating": 1869 + }, + "white": { + "name": "baz", + "rating": 1912 + }, + "year": 2023, + "month": "2023-08" + } + ], + "opening": null, + "queuePosition": 0 +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/player') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final results = await repo.getPlayerDatabase( + 'fen', + usernameOrId: 'baz', + color: Side.white, + speeds: const ISetConst({Speed.bullet}), + gameModes: const ISetConst({GameMode.rated}), + ); + expect(results, isA>()); + await for (final result in results) { + expect(result, isA()); + expect(result.moves.length, 1); + expect(result.recentGames, isNotNull); + expect(result.recentGames!.length, 1); + } + }); + }); +} diff --git a/test/model/puzzle/mock_server_responses.dart b/test/model/puzzle/mock_server_responses.dart new file mode 100644 index 0000000000..eca36e9cb7 --- /dev/null +++ b/test/model/puzzle/mock_server_responses.dart @@ -0,0 +1,13 @@ +const mockDailyPuzzleResponse = ''' +{"game":{"id":"MNMYnEjm","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"name":"Igor76","id":"igor76","color":"white","rating":2211},{"name":"dmitriy_duyun","id":"dmitriy_duyun","color":"black","rating":2180}],"pgn":"e4 c6 d4 d5 Nc3 g6 Nf3 Bg7 h3 dxe4 Nxe4 Nf6 Bd3 Nxe4 Bxe4 Nd7 O-O Nf6 Bd3 O-O Re1 Bf5 Bxf5 gxf5 c3 e6 Bg5 Qb6 Qc2 Rac8 Ne5 Qc7 Rad1 Nd7 Bf4 Nxe5 Bxe5 Bxe5 Rxe5 Rcd8 Qd2 Kh8 Rde1 Rg8 Qf4","clock":"20+15"},"puzzle":{"id":"0XqV2","rating":1929,"plays":93270,"solution":["f7f6","e5f5","c7g7","g2g3","e6f5"],"themes":["clearance","endgame","advantage","intermezzo","long"],"initialPly":44}} +'''; + +const mockMixBatchResponse = ''' +{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]} +'''; + +const mockActivityResponse = ''' +{ "date": 1717460624888, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLL", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +{ "date": 1717460624788, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLK", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +{ "date": 1717460624688, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLG", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +'''; diff --git a/test/model/puzzle/puzzle_batch_storage_test.dart b/test/model/puzzle/puzzle_batch_storage_test.dart index cb1508e685..8ee1623183 100644 --- a/test/model/puzzle/puzzle_batch_storage_test.dart +++ b/test/model/puzzle/puzzle_batch_storage_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,63 +10,31 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite/sqflite.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('PuzzleBatchStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); - final storage = container.read(puzzleBatchStorageProvider); + final storage = await container.read(puzzleBatchStorageProvider.future); - await storage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: data, - ); + await storage.save(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix), data: data); expect( - storage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + storage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), completion(equals(data)), ); }); test('fetchSavedThemes', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); - final storage = container.read(puzzleBatchStorageProvider); + final storage = await container.read(puzzleBatchStorageProvider.future); - await storage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: data, - ); + await storage.save(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix), data: data); await storage.save( userId: null, angle: const PuzzleTheme(PuzzleThemeKey.rookEndgame), @@ -89,6 +59,55 @@ void main() { ), ); }); + + test('fetchSavedOpenings', () async { + final container = await makeContainer(); + + final storage = await container.read(puzzleBatchStorageProvider.future); + + await storage.save(userId: null, angle: const PuzzleOpening('test_opening'), data: data); + await storage.save(userId: null, angle: const PuzzleOpening('test_opening2'), data: data); + + expect( + storage.fetchSavedOpenings(userId: null), + completion(equals(IMap(const {'test_opening': 1, 'test_opening2': 1}))), + ); + }); + + test('fetchAll', () async { + final container = await makeContainer(); + + final database = await container.read(databaseProvider.future); + final storage = await container.read(puzzleBatchStorageProvider.future); + + Future save(PuzzleAngle angle, PuzzleBatch data, String timestamp) { + return database.insert('puzzle_batchs', { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(data.toJson()), + 'lastModified': timestamp, + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + await save(const PuzzleTheme(PuzzleThemeKey.rookEndgame), data, '2021-01-02T00:00:00Z'); + await save(const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), data, '2021-01-03T00:00:00Z'); + await save(const PuzzleOpening('test_opening'), data, '2021-01-04T00:00:00Z'); + await save(const PuzzleOpening('test_opening2'), data, '2021-01-04T80:00:00Z'); + + expect( + storage.fetchAll(userId: null), + completion( + equals( + [ + const PuzzleOpening('test_opening2'), + const PuzzleOpening('test_opening'), + const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), + const PuzzleTheme(PuzzleThemeKey.rookEndgame), + ].map((angle) => (angle, 1)).toIList(), + ), + ), + ); + }); }); } @@ -111,14 +130,8 @@ final data = PuzzleBatch( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ), diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 0f884807d0..da9a1663ef 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -1,23 +1,20 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import 'mock_server_responses.dart'; void main() { group('PuzzleRepository', () { test('selectBatch', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' -{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]} -''', - 200, - ); + return mockResponse(mockMixBatchResponse, 200); } return mockResponse('', 404); }); @@ -35,12 +32,9 @@ void main() { test('selectBatch with glicko', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' + return mockResponse(''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}],"glicko":{"rating":1834.54,"deviation":23.45}} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -59,12 +53,9 @@ void main() { test('selectBatch with rounds', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' + return mockResponse(''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}],"glicko":{"rating":1834.54,"deviation":23.45}, "rounds": [{"id": "07jQK", "ratingDiff": 10, "win": true}, {"id": "06jOK", "ratingDiff": -40, "win": false}]} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -82,12 +73,9 @@ void main() { test('streak', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/streak') { - return mockResponse( - ''' + return mockResponse(''' {"game":{"id":"3dwjUYP0","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"suresh","name":"Suresh (1716)","color":"white"},{"userId":"yulia","name":"Yulia (1765)","color":"black"}],"pgn":"d4 Nf6 Nf3 e6 c4 d5 cxd5 Bb4+ Nc3 Nxd5 Bd2 c6 a3 Bxc3 Bxc3 Nd7 Ne5 Qc7 e3 O-O Bd3 Nxc3 bxc3 Nxe5 dxe5 Qxe5 O-O Qxc3 a4 b6 Rc1 Qf6 Rxc6 Bb7 Rc4 Rad8 Rd4 Qg5 g3 Rxd4 exd4 Qd5 f3 Rd8 Rf2 g6 Be4 Qd7 Bxb7 Qxb7 Kg2 Qd7 Rd2 e5 dxe5","clock":"10+0"},"puzzle":{"id":"9afDa","rating":642,"plays":13675,"initialPly":54,"solution":["d7d2","d1d2","d8d2"],"themes":["endgame","crushing","short"]},"angle":{"key":"mix","name":"Puzzle themes","desc":"A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games."},"streak":"9afDa 4V5gW 3mslj 41adQ 2tu7D 9RkvX 0vy7p A4v8U 5ZOBZ 193w0 98fRK CeonU 7yLlT 5RSB1 1tHFC 0Vsh7 7VFdg Dw0Rn EL08H 4dfgu 9ZxSP DUs0d 55MLt 9kmiT 0H0mL 0tBRV 7J6hk 0TjRQ 4G3KC DVlXY 1160r B8UHS 9NmPL 70ujM DJc5M BwkrY 94ynq D9wc6 41QGW 5sDnM 6xRVq 0EkpQ 7nksF 35Umd 0lJjY BrA7Z 8iHjv 5ypqy 4seCY 1bKuj 27svg 6K2S9 5lR21 9WveK DseMX C9m8Q 0K2CK 73mQX Bey7R CFniS 2NMq3 1eKTu 6131w 9m4mG 1H3Oi 9FxX2 4zRod 1C05H 9iEBH 21pIt 95dod 01tg7 47p37 1sK7x 0nSaW BWD8D C6WCD 9h38Q AoWyN CPdp8 ATUTK EFWL2 7GrRe 6W1OR 538Mf CH2cU An8P5 9LrrA 1cIQP B56EI 32pBl 34nq9 1aS2z 3qxyU 4NGY7 9GCq2 C43lx 2W8WA 1bnwL 4I8D1 Dc1u5 BG3VT 3pC4h C5tQJ 3rM5l 6KF3m 6Xnj5 EUX2q 1qiVv 2UTkb 7AtYx CbRCh 5xs9Y BlYuY BGFSj E7AIl 5keIv 1431G 7KYgv 68F2M 16IRi 8cNr9 8g79l BBM7N CmgIo 6zoOr D6Zsx 20mtz"} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -103,12 +91,9 @@ void main() { test('puzzle dashboard', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/dashboard/30') { - return mockResponse( - ''' + return mockResponse(''' {"days":30,"global":{"nb":196,"firstWins":107,"replayWins":0,"puzzleRatingAvg":1607,"performance":1653},"themes":{"middlegame":{"theme":"Middlegame","results":{"nb":97,"firstWins":51,"replayWins":0,"puzzleRatingAvg":1608,"performance":1634}},"endgame":{"theme":"Endgame","results":{"nb":81,"firstWins":48,"replayWins":0,"puzzleRatingAvg":1604,"performance":1697}}}} - ''', - 200, - ); + ''', 200); } return mockResponse('', 404); }); @@ -120,5 +105,22 @@ void main() { expect(result, isA()); }); + + test('puzzle activity', () async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/activity') { + return mockResponse(mockActivityResponse, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = PuzzleRepository(client); + final result = await repo.puzzleActivity(3); + + expect(result, isA>()); + expect(result.length, 3); + }); }); } diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index 734b3b15b2..4d58402ad1 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -5,8 +5,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -14,24 +12,15 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - Future makeTestContainer(MockClient mockClient) async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - return makeContainer( overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), lichessClientProvider.overrideWith((ref) { return LichessClient(mockClient, ref); }), @@ -51,14 +40,10 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 3, - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 3); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(nbReq, equals(1)); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); final data = await storage.fetch(userId: null); @@ -66,9 +51,7 @@ void main() { expect(data?.unsolved.length, equals(3)); }); - test( - 'if local queue is full, it will not download data but still try to fetch rating', - () async { + test('will not download data if local queue is full', () async { int nbReq = 0; final mockClient = MockClient((request) { nbReq++; @@ -79,20 +62,13 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: null, - ); - expect(nbReq, equals(1)); + final next = await service.nextPuzzle(userId: null); + expect(nbReq, equals(0)); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(1)); @@ -110,26 +86,18 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); expect(nbReq, equals(1)); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(2)); }); - test('nextPuzzle will always get the first puzzle of unsolved queue', - () async { + test('nextPuzzle will always get the first puzzle of unsolved queue', () async { int nbReq = 0; final mockClient = MockClient((request) { nbReq++; @@ -137,45 +105,31 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); expect(nbReq, equals(0)); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(1)); - final next2 = await service.nextPuzzle( - userId: null, - ); + final next2 = await service.nextPuzzle(userId: null); expect(next2?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data2 = await storage.fetch(userId: null); expect(data2?.unsolved.length, equals(1)); }); - test('nextPuzzle returns null is unsolved queue is empty and is offline', - () async { + test('nextPuzzle returns null is unsolved queue is empty and is offline', () async { final mockClient = MockClient((request) { throw const SocketException('offline'); }); final container = await makeTestContainer(mockClient); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); - final nextPuzzle = await service.nextPuzzle( - userId: null, - ); + final nextPuzzle = await service.nextPuzzle(userId: null); expect(nextPuzzle, isNull); }); @@ -191,26 +145,16 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: const UserId('testUserId'), - ); + final next = await service.nextPuzzle(userId: const UserId('testUserId')); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); expect(nbReq, equals(1)); final data = await storage.fetch(userId: const UserId('testUserId')); - expect( - data?.unsolved.length, - equals(1), - ); + expect(data?.unsolved.length, equals(1)); }); test('different batch is saved per angle', () async { @@ -224,14 +168,9 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); final next = await service.nextPuzzle( angle: const PuzzleTheme(PuzzleThemeKey.opening), @@ -244,10 +183,7 @@ void main() { userId: null, angle: const PuzzleTheme(PuzzleThemeKey.opening), ); - expect( - data?.unsolved.length, - equals(1), - ); + expect(data?.unsolved.length, equals(1)); }); test('solve puzzle when online, no userId', () async { @@ -261,22 +197,13 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); final next = await service.solve( puzzle: samplePuzzle, - solution: const PuzzleSolution( - id: PuzzleId('pId3'), - win: true, - rated: true, - ), + solution: const PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true), userId: null, ); @@ -296,8 +223,7 @@ void main() { nbReq++; if (request.method == 'POST' && request.url.path == '/api/puzzle/batch/mix' && - request.body == - '{"solutions":[{"id":"pId3","win":true,"rated":true}]}') { + request.body == '{"solutions":[{"id":"pId3","win":true,"rated":true}]}') { return mockResponse( '''{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}], "glicko":{"rating":1834.54,"deviation":23.45},"rounds":[{"id": "pId3","ratingDiff": 10,"win": true}]}''', 200, @@ -307,10 +233,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); await storage.save( userId: const UserId('testUserId'), data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), @@ -318,33 +242,21 @@ void main() { final next = await service.solve( puzzle: samplePuzzle, - solution: const PuzzleSolution( - id: PuzzleId('pId3'), - win: true, - rated: true, - ), + solution: const PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true), userId: const UserId('testUserId'), ); expect(nbReq, equals(1)); final data = await storage.fetch(userId: const UserId('testUserId')); - expect(data?.solved, equals(IList(const []))); + expect(data?.solved, equals(IList(const []))); expect(data?.unsolved[0].puzzle.id, equals(const PuzzleId('20yWT'))); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); expect(next?.glicko?.rating, equals(1834.54)); expect(next?.glicko?.deviation, equals(23.45)); expect( next?.rounds, - equals( - IList(const [ - PuzzleRound( - id: PuzzleId('pId3'), - ratingDiff: 10, - win: true, - ), - ]), - ), + equals(IList(const [PuzzleRound(id: PuzzleId('pId3'), ratingDiff: 10, win: true)])), ); }); @@ -356,20 +268,14 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); await storage.save( userId: const UserId('testUserId'), - data: _makeUnsolvedPuzzles([ - const PuzzleId('pId3'), - const PuzzleId('pId4'), - ]), + data: _makeUnsolvedPuzzles([const PuzzleId('pId3'), const PuzzleId('pId4')]), ); - const solution = - PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true); + const solution = PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true); final next = await service.solve( puzzle: samplePuzzle, @@ -396,17 +302,12 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); await storage.save( userId: const UserId('testUserId'), - data: _makeUnsolvedPuzzles([ - const PuzzleId('pId3'), - const PuzzleId('pId4'), - ]), + data: _makeUnsolvedPuzzles([const PuzzleId('pId3'), const PuzzleId('pId4')]), ); final next = await service.resetBatch(userId: const UserId('testUserId')); @@ -447,14 +348,8 @@ final samplePuzzle = Puzzle( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ); @@ -477,16 +372,9 @@ PuzzleBatch _makeUnsolvedPuzzles(List ids) { id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), - pgn: - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), + pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ), ]), diff --git a/test/model/puzzle/puzzle_storage_test.dart b/test/model/puzzle/puzzle_storage_test.dart index 5f66c4c43d..07bc10bd64 100644 --- a/test/model/puzzle/puzzle_storage_test.dart +++ b/test/model/puzzle/puzzle_storage_test.dart @@ -12,11 +12,10 @@ import '../../test_container.dart'; void main() { final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); group('PuzzleHistoryStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); + final db = await openAppDatabase(dbFactory, inMemoryDatabasePath); final container = await makeContainer( overrides: [ @@ -27,17 +26,10 @@ void main() { ], ); - final storage = container.read(puzzleStorageProvider); + final storage = await container.read(puzzleStorageProvider.future); - await storage.save( - puzzle: puzzle, - ); - expect( - storage.fetch( - puzzleId: const PuzzleId('pId3'), - ), - completion(equals(puzzle)), - ); + await storage.save(puzzle: puzzle); + expect(storage.fetch(puzzleId: const PuzzleId('pId3')), completion(equals(puzzle))); }); }); } @@ -55,14 +47,8 @@ final puzzle = Puzzle( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ); diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index 5800d76431..98caa64a47 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -1,12 +1,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('RelationRepository.getFollowing', () { @@ -17,10 +17,7 @@ void main() { '''; final mockClient = MockClient((request) { if (request.url.path == '/api/rel/following') { - return mockResponse( - testRelationResponseMinimal, - 200, - ); + return mockResponse(testRelationResponseMinimal, 200); } return mockResponse('', 404); }); @@ -39,10 +36,7 @@ void main() { '''; final mockClient = MockClient((request) { if (request.url.path == '/api/rel/following') { - return mockResponse( - testRelationResponse, - 200, - ); + return mockResponse(testRelationResponse, 200); } return mockResponse('', 404); }); diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart new file mode 100644 index 0000000000..2f709fa087 --- /dev/null +++ b/test/model/study/study_repository_test.dart @@ -0,0 +1,448 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; + +import '../../test_container.dart'; +import '../../test_helpers.dart'; + +void main() { + Future makeTestContainer(MockClient mockClient) async { + return makeContainer( + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + } + + group('StudyRepository.getStudy', () { + test('correctly parse study JSON', () async { + // curl -X GET https://lichess.org/study/JbWtuaeK/7OJXp679\?chapters\=1 -H "Accept: application/json" | sed "s/\\\\n/ /g" | jq 'del(.study.chat)' + const response = ''' +{ + "study": { + "id": "JbWtuaeK", + "name": "How to Solve Puzzles Correctly", + "members": { + "kyle-and-jess": { + "user": { + "name": "Kyle-and-Jess", + "flair": "nature.chipmunk", + "id": "kyle-and-jess" + }, + "role": "w" + }, + "jessieu726": { + "user": { + "name": "jessieu726", + "flair": "nature.duck", + "id": "jessieu726" + }, + "role": "w" + }, + "kyle11878": { + "user": { + "name": "kyle11878", + "flair": "activity.lichess-horsey", + "id": "kyle11878" + }, + "role": "w" + } + }, + "position": { + "chapterId": "EgqyeQIp", + "path": "" + }, + "ownerId": "kyle-and-jess", + "settings": { + "explorer": "contributor", + "description": false, + "computer": "contributor", + "chat": "everyone", + "sticky": false, + "shareable": "contributor", + "cloneable": "contributor" + }, + "visibility": "public", + "createdAt": 1729286237789, + "secondsSinceUpdate": 4116, + "from": "scratch", + "likes": 29, + "flair": "activity.puzzle-piece", + "liked": false, + "features": { + "cloneable": false, + "shareable": false, + "chat": true + }, + "topics": [], + "chapter": { + "id": "7OJXp679", + "ownerId": "kyle-and-jess", + "setup": { + "variant": { + "key": "standard", + "name": "Standard" + }, + "orientation": "black", + "fromFen": true + }, + "tags": [], + "features": { + "computer": false, + "explorer": false + }, + "gamebook": true + }, + "chapters": [ + { + "id": "EgqyeQIp", + "name": "Introduction" + }, + { + "id": "z6tGV47W", + "name": "Practice Your Thought Process", + "fen": "2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20", + "orientation": "black" + }, + { + "id": "dTfxbccx", + "name": "Practice Strategic Thinking", + "fen": "r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23" + }, + { + "id": "B1U4pFdG", + "name": "Calculate Fully", + "fen": "3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26", + "orientation": "black" + }, + { + "id": "NJLW7jil", + "name": "Calculate Freely", + "fen": "4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39", + "orientation": "black" + }, + { + "id": "7OJXp679", + "name": "Use a Timer", + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "orientation": "black" + }, + { + "id": "Rgk6UlTP", + "name": "Understand Your Mistakes", + "fen": "r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26", + "orientation": "black" + }, + { + "id": "VsdxmjCf", + "name": "Adjusting Difficulty", + "fen": "3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1", + "orientation": "black" + }, + { + "id": "FHU6xhYs", + "name": "Using Themes", + "fen": "r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12", + "orientation": "black" + }, + { + "id": "8FhO455h", + "name": "Endurance Training", + "fen": "8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41" + }, + { + "id": "jWUEWsEf", + "name": "Final Thoughts", + "fen": "8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1" + } + ], + "federations": {} + }, + "analysis": { + "game": { + "id": "synthetic", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "opening": null, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "turns": 39, + "player": "black", + "status": { + "id": 10, + "name": "created" + }, + "initialFen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20" + }, + "player": { + "id": null, + "color": "black" + }, + "opponent": { + "color": "white", + "ai": null + }, + "orientation": "black", + "pref": { + "animationDuration": 300, + "coords": 1, + "moveEvent": 2, + "showCaptured": true, + "keyboardMove": false, + "rookCastle": true, + "highlight": true, + "destination": true + }, + "userAnalysis": true, + "treeParts": [ + { + "ply": 39, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "comments": [ + { + "id": "4nZ6", + "text": "Using a timer can be great during puzzle solving, and I don't mean timing yourself to solve quickly. What I mean is setting a timer that restricts when you're allowed to play a move. Start with a minute or two (for more difficult puzzles; if you're solving easy puzzles, you don't need the timer) and calculate the entire time. When you're solving even harder puzzles, set an even longer timer (5-10 minutes maybe). Practice pushing calculations further and looking at different lines during that time (for very difficult puzzles, you should have plenty to calculate). This is to train yourself to take time during important moments, instead of rushing through the position. Set a timer for one to two minutes and calculate this position as black as fully as you can.", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "gamebook": { + "hint": "The white king is not very safe. Can black increase the pressure on the king?" + }, + "dests": "456789 LbktxCESUZ6 wenopvxDEFKMU WGO YIQ 2MU XHP VhpxFNOPQRSTU !9?" + }, + { + "ply": 40, + "fen": "r5k1/ppp2ppp/8/4Nb2/3P4/1QN1PPq1/PP2B1Pr/R4RK1 w - - 2 21", + "id": "R2", + "uci": "h6h2", + "san": "Rh2", + "gamebook": { + "deviation": "Black has to be quick to jump on the initiative of white's king being vulnerable." + } + }, + { + "ply": 41, + "fen": "r5k1/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 b - - 0 21", + "id": "4X", + "uci": "b3f7", + "san": "Qxf7+", + "check": true + }, + { + "ply": 42, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 w - - 1 22", + "id": "ab", + "uci": "g8h8", + "san": "Kh8" + }, + { + "ply": 43, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2BRPr/R5K1 b - - 2 22", + "id": "(0", + "uci": "f1f2", + "san": "Rf2" + }, + { + "ply": 44, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 3 23", + "id": "9B", + "uci": "g3h4", + "san": "Qh4", + "gamebook": { + "deviation": "Keep the initiative going! Go for the king!" + } + }, + { + "ply": 45, + "fen": "r5Qk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 b - - 4 23", + "id": "Xa", + "uci": "f7g8", + "san": "Qg8+", + "check": true, + "children": [ + { + "ply": 46, + "fen": "6rk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "[a", + "uci": "a8g8", + "san": "Rxg8", + "comments": [ + { + "id": "lq80", + "text": "This allows for Nf7#", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "glyphs": [ + { + "id": 4, + "symbol": "??", + "name": "Blunder" + } + ], + "children": [] + } + ] + }, + { + "ply": 46, + "fen": "r5k1/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "ba", + "uci": "h8g8", + "san": "Kxg8", + "comments": [ + { + "id": "sAXm", + "text": "Good job avoiding the smothered mate!", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ] + } + ] + } +} +'''; + + final mockClient = MockClient((request) { + if (request.url.path == '/study/JbWtuaeK/7OJXp679') { + expect(request.url.queryParameters['chapters'], '1'); + return mockResponse(response, 200); + } else if (request.url.path == '/api/study/JbWtuaeK/7OJXp679.pgn') { + return mockResponse('pgn', 200); + } + return mockResponse('', 404); + }); + + final container = await makeTestContainer(mockClient); + final repo = container.read(studyRepositoryProvider); + + final (study, pgn) = await repo.getStudy( + id: const StudyId('JbWtuaeK'), + chapterId: const StudyChapterId('7OJXp679'), + ); + + expect(pgn, 'pgn'); + + expect( + study, + Study( + id: const StudyId('JbWtuaeK'), + name: 'How to Solve Puzzles Correctly', + liked: false, + likes: 29, + ownerId: const UserId('kyle-and-jess'), + features: (cloneable: false, chat: true, sticky: false), + topics: const IList.empty(), + chapters: IList(const [ + StudyChapterMeta(id: StudyChapterId('EgqyeQIp'), name: 'Introduction', fen: null), + StudyChapterMeta( + id: StudyChapterId('z6tGV47W'), + name: 'Practice Your Thought Process', + fen: '2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20', + ), + StudyChapterMeta( + id: StudyChapterId('dTfxbccx'), + name: 'Practice Strategic Thinking', + fen: 'r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23', + ), + StudyChapterMeta( + id: StudyChapterId('B1U4pFdG'), + name: 'Calculate Fully', + fen: '3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26', + ), + StudyChapterMeta( + id: StudyChapterId('NJLW7jil'), + name: 'Calculate Freely', + fen: '4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39', + ), + StudyChapterMeta( + id: StudyChapterId('7OJXp679'), + name: 'Use a Timer', + fen: 'r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20', + ), + StudyChapterMeta( + id: StudyChapterId('Rgk6UlTP'), + name: 'Understand Your Mistakes', + fen: 'r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26', + ), + StudyChapterMeta( + id: StudyChapterId('VsdxmjCf'), + name: 'Adjusting Difficulty', + fen: '3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1', + ), + StudyChapterMeta( + id: StudyChapterId('FHU6xhYs'), + name: 'Using Themes', + fen: 'r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12', + ), + StudyChapterMeta( + id: StudyChapterId('8FhO455h'), + name: 'Endurance Training', + fen: '8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41', + ), + StudyChapterMeta( + id: StudyChapterId('jWUEWsEf'), + name: 'Final Thoughts', + fen: '8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1', + ), + ]), + chapter: const StudyChapter( + id: StudyChapterId('7OJXp679'), + setup: StudyChapterSetup( + id: null, + orientation: Side.black, + variant: Variant.standard, + fromFen: true, + ), + practise: false, + conceal: null, + gamebook: true, + features: (computer: false, explorer: false), + ), + hints: + [ + 'The white king is not very safe. Can black increase the pressure on the king?', + null, + null, + null, + null, + null, + null, + null, + ].lock, + deviationComments: + [ + null, + "Black has to be quick to jump on the initiative of white's king being vulnerable.", + null, + null, + null, + 'Keep the initiative going! Go for the king!', + null, + null, + ].lock, + ), + ); + }); + }); +} diff --git a/test/model/tv/tv_repository_test.dart b/test/model/tv/tv_repository_test.dart index ff5f577df2..01cdc0dc2e 100644 --- a/test/model/tv/tv_repository_test.dart +++ b/test/model/tv/tv_repository_test.dart @@ -3,7 +3,7 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('TvRepository.channels', () { @@ -166,10 +166,7 @@ void main() { final mockClient = MockClient((request) { if (request.url.path == '/api/tv/channels') { - return mockResponse( - response, - 200, - ); + return mockResponse(response, 200); } return mockResponse('', 404); }); @@ -183,10 +180,7 @@ void main() { // supported channels only expect(result.length, 13); - expect( - result[TvChannel.best]?.user.name, - 'Chessisnotfair', - ); + expect(result[TvChannel.best]?.user.name, 'Chessisnotfair'); }); }); } diff --git a/test/model/user/user_repository_test.dart b/test/model/user/user_repository_test.dart index 3a420e19fd..afae7b35c0 100644 --- a/test/model/user/user_repository_test.dart +++ b/test/model/user/user_repository_test.dart @@ -1,15 +1,15 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; const testUserId = UserId('test'); @@ -18,8 +18,7 @@ void main() { test('json read, minimal example', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/user/$testUserId') { - return mockResponse( - ''' + return mockResponse(''' { "id": "$testUserId", "username": "$testUserId", @@ -28,9 +27,7 @@ void main() { "perfs": { } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -48,8 +45,7 @@ void main() { test('json read, full example', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/user/$testUserId') { - return mockResponse( - ''' + return mockResponse(''' { "id": "$testUserId", "username": "$testUserId", @@ -87,9 +83,7 @@ void main() { "links": "http://test.com" } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -113,8 +107,7 @@ void main() { test('json read, minimal example', () async { final mockClient = MockClient((request) { if (request.url.path == path) { - return mockResponse( - ''' + return mockResponse(''' { "user": { "name": "$testUserId" @@ -143,9 +136,7 @@ void main() { } } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -163,8 +154,7 @@ void main() { test('json read, full example', () async { final mockClient = MockClient((request) { if (request.url.path == path) { - return mockResponse( - ''' + return mockResponse(''' { "user": { "name": "testOpponentName" @@ -400,9 +390,7 @@ void main() { } } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -423,16 +411,11 @@ void main() { group('UserRepository.getUsersStatuses', () { test('json read, minimal example', () async { - final ids = ISet( - const {UserId('maia1'), UserId('maia5'), UserId('maia9')}, - ); + final ids = ISet(const {UserId('maia1'), UserId('maia5'), UserId('maia9')}); final mockClient = MockClient((request) { if (request.url.path == '/api/users/status') { - return mockResponse( - '[]', - 200, - ); + return mockResponse('[]', 200); } return mockResponse('', 404); }); @@ -447,13 +430,10 @@ void main() { }); test('json read, full example', () async { - final ids = ISet( - const {UserId('maia1'), UserId('maia5'), UserId('maia9')}, - ); + final ids = ISet(const {UserId('maia1'), UserId('maia5'), UserId('maia9')}); final mockClient = MockClient((request) { if (request.url.path == '/api/users/status') { - return mockResponse( - ''' + return mockResponse(''' [ { "id": "maia1", @@ -472,9 +452,7 @@ void main() { "online": true } ] -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); diff --git a/test/network/fake_http_client_factory.dart b/test/network/fake_http_client_factory.dart new file mode 100644 index 0000000000..3b07ad48bc --- /dev/null +++ b/test/network/fake_http_client_factory.dart @@ -0,0 +1,13 @@ +import 'package:http/http.dart' as http; +import 'package:lichess_mobile/src/network/http.dart'; + +class FakeHttpClientFactory implements HttpClientFactory { + const FakeHttpClientFactory(this._factory); + + final http.Client Function() _factory; + + @override + http.Client call() { + return _factory(); + } +} diff --git a/test/model/common/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart similarity index 69% rename from test/model/common/fake_websocket_channel.dart rename to test/network/fake_websocket_channel.dart index 8010b1203f..edc6b66814 100644 --- a/test/model/common/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:async/src/stream_sink_transformer.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class FakeWebSocketChannelFactory implements WebSocketChannelFactory { - final FutureOr Function() createFunction; + final FutureOr Function(String url) createFunction; const FakeWebSocketChannelFactory(this.createFunction); @@ -17,10 +17,7 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 1), }) async { - // in the real implementation the channel is returned after the [WebSocket] - // is connected, so we need to simulate this delay - await Future.delayed(const Duration(milliseconds: 5)); - return createFunction(); + return createFunction(url); } } @@ -33,11 +30,19 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { /// behavior can be changed by setting [shouldSendPong] to false. /// /// It also allows to increase the lag of the connection by setting the -/// [connectionLag] property. +/// [connectionLag] property. By default [connectionLag] is set to [Duration.zero] +/// to simplify testing. +/// When lag is 0, the pong response will be sent in the next microtask. /// /// The [sentMessages] and [sentMessagesExceptPing] streams can be used to /// verify that the client sends the expected messages. class FakeWebSocketChannel implements WebSocketChannel { + FakeWebSocketChannel({this.connectionLag = Duration.zero}); + + int _pongCount = 0; + + final _connectionCompleter = Completer(); + static bool isPing(dynamic data) { if (data is! String) { return false; @@ -58,13 +63,19 @@ class FakeWebSocketChannel implements WebSocketChannel { /// The controller for outgoing (to server) messages. final _outcomingController = StreamController.broadcast(); + /// The lag of the connection (duration before pong response) in milliseconds. + Duration connectionLag; + /// Whether the server should send a pong response to a ping request. /// /// Can be used to simulate a faulty connection. bool shouldSendPong = true; - /// The lag of the connection (duration before pong response) in milliseconds. - Duration connectionLag = const Duration(milliseconds: 10); + /// Number of pong response received + int get pongCount => _pongCount; + + /// A Future that resolves when the first pong message is received + Future get connectionEstablished => _connectionCompleter.future; /// The stream of all outgoing messages. Stream get sentMessages => _outcomingController.stream; @@ -74,10 +85,11 @@ class FakeWebSocketChannel implements WebSocketChannel { _outcomingController.stream.where((message) => !isPing(message)); /// Simulates incoming messages from the server. - Future addIncomingMessages(Iterable messages) async { - await Future.delayed(const Duration(milliseconds: 5)); - return _incomingController - .addStream(Stream.fromIterable(messages)); + void addIncomingMessages(Iterable messages) { + for (final message in messages) { + _incomingController.add(message); + } + // await _incomingController.addStream(Stream.fromIterable(messages)); } @override @@ -102,25 +114,19 @@ class FakeWebSocketChannel implements WebSocketChannel { void pipe(StreamChannel other) {} @override - StreamChannel transform( - StreamChannelTransformer transformer, - ) { + StreamChannel transform(StreamChannelTransformer transformer) { // TODO: implement transform throw UnimplementedError(); } @override - StreamChannel transformSink( - StreamSinkTransformer transformer, - ) { + StreamChannel transformSink(StreamSinkTransformer transformer) { // TODO: implement transformSink throw UnimplementedError(); } @override - StreamChannel transformStream( - StreamTransformer transformer, - ) { + StreamChannel transformStream(StreamTransformer transformer) { // TODO: implement transformStream throw UnimplementedError(); } @@ -132,17 +138,13 @@ class FakeWebSocketChannel implements WebSocketChannel { } @override - StreamChannel changeSink( - StreamSink Function(StreamSink p1) change, - ) { + StreamChannel changeSink(StreamSink Function(StreamSink p1) change) { // TODO: implement changeSink throw UnimplementedError(); } @override - StreamChannel changeStream( - Stream Function(Stream p1) change, - ) { + StreamChannel changeStream(Stream Function(Stream p1) change) { // TODO: implement changeStream throw UnimplementedError(); } @@ -159,12 +161,22 @@ class FakeWebSocketSink implements WebSocketSink { // Simulates pong response if connection is not closed if (_channel.shouldSendPong && FakeWebSocketChannel.isPing(data)) { - Future.delayed(_channel.connectionLag, () { + void sendPong() { if (_channel._incomingController.isClosed) { return; } + _channel._pongCount++; + if (_channel._pongCount == 1) { + _channel._connectionCompleter.complete(); + } _channel._incomingController.add('0'); - }); + } + + if (_channel.connectionLag > Duration.zero) { + Future.delayed(_channel.connectionLag, sendPong); + } else { + scheduleMicrotask(sendPong); + } } } @@ -178,9 +190,7 @@ class FakeWebSocketSink implements WebSocketSink { @override Future close([int? closeCode, String? closeReason]) { - return Future.wait([ - _channel._incomingController.close(), - ]); + return Future.wait([_channel._incomingController.close()]); } @override diff --git a/test/network/http_test.dart b/test/network/http_test.dart new file mode 100644 index 0000000000..c9b775b574 --- /dev/null +++ b/test/network/http_test.dart @@ -0,0 +1,392 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/bearer.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; + +import '../test_container.dart'; +import 'fake_http_client_factory.dart'; + +void main() { + setUp(() { + FakeClient.reset(); + }); + + group('LichessClient', () { + test('sends requests to lichess host', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + final response = await client.get(Uri(path: '/test')); + expect(response.statusCode, 200); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA() + .having((r) => r.url.path, 'path', '/test') + .having((r) => r.url.host, 'host', 'lichess.dev') + .having((r) => r.url.scheme, 'scheme', 'https'), + ); + }); + + test('sets user agent (no session)', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA().having( + (r) => r.headers['User-Agent'], + 'User-Agent', + 'Lichess Mobile/0.0.0 as:anon sri:test-sri', + ), + ); + }); + + test('sets user agent (with session)', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA().having( + (r) => r.headers['User-Agent'], + 'User-Agent', + 'Lichess Mobile/0.0.0 as:test-user-id sri:test-sri', + ), + ); + }); + + test('read methods throw ServerException on status code >= 400', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + for (final method in [ + client.read, + client.readBytes, + (Uri url) => client.readJson(url, mapper: (json) => json), + (Uri url) => client.readJsonList(url, mapper: (json) => json), + (Uri url) => client.readNdJsonList(url, mapper: (json) => json), + (Uri url) => client.readNdJsonStream(url, mapper: (json) => json), + ]) { + expect( + () => method(Uri(path: '/will/return/500')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 500)), + ); + expect( + () => method(Uri(path: '/will/return/503')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 503)), + ); + expect( + () => method(Uri(path: '/will/return/400')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 400)), + ); + expect( + () => method(Uri(path: '/will/return/404')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 404)), + ); + expect( + () => method(Uri(path: '/will/return/401')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 401)), + ); + expect( + () => method(Uri(path: '/will/return/403')), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 403)), + ); + } + }); + + test('other methods do not throw on status code >= 400', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + for (final method in [client.get, client.post, client.put, client.patch, client.delete]) { + expect(() => method(Uri(path: '/will/return/500')), returnsNormally); + expect(() => method(Uri(path: '/will/return/503')), returnsNormally); + expect(() => method(Uri(path: '/will/return/400')), returnsNormally); + expect(() => method(Uri(path: '/will/return/404')), returnsNormally); + expect(() => method(Uri(path: '/will/return/401')), returnsNormally); + expect(() => method(Uri(path: '/will/return/403')), returnsNormally); + } + }); + + test('socket and tls errors do not throw ClientException', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + expect( + () => client.get(Uri(path: '/will/throw/socket/exception')), + throwsA(isA().having((e) => e.message, 'message', 'no internet')), + ); + expect( + () => client.get(Uri(path: '/will/throw/tls/exception')), + throwsA(isA().having((e) => e.message, 'message', 'tls error')), + ); + }); + + test('failed JSON parsing will throw ClientException', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + expect( + () => client.readJson( + Uri(path: '/will/return/204'), + mapper: (json) { + return json; + }, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Could not read JSON object as Map: expected an object.', + ), + ), + ); + }); + + test('adds a signed bearer token when a session is available the request', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); + }); + + test( + 'when receiving a 401, will test session token and delete session if not valid anymore', + () async { + int nbTokenTestRequests = 0; + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + defaultClientProvider.overrideWith((ref) { + return MockClient((request) async { + if (request.url.path == '/api/token/test') { + nbTokenTestRequests++; + final token = request.body.split(',')[0]; + final response = '{"$token": null}'; + return http.Response(response, 200); + } + return http.Response('', 404); + }); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + fakeAsync((async) { + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + try { + client.get(Uri(path: '/will/return/401')); + } on ServerException catch (_) {} + + async.flushMicrotasks(); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); + + expect(nbTokenTestRequests, 1); + + expect(container.read(authSessionProvider), isNull); + }); + }, + ); + + test('when receiving a 401, will test session token and keep session if still valid', () async { + int nbTokenTestRequests = 0; + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + defaultClientProvider.overrideWith((ref) { + return MockClient((request) async { + if (request.url.path == '/api/token/test') { + nbTokenTestRequests++; + final token = request.body.split(',')[0]; + final response = + '{"$token": {"userId": "test-user-id","scope": "web:mobile", "expires":1760704968038}}'; + return http.Response(response, 200); + } + return http.Response('', 404); + }); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + fakeAsync((async) { + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + try { + client.get(Uri(path: '/will/return/401')); + } on ServerException catch (_) {} + + async.flushMicrotasks(); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); + + expect(nbTokenTestRequests, 1); + + expect(container.read(authSessionProvider), equals(session)); + }); + }); + }); +} + +class FakeClient extends http.BaseClient { + static List _requests = []; + + static List verifyRequests() { + final result = _requests; + _requests = []; + return result; + } + + static void reset() { + _requests = []; + } + + @override + Future send(http.BaseRequest request) { + _requests.add(request); + + return _responseBasedOnPath(request, request.finalize()); + } + + Future _responseBasedOnPath( + http.BaseRequest request, + http.ByteStream bodyStream, + ) async { + switch (request.url.path) { + case '/will/throw/socket/exception': + throw const SocketException('no internet'); + case '/will/throw/tls/exception': + throw const TlsException('tls error'); + case '/will/return/500': + return http.StreamedResponse(_streamBody('500'), 500); + case '/will/return/503': + return http.StreamedResponse(_streamBody('503'), 503); + case '/will/return/400': + return http.StreamedResponse(_streamBody('400'), 400); + case '/will/return/404': + return http.StreamedResponse(_streamBody('404'), 404); + case '/will/return/401': + return http.StreamedResponse(_streamBody('401'), 401); + case '/will/return/403': + return http.StreamedResponse(_streamBody('403'), 403); + case '/will/return/204': + return http.StreamedResponse(_streamBody('204'), 204); + case '/will/return/301': + return http.StreamedResponse(_streamBody('301'), 301); + default: + return http.StreamedResponse(_streamBody('200'), 200); + } + } +} + +Stream> _streamBody(String body) => Stream.value(utf8.encode(body)); diff --git a/test/model/common/socket_test.dart b/test/network/socket_test.dart similarity index 81% rename from test/model/common/socket_test.dart rename to test/network/socket_test.dart index 04fdef8e4c..bf9d43595d 100644 --- a/test/model/common/socket_test.dart +++ b/test/network/socket_test.dart @@ -2,12 +2,12 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'fake_websocket_channel.dart'; -SocketClient _makeSocketClient(FakeWebSocketChannelFactory fakeChannelFactory) { +SocketClient makeTestSocketClient(FakeWebSocketChannelFactory fakeChannelFactory) { final client = SocketClient( Uri(path: kDefaultSocketRoute), channelFactory: fakeChannelFactory, @@ -42,8 +42,7 @@ void main() { test('handles ping/pong', () async { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); int sentPingCount = 0; @@ -67,7 +66,7 @@ void main() { test('reconnects when connection attempt fails', () async { int numConnectionAttempts = 0; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; if (numConnectionAttempts == 1) { throw const SocketException('Connection failed'); @@ -75,7 +74,7 @@ void main() { return FakeWebSocketChannel(); }); - final socketClient = _makeSocketClient(fakeChannelFactory); + final socketClient = makeTestSocketClient(fakeChannelFactory); socketClient.connect(); // The first connection attempt will fail, but the second one will succeed @@ -93,7 +92,7 @@ void main() { // channels per connection attempt final Map channels = {}; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; final channel = FakeWebSocketChannel(); int sentPingCount = 0; @@ -111,7 +110,7 @@ void main() { return channel; }); - final socketClient = _makeSocketClient(fakeChannelFactory); + final socketClient = makeTestSocketClient(fakeChannelFactory); socketClient.connect(); await socketClient.firstConnection; @@ -131,10 +130,9 @@ void main() { }); test('computes average lag', () async { - final fakeChannel = FakeWebSocketChannel(); + final fakeChannel = FakeWebSocketChannel(connectionLag: const Duration(milliseconds: 10)); - final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); // before the connection is ready the average lag is zero @@ -150,19 +148,13 @@ void main() { await expectLater(fakeChannel.stream, emits('0')); // after the ping/pong exchange the average lag is computed - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(10), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(10)); // wait for more ping/pong exchanges await expectLater(fakeChannel.stream, emitsInOrder(['0', '0', '0', '0'])); // average lag is still the same - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(10), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(10)); // increase the lag of the connection fakeChannel.connectionLag = const Duration(milliseconds: 100); @@ -171,10 +163,7 @@ void main() { await expectLater(fakeChannel.stream, emitsInOrder(['0', '0', '0', '0'])); // average lag should be higher - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(40), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(40)); await socketClient.close(); @@ -185,8 +174,7 @@ void main() { test('handles ackable messages', () async { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); await socketClient.firstConnection; @@ -205,13 +193,10 @@ void main() { ); // server acks the message - await fakeChannel.addIncomingMessages(['{"t":"ack","d":1}']); + fakeChannel.addIncomingMessages(['{"t":"ack","d":1}']); // no more messages are expected - await expectLater( - fakeChannel.sentMessagesExceptPing, - emitsInOrder([]), - ); + await expectLater(fakeChannel.sentMessagesExceptPing, emitsInOrder([])); socketClient.close(); }); diff --git a/test/test_app.dart b/test/test_app.dart deleted file mode 100644 index d8a39d3a02..0000000000 --- a/test/test_app.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:lichess_mobile/src/crashlytics.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -import './fake_crashlytics.dart'; -import './model/auth/fake_session_storage.dart'; -import './model/common/service/fake_sound_service.dart'; -import 'fake_notification_service.dart'; -import 'model/common/fake_websocket_channel.dart'; -import 'model/game/mock_game_storage.dart'; -import 'utils/fake_connectivity_changes.dart'; - -class MockDatabase extends Mock implements Database {} - -final mockClient = MockClient((request) async { - return http.Response('', 200); -}); - -// iPhone 14 screen size -const double _kTestScreenWidth = 390.0; -const double _kTestScreenHeight = 844.0; -const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); - -Future buildTestApp( - WidgetTester tester, { - required Widget home, - List? overrides, - AuthSessionState? userSession, -}) async { - await tester.binding.setSurfaceSize(kTestSurfaceSize); - - VisibilityDetectorController.instance.updateInterval = Duration.zero; - - SharedPreferences.setMockInitialValues({}); - - final sharedPreferences = await SharedPreferences.getInstance(); - - FlutterSecureStorage.setMockInitialValues({ - kSRIStorageKey: 'test', - }); - - // TODO consider loading true fonts as well - FlutterError.onError = _ignoreOverflowErrors; - - Logger.root.onRecord.listen((record) { - if (record.level > Level.WARNING) { - final time = DateFormat.Hms().format(record.time); - debugPrint( - '${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}', - ); - } - }); - - return ProviderScope( - overrides: [ - // ignore: scoped_providers_should_specify_dependencies - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - // ignore: scoped_providers_should_specify_dependencies - defaultClientProvider.overrideWith((_) { - return mockClient; - }), - // ignore: scoped_providers_should_specify_dependencies - webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); - }), - // ignore: scoped_providers_should_specify_dependencies - socketPoolProvider.overrideWith((ref) { - final pool = SocketPool(ref); - ref.onDispose(pool.dispose); - return pool; - }), - // ignore: scoped_providers_should_specify_dependencies - connectivityChangesProvider.overrideWith(() { - return FakeConnectivityChanges(); - }), - // ignore: scoped_providers_should_specify_dependencies - showRatingsPrefProvider.overrideWith((ref) { - return true; - }), - // ignore: scoped_providers_should_specify_dependencies - notificationServiceProvider.overrideWithValue(FakeNotificationService()), - // ignore: scoped_providers_should_specify_dependencies - crashlyticsProvider.overrideWithValue(FakeCrashlytics()), - // ignore: scoped_providers_should_specify_dependencies - soundServiceProvider.overrideWithValue(FakeSoundService()), - // ignore: scoped_providers_should_specify_dependencies - sharedPreferencesProvider.overrideWithValue(sharedPreferences), - // ignore: scoped_providers_should_specify_dependencies - sessionStorageProvider.overrideWithValue(FakeSessionStorage(userSession)), - // ignore: scoped_providers_should_specify_dependencies - gameStorageProvider.overrideWithValue(MockGameStorage()), - // ignore: scoped_providers_should_specify_dependencies - appInitializationProvider.overrideWith((ref) { - return AppInitializationData( - packageInfo: PackageInfo( - appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', - packageName: 'lichess_mobile_test', - ), - deviceInfo: BaseDeviceInfo({ - 'name': 'test', - 'model': 'test', - 'manufacturer': 'test', - 'systemName': 'test', - 'systemVersion': 'test', - 'identifierForVendor': 'test', - 'isPhysicalDevice': true, - }), - sharedPreferences: sharedPreferences, - userSession: userSession, - database: MockDatabase(), - sri: 'test', - engineMaxMemoryInMb: 16, - ); - }), - ...overrides ?? [], - ], - // simplified version of class [App] in lib/src/app.dart - child: Consumer( - builder: (context, ref, child) { - return MediaQuery( - data: const MediaQueryData(size: kTestSurfaceSize), - child: Center( - child: SizedBox( - width: _kTestScreenWidth, - height: _kTestScreenHeight, - child: MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: home, - builder: (context, child) { - return CupertinoTheme( - data: const CupertinoThemeData(), - child: Material(child: child), - ); - }, - ), - ), - ), - ); - }, - ), - ); -} - -void _ignoreOverflowErrors( - FlutterErrorDetails details, { - bool forceReport = false, -}) { - bool isOverflowError = false; - final exception = details.exception; - - if (exception is FlutterError) { - isOverflowError = exception.diagnostics - .any((e) => e.value.toString().contains('A RenderFlex overflowed by')); - } - - if (isOverflowError) { - // debugPrint('Overflow error detected.'); - } else { - FlutterError.dumpErrorToConsole(details, forceReport: forceReport); - throw exception; - } -} diff --git a/test/test_container.dart b/test/test_container.dart index adb11bddb6..435d415720 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -1,47 +1,42 @@ import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; -import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; -import 'fake_notification_service.dart'; -import 'model/common/fake_websocket_channel.dart'; -import 'utils/fake_connectivity_changes.dart'; +import 'binding.dart'; +import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_http_client_factory.dart'; +import 'network/fake_websocket_channel.dart'; +import 'utils/fake_connectivity.dart'; -class MockDatabase extends Mock implements Database {} +/// A mock client that always returns a 200 empty response. +final testContainerMockClient = MockClient((request) async { + return http.Response('', 200); +}); -class MockHttpClient extends Mock implements http.Client {} - -const shouldLog = false; - -/// Returns a [ProviderContainer] with a mocked [LichessClient] configured with -/// the given [mockClient]. +/// Returns a [ProviderContainer] with the [httpClientFactoryProvider] configured +/// with the given [mockClient]. Future lichessClientContainer(MockClient mockClient) async { return makeContainer( overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); }), ], ); @@ -52,50 +47,43 @@ Future makeContainer({ List? overrides, AuthSessionState? userSession, }) async { - SharedPreferences.setMockInitialValues({}); - final sharedPreferences = await SharedPreferences.getInstance(); + final binding = TestLichessBinding.ensureInitialized(); - FlutterSecureStorage.setMockInitialValues({ - kSRIStorageKey: 'test', - }); - - Logger.root.onRecord.listen((record) { - if (shouldLog && record.level >= Level.FINE) { - final time = DateFormat.Hms().format(record.time); - debugPrint( - '${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}', - ); - } - }); + FlutterSecureStorage.setMockInitialValues({kSRIStorageKey: 'test'}); final container = ProviderContainer( overrides: [ + connectivityPluginProvider.overrideWith((_) { + return FakeConnectivity(); + }), + notificationDisplayProvider.overrideWith((ref) { + return FakeNotificationDisplay(); + }), + databaseProvider.overrideWith((ref) async { + final db = await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); + ref.onDispose(db.close); + return db; + }), webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); }), socketPoolProvider.overrideWith((ref) { final pool = SocketPool(ref); ref.onDispose(pool.dispose); return pool; }), - lichessClientProvider.overrideWith((ref) { - return LichessClient(MockHttpClient(), ref); - }), - connectivityChangesProvider.overrideWith(() { - return FakeConnectivityChanges(); + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => testContainerMockClient); }), - defaultClientProvider.overrideWithValue(MockHttpClient()), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), - notificationServiceProvider.overrideWithValue(FakeNotificationService()), soundServiceProvider.overrideWithValue(FakeSoundService()), - sharedPreferencesProvider.overrideWithValue(sharedPreferences), - sessionStorageProvider.overrideWithValue(FakeSessionStorage()), - appInitializationProvider.overrideWith((ref) { - return AppInitializationData( + preloadedDataProvider.overrideWith((ref) { + return ( + sri: 'test-sri', packageInfo: PackageInfo( appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', + version: '0.0.0', + buildNumber: '0', packageName: 'lichess_mobile_test', ), deviceInfo: BaseDeviceInfo({ @@ -107,17 +95,15 @@ Future makeContainer({ 'identifierForVendor': 'test', 'isPhysicalDevice': true, }), - sharedPreferences: sharedPreferences, userSession: userSession, - database: MockDatabase(), - sri: 'test', - engineMaxMemoryInMb: 16, + engineMaxMemoryInMb: 256, ); }), ...overrides ?? [], ], ); + addTearDown(binding.reset); addTearDown(container.dispose); return container; diff --git a/test/test_utils.dart b/test/test_helpers.dart similarity index 51% rename from test/test_utils.dart rename to test/test_helpers.dart index e03dac1d02..5dae185a43 100644 --- a/test/test_utils.dart +++ b/test/test_helpers.dart @@ -1,52 +1,82 @@ import 'dart:convert'; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; -const kPlatformVariant = - TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}); +const double _kTestScreenWidth = 390.0; +const double _kTestScreenHeight = 844.0; + +const kTestSurfaces = [ + // https://www.browserstack.com/guide/common-screen-resolutions + // phones + Size(360, 800), + Size(390, 844), + Size(393, 873), + Size(412, 915), + Size(414, 896), + Size(360, 780), + // tablets + Size(600, 1024), + Size(810, 1080), + Size(820, 1180), + Size(1280, 800), + Size(800, 1280), + Size(601, 962), + // folded motorola + Size(564.7, 482.6), + // pixel fold unfolded + Size(701.0, 841.1), + Size(841.1, 701.0), +]; + +/// iPhone 14 screen size. +const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); + +const kPlatformVariant = TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}); Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); -Future delayedAnswer(T value) => - Future.delayed(const Duration(milliseconds: 5)).then((_) => value); +/// Mocks a surface with a given size. +class TestSurface extends StatelessWidget { + const TestSurface({required this.child, required this.size, super.key}); + final Size size; + final Widget child; + + @override + Widget build(BuildContext context) { + return MediaQuery( + data: MediaQueryData(size: size), + child: SizedBox(width: size.width, height: size.height, child: child), + ); + } +} + +/// Mocks an http response Future mockResponse( String body, int code, { Map headers = const {}, -}) => - Future.delayed(const Duration(milliseconds: 20)).then( - (_) => http.Response( - body, - code, - headers: headers, - ), - ); +}) => Future.value(http.Response(body, code, headers: headers)); Future mockStreamedResponse(String body, int code) => - Future.delayed(const Duration(milliseconds: 20)).then( - (_) => http.StreamedResponse(Stream.value(body).map(utf8.encode), code), - ); + Future.value(http.StreamedResponse(Stream.value(body).map(utf8.encode), code)); -Future mockHttpStreamFromIterable( - Iterable events, -) async { - await Future.delayed(const Duration(milliseconds: 20)); +Future mockHttpStreamFromIterable(Iterable events) async { return http.StreamedResponse( - _streamFromFutures(events.map((e) => _withDelay(utf8.encode(e)))), + _streamFromFutures(events.map((e) => Future.value(utf8.encode(e)))), 200, ); } Future mockHttpStream(Stream stream) => - Future.delayed(const Duration(milliseconds: 20)) - .then((_) => http.StreamedResponse(stream.map(utf8.encode), 200)); + Future.value(http.StreamedResponse(stream.map(utf8.encode), 200)); Future tapBackButton(WidgetTester tester) async { if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { @@ -64,29 +94,28 @@ Future meetsTapTargetGuideline(WidgetTester tester) async { } } -Offset squareOffset( - cg.SquareId id, - Rect boardRect, { - cg.Side orientation = cg.Side.white, -}) { +/// Returns the offset of a square on a board defined by [Rect]. +Offset squareOffset(Square square, Rect boardRect, {Side orientation = Side.white}) { final squareSize = boardRect.width / 8; - final o = cg.Coord.fromSquareId(id).offset(orientation, squareSize); - return Offset( - o.dx + boardRect.left + squareSize / 2, - o.dy + boardRect.top + squareSize / 2, - ); + + final dx = (orientation == Side.white ? square.file.value : 7 - square.file.value) * squareSize; + final dy = (orientation == Side.white ? 7 - square.rank.value : square.rank.value) * squareSize; + + return Offset(dx + boardRect.left + squareSize / 2, dy + boardRect.top + squareSize / 2); } +/// Plays a move on the board. Future playMove( WidgetTester tester, - Rect boardRect, String from, String to, { - cg.Side orientation = cg.Side.white, + Rect? boardRect, + Side orientation = Side.white, }) async { - await tester.tapAt(squareOffset(from, boardRect, orientation: orientation)); + final rect = boardRect ?? tester.getRect(find.byType(Chessboard)); + await tester.tapAt(squareOffset(Square.fromName(from), rect, orientation: orientation)); await tester.pump(); - await tester.tapAt(squareOffset(to, boardRect, orientation: orientation)); + await tester.tapAt(squareOffset(Square.fromName(to), rect, orientation: orientation)); await tester.pump(); } @@ -128,9 +157,3 @@ Stream _streamFromFutures(Iterable> futures) async* { yield result; } } - -Future _withDelay( - T value, { - Duration delay = const Duration(milliseconds: 10), -}) => - Future.delayed(delay).then((_) => value); diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart new file mode 100644 index 0000000000..c025467349 --- /dev/null +++ b/test/test_provider_scope.dart @@ -0,0 +1,231 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/crashlytics.dart'; +import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/session_storage.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +import './fake_crashlytics.dart'; +import './model/common/service/fake_sound_service.dart'; +import 'binding.dart'; +import 'model/analysis/fake_opening_service.dart'; +import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_http_client_factory.dart'; +import 'network/fake_websocket_channel.dart'; +import 'test_helpers.dart'; +import 'utils/fake_connectivity.dart'; + +final mockClient = MockClient((request) async { + return http.Response('', 200); +}); + +final offlineClient = MockClient((request) async { + throw const SocketException('No internet'); +}); + +/// Returns a [MaterialApp] wrapped in a [ProviderScope] and default mocks, ready for testing. +/// +/// The [home] widget is the widget we want to test. Typically a screen widget, to +/// perform end-to-end tests. +/// It will be wrapped in a [MaterialApp] to simulate a simple app. +/// +/// The [overrides] parameter can be used to override any provider in the app. +/// The [userSession] parameter can be used to set the initial user session state. +/// The [defaultPreferences] parameter can be used to set the initial shared preferences. +Future makeTestProviderScopeApp( + WidgetTester tester, { + required Widget home, + List? overrides, + AuthSessionState? userSession, + Map? defaultPreferences, +}) async { + return makeTestProviderScope( + tester, + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: home, + builder: (context, child) { + return CupertinoTheme(data: const CupertinoThemeData(), child: Material(child: child)); + }, + ), + overrides: overrides, + userSession: userSession, + defaultPreferences: defaultPreferences, + ); +} + +/// Wraps [makeTestProviderScope] with a [FakeHttpClientFactory] that returns an offline client. +/// +/// This is useful to test the app in offline mode. +Future makeOfflineTestProviderScope( + WidgetTester tester, { + required Widget child, + List? overrides, + AuthSessionState? userSession, + Map? defaultPreferences, +}) => makeTestProviderScope( + tester, + child: child, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => offlineClient); + }), + ...overrides ?? [], + ], + userSession: userSession, + defaultPreferences: defaultPreferences, +); + +/// Returns a [ProviderScope] and default mocks, ready for testing. +/// +/// The [child] widget is the widget we want to test. It will be wrapped in a +/// [MediaQuery.new] widget, to simulate a device with a specific size, controlled +/// by [surfaceSize] (which default to [kTestSurfaceSize]). +/// +/// The [overrides] parameter can be used to override any provider in the app. +/// The [userSession] parameter can be used to set the initial user session state. +/// The [defaultPreferences] parameter can be used to set the initial shared preferences. +Future makeTestProviderScope( + WidgetTester tester, { + required Widget child, + List? overrides, + AuthSessionState? userSession, + Map? defaultPreferences, + Size surfaceSize = kTestSurfaceSize, + Key? key, +}) async { + final binding = TestLichessBinding.ensureInitialized(); + + addTearDown(binding.reset); + + await tester.binding.setSurfaceSize(surfaceSize); + addTearDown(() { + tester.binding.setSurfaceSize(null); + }); + + VisibilityDetectorController.instance.updateInterval = Duration.zero; + + // disable piece animation to simplify tests + final defaultBoardPref = { + 'preferences.board': jsonEncode(BoardPrefs.defaults.copyWith(pieceAnimation: false).toJson()), + }; + + await binding.setInitialSharedPreferencesValues( + defaultPreferences != null ? {...defaultBoardPref, ...defaultPreferences} : defaultBoardPref, + ); + + FlutterSecureStorage.setMockInitialValues({ + kSRIStorageKey: 'test', + if (userSession != null) kSessionStorageKey: jsonEncode(userSession.toJson()), + }); + + final flutterTestOnError = FlutterError.onError!; + + void ignoreOverflowErrors(FlutterErrorDetails details) { + bool isOverflowError = false; + final exception = details.exception; + + if (exception is FlutterError) { + isOverflowError = exception.diagnostics.any( + (e) => e.value.toString().contains('A RenderFlex overflowed by'), + ); + } + + if (isOverflowError) { + // debugPrint('Overflow error detected.'); + } else { + flutterTestOnError(details); + } + } + + // TODO consider loading true fonts as well + FlutterError.onError = ignoreOverflowErrors; + + return ProviderScope( + key: key, + overrides: [ + // ignore: scoped_providers_should_specify_dependencies + notificationDisplayProvider.overrideWith((ref) { + return FakeNotificationDisplay(); + }), + // ignore: scoped_providers_should_specify_dependencies + databaseProvider.overrideWith((ref) async { + final testDb = await openAppDatabase(databaseFactoryFfiNoIsolate, inMemoryDatabasePath); + ref.onDispose(testDb.close); + return testDb; + }), + // ignore: scoped_providers_should_specify_dependencies + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + // ignore: scoped_providers_should_specify_dependencies + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); + }), + // ignore: scoped_providers_should_specify_dependencies + socketPoolProvider.overrideWith((ref) { + final pool = SocketPool(ref); + ref.onDispose(pool.dispose); + return pool; + }), + // ignore: scoped_providers_should_specify_dependencies + connectivityPluginProvider.overrideWith((_) => FakeConnectivity()), + // ignore: scoped_providers_should_specify_dependencies + showRatingsPrefProvider.overrideWith((ref) => true), + // ignore: scoped_providers_should_specify_dependencies + crashlyticsProvider.overrideWithValue(FakeCrashlytics()), + // ignore: scoped_providers_should_specify_dependencies + soundServiceProvider.overrideWithValue(FakeSoundService()), + // ignore: scoped_providers_should_specify_dependencies + openingServiceProvider.overrideWithValue(FakeOpeningService()), + // ignore: scoped_providers_should_specify_dependencies + preloadedDataProvider.overrideWith((ref) { + return ( + sri: 'test-sri', + packageInfo: PackageInfo( + appName: 'lichess_mobile_test', + version: '0.0.0', + buildNumber: '0', + packageName: 'lichess_mobile_test', + ), + deviceInfo: BaseDeviceInfo({ + 'name': 'test', + 'model': 'test', + 'manufacturer': 'test', + 'systemName': 'test', + 'systemVersion': 'test', + 'identifierForVendor': 'test', + 'isPhysicalDevice': true, + }), + userSession: userSession, + engineMaxMemoryInMb: 256, + ); + }), + ...overrides ?? [], + ], + child: TestSurface(size: surfaceSize, child: child), + ); +} diff --git a/test/utils/duration_test.dart b/test/utils/duration_test.dart index 7405571d11..cacdc32270 100644 --- a/test/utils/duration_test.dart +++ b/test/utils/duration_test.dart @@ -24,23 +24,11 @@ void main() { group('DurationExtensions.toDaysHoursMinutes()', () { test('all values nonzero, plural', () { - testTimeStr( - mockAppLocalizations, - 2, - 2, - 2, - '2 days, 2 hours and 2 minutes', - ); + testTimeStr(mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes'); }); test('all values nonzero, plural', () { - testTimeStr( - mockAppLocalizations, - 2, - 2, - 2, - '2 days, 2 hours and 2 minutes', - ); + testTimeStr(mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes'); }); test('all values nonzero, single', () { @@ -84,7 +72,10 @@ void testTimeStr( int minutes, String expected, ) { - final timeStr = Duration(days: days, hours: hours, minutes: minutes) - .toDaysHoursMinutes(mockAppLocalizations); + final timeStr = Duration( + days: days, + hours: hours, + minutes: minutes, + ).toDaysHoursMinutes(mockAppLocalizations); expect(timeStr, expected); } diff --git a/test/utils/fake_connectivity.dart b/test/utils/fake_connectivity.dart new file mode 100644 index 0000000000..034e048467 --- /dev/null +++ b/test/utils/fake_connectivity.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +/// A fake implementation of [Connectivity] that always returns [ConnectivityResult.wifi]. +class FakeConnectivity implements Connectivity { + @override + Future> checkConnectivity() { + return Future.value([ConnectivityResult.wifi]); + } + + /// A broadcast stream controller of connectivity changes. + /// + /// This is used to simulate connectivity changes in tests. + static StreamController> controller = StreamController.broadcast(); + + @override + Stream> get onConnectivityChanged => controller.stream; +} diff --git a/test/utils/fake_connectivity_changes.dart b/test/utils/fake_connectivity_changes.dart deleted file mode 100644 index 9f19a707c9..0000000000 --- a/test/utils/fake_connectivity_changes.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'fake_connectivity_changes.g.dart'; - -@riverpod -class FakeConnectivityChanges extends _$FakeConnectivityChanges - implements ConnectivityChanges { - @override - Future build() async { - return const ConnectivityStatus( - isOnline: true, - ); - } -} diff --git a/test/utils/l10n_test.dart b/test/utils/l10n_test.dart index 72f178b3a7..2846a3b785 100644 --- a/test/utils/l10n_test.dart +++ b/test/utils/l10n_test.dart @@ -7,10 +7,7 @@ void main() { group('l10nWithWidget', () { const widget = Text('I am a widget'); test('placeholder in the middle', () { - final text = l10nWithWidget( - (_) => 'foo %s bar', - widget, - ); + final text = l10nWithWidget((_) => 'foo %s bar', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 3); expect((children[0] as TextSpan).text, 'foo '); @@ -19,18 +16,12 @@ void main() { }); test('no placeholder', () { - final text = l10nWithWidget( - (_) => 'foo bar', - widget, - ); + final text = l10nWithWidget((_) => 'foo bar', widget); expect(text.data, 'foo bar'); }); test('placeholder at the beginning', () { - final text = l10nWithWidget( - (_) => '%s foo bar', - widget, - ); + final text = l10nWithWidget((_) => '%s foo bar', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 2); expect((children[0] as WidgetSpan).child, widget); @@ -38,10 +29,7 @@ void main() { }); test('placeholder at the end', () { - final text = l10nWithWidget( - (_) => 'foo bar %s', - widget, - ); + final text = l10nWithWidget((_) => 'foo bar %s', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 2); expect((children[0] as TextSpan).text, 'foo bar '); diff --git a/test/utils/rate_limit_test.dart b/test/utils/rate_limit_test.dart index b652932bd3..78b2a54881 100644 --- a/test/utils/rate_limit_test.dart +++ b/test/utils/rate_limit_test.dart @@ -16,8 +16,7 @@ void main() { expect(called, true); }); - test('should not execute callback more than once if called multiple times', - () async { + test('should not execute callback more than once if called multiple times', () async { final debouncer = Debouncer(const Duration(milliseconds: 100)); var called = 0; debouncer(() { @@ -85,8 +84,7 @@ void main() { expect(called, 1); }); - test('should call the callback multiple times if delay is passed', - () async { + test('should call the callback multiple times if delay is passed', () async { final throttler = Throttler(const Duration(milliseconds: 100)); var called = 0; throttler(() { diff --git a/test/view/analysis/analysis_layout_test.dart b/test/view/analysis/analysis_layout_test.dart new file mode 100644 index 0000000000..e8ce54cb0f --- /dev/null +++ b/test/view/analysis/analysis_layout_test.dart @@ -0,0 +1,129 @@ +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + testWidgets('board background size should match board size on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [Center(child: Text('Analysis tab'))], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final backgroundSize = tester.getSize(find.byType(SolidColorChessboardBackground)); + + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square on $surface', + ); + + final boardSize = tester.getSize(find.byType(Chessboard)); + + expect(boardSize.width, boardSize.height, reason: 'Board size is square on $surface'); + + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size on $surface', + ); + } + }, variant: kPlatformVariant); + + testWidgets('board size and table side size should be harmonious on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [Center(child: Text('Analysis tab'))], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final isPortrait = surface.aspectRatio < 1.0; + final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); + + if (isPortrait) { + final expectedBoardSize = isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final tabBarViewSize = tester.getSize(find.byType(TabBarView)); + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = surface.shortestSide - kBottomBarHeight - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); + // TabBarView is inside a Card so we need to account for its padding + const cardPadding = 8.0; + final minSideWidth = min( + surface.longestSide - goldenBoardSize - 16.0 * 3 - cardPadding, + 250.0, + ); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + tabBarViewSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Tab bar view width should be at least $minSideWidth on $surface', + ); + } + } + }, variant: kPlatformVariant); +} diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 81a7d1494d..df63b7eb08 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -1,90 +1,80 @@ import 'dart:convert'; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; -import '../../test_app.dart'; +import '../../test_provider_scope.dart'; void main() { // ignore: avoid_dynamic_calls final sanMoves = jsonDecode(gameResponse)['moves'] as String; - const opening = LightOpening( - eco: 'C20', - name: "King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit", - ); group('Analysis Screen', () { testWidgets('displays correct move and position', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isComputerAnalysisAllowed: false, + variant: Variant.standard, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); - final currentMove = find.widgetWithText(InlineMove, 'Qe1#'); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); + final currentMove = find.textContaining('Qe1#'); expect(currentMove, findsOneWidget); expect( - tester.widgetList(currentMove).any((e) => e.isCurrentMove), + tester + .widget(find.ancestor(of: currentMove, matching: find.byType(InlineMove))) + .isCurrentMove, isTrue, ); }); testWidgets('move backwards and forward', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isComputerAnalysisAllowed: false, + variant: Variant.standard, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // cannot go forward - expect( - tester - .widget(find.byKey(const Key('goto-next'))) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('goto-next'))).onTap, isNull); // can go back expect( - tester - .widget(find.byKey(const Key('goto-previous'))) - .onTap, + tester.widget(find.byKey(const Key('goto-previous'))).onTap, isNotNull, ); @@ -92,37 +82,305 @@ void main() { await tester.tap(find.byKey(const Key('goto-previous'))); await tester.pumpAndSettle(); - final currentMove = find.widgetWithText(InlineMove, 'Kc1'); + final currentMove = find.textContaining('Kc1'); expect(currentMove, findsOneWidget); expect( - tester.widgetList(currentMove).any((e) => e.isCurrentMove), + tester + .widget(find.ancestor(of: currentMove, matching: find.byType(InlineMove))) + .isCurrentMove, + isTrue, + ); + }); + }); + + group('Analysis Tree View', () { + Future buildTree(WidgetTester tester, String pgn) async { + final app = await makeTestProviderScopeApp( + tester, + defaultPreferences: { + PrefCategory.analysis.storageKey: jsonEncode( + AnalysisPrefs.defaults.copyWith(enableLocalEvaluation: false).toJson(), + ), + }, + home: AnalysisScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: (pgn: pgn, isComputerAnalysisAllowed: false, variant: Variant.standard), + ), + enableDrawingShapes: false, + ), + ); + + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 1)); + } + + Text parentText(WidgetTester tester, String move) { + return tester.widget(find.ancestor(of: find.text(move), matching: find.byType(Text))); + } + + void expectSameLine(WidgetTester tester, Iterable moves) { + final line = parentText(tester, moves.first); + + for (final move in moves.skip(1)) { + final moveText = find.text(move); + expect(moveText, findsOneWidget); + expect(parentText(tester, move), line); + } + } + + void expectDifferentLines(WidgetTester tester, List moves) { + for (int i = 0; i < moves.length; i++) { + for (int j = i + 1; j < moves.length; j++) { + expect(parentText(tester, moves[i]), isNot(parentText(tester, moves[j]))); + } + } + } + + testWidgets('displays short sideline as inline', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) 2. Nf3 *'); + + final mainline = find.ancestor(of: find.text('1. e4'), matching: find.byType(Text)); + expect(mainline, findsOneWidget); + + expectSameLine(tester, ['1. e4', 'e5', '1… d5', '2. exd5', '2. Nf3']); + }); + + testWidgets('displays long sideline on its own line', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *'); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6']); + expectSameLine(tester, ['2. Nc3']); + + expectDifferentLines(tester, ['1. e4', '1… d5', '2. Nc3']); + }); + + testWidgets('displays sideline with branching on its own line', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 (2. Nc3)) *'); + + expectSameLine(tester, ['1. e4', 'e5']); + + // 2nd branch is rendered inline again + expectSameLine(tester, ['1… d5', '2. exd5', '2. Nc3']); + + expectDifferentLines(tester, ['1. e4', '1… d5']); + }); + + testWidgets('multiple sidelines', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *'); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5', '2. exd5']); + expectSameLine(tester, ['1… Nf6', '2. e5']); + expectSameLine(tester, ['2. Nf3', 'Nc6', '2… a5']); + + expectDifferentLines(tester, ['1. e4', '1… d5', '1… Nf6', '2. Nf3']); + }); + + testWidgets('collapses lines with nesting > 2', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *'); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5']); + expectSameLine(tester, ['2. Nc3']); + expectSameLine(tester, ['2. h4']); + + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); + + // sidelines with nesting > 2 are collapsed -> expand them + expect(find.byIcon(Icons.add_box), findsOneWidget); + + await tester.tap(find.byIcon(Icons.add_box)); + await tester.pumpAndSettle(); + + expectSameLine(tester, ['2… h5']); + expectSameLine(tester, ['2… Nc6', '3. d3']); + expectSameLine(tester, ['2… Qd7']); + + final d3 = find.text('3. d3'); + await tester.longPress(d3); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Collapse variations')); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // Sidelines should be collapsed again + expect(find.byIcon(Icons.add_box), findsOneWidget); + + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); + }); + + testWidgets('Expanding one line does not expand the following one (regression test)', ( + tester, + ) async { + /// Will be rendered as: + /// ------------------- + /// 1. e4 e5 + /// |- 1... d5 2. Nf3 (2.Nc3) + /// 2. Nf3 + /// |- 2. a4 d5 (2... f5) + /// ------------------- + await buildTree(tester, '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))'); + + expect(find.byIcon(Icons.add_box), findsNothing); + + // Collapse both lines + await tester.longPress(find.text('1… d5')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); + + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + await tester.longPress(find.text('2. a4')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); + + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // In this state, there used to be a bug where expanding the first line would + // also expand the second line. + expect(find.byIcon(Icons.add_box), findsNWidgets(2)); + await tester.tap(find.byIcon(Icons.add_box).first); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_box), findsOneWidget); + + // Second sideline should still be collapsed + expect(find.text('2. a4'), findsNothing); + }); + + testWidgets('subtrees not part of the current mainline part are cached', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *'); + + // will be rendered as: + // ------------------- + // 1. e4 e5 <-- first mainline part + // |- 1... d5 2. exd5 + // |- 1... Nf6 2. e5 + // 2. Nf3 Nc6 (2... a5) <-- second mainline part + // ^ + // | + // current move + + final firstMainlinePart = parentText(tester, '1. e4'); + final secondMainlinePart = parentText(tester, '2. Nf3'); + + expect( + tester + .widgetList( + find.ancestor(of: find.textContaining('Nc6'), matching: find.byType(InlineMove)), + ) + .last + .isCurrentMove, + isTrue, + ); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + tester + .widgetList( + find.ancestor(of: find.textContaining('Nf3'), matching: find.byType(InlineMove)), + ) + .last + .isCurrentMove, + isTrue, + ); + + // first mainline part has not changed since the current move is + // not part of it + expect(identical(firstMainlinePart, parentText(tester, '1. e4')), isTrue); + + final secondMainlinePartOnMoveNf3 = parentText(tester, '2. Nf3'); + + // second mainline part has changed since the current move is part of it + expect(secondMainlinePart, isNot(secondMainlinePartOnMoveNf3)); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + tester + .widgetList( + find.ancestor(of: find.textContaining('e5'), matching: find.byType(InlineMove)), + ) + .first + .isCurrentMove, + isTrue, + ); + + final firstMainlinePartOnMoveE5 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE5 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect(firstMainlinePart, isNot(firstMainlinePartOnMoveE5)); + + // second mainline part has changed since the current move is not part of it + // anymore + expect(secondMainlinePartOnMoveNf3, isNot(secondMainlinePartOnMoveE5)); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + tester + .firstWidget( + find.ancestor(of: find.textContaining('e4'), matching: find.byType(InlineMove)), + ) + .isCurrentMove, isTrue, ); + + final firstMainlinePartOnMoveE4 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE4 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect(firstMainlinePartOnMoveE4, isNot(firstMainlinePartOnMoveE5)); + + // second mainline part has not changed since the current move is not part of it + expect(identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), isTrue); }); }); } -final gameData = LightArchivedGame( - id: const GameId('qVChCOTc'), - rated: false, - speed: Speed.blitz, - perf: Perf.blitz, - createdAt: DateTime.parse('2023-01-11 14:30:22.389'), - lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), - status: GameStatus.mate, - white: const Player(aiLevel: 1), - black: const Player( - user: LightUser( - id: UserId('veloce'), - name: 'veloce', - isPatron: true, - ), - rating: 1435, - ), - variant: Variant.standard, - lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', - winner: Side.black, -); +// final gameData = LightArchivedGame( +// id: const GameId('qVChCOTc'), +// rated: false, +// speed: Speed.blitz, +// perf: Perf.blitz, +// createdAt: DateTime.parse('2023-01-11 14:30:22.389'), +// lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), +// status: GameStatus.mate, +// white: const Player(aiLevel: 1), +// black: const Player( +// user: LightUser( +// id: UserId('veloce'), +// name: 'veloce', +// isPatron: true, +// ), +// rating: 1435, +// ), +// variant: Variant.standard, +// lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', +// winner: Side.black, +// ); const gameResponse = ''' {"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart new file mode 100644 index 0000000000..320d486d8b --- /dev/null +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -0,0 +1,333 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; + +import '../../test_provider_scope.dart'; + +void main() { + group('Board Editor', () { + testWidgets('Displays initial FEN on start', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final editor = tester.widget(find.byType(ChessboardEditor)); + expect(editor.pieces, readFen(kInitialFEN)); + expect(editor.orientation, Side.white); + expect(editor.pointerMode, EditorPointerMode.drag); + + // Legal position, so allowed top open analysis board + expect( + tester.widget(find.byKey(const Key('analysis-board-button'))).onTap, + isNotNull, + ); + }); + + testWidgets('Flip board', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.byKey(const Key('flip-button'))); + await tester.pump(); + + expect( + tester.widget(find.byType(ChessboardEditor)).orientation, + Side.black, + ); + }); + + testWidgets('Side to play and castling rights', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.byKey(const Key('flip-button'))); + await tester.pump(); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + + final controllerProvider = boardEditorControllerProvider(null); + + container.read(controllerProvider.notifier).setSideToPlay(Side.black); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', + ); + + container.read(controllerProvider.notifier).setCastling(Side.white, CastlingSide.king, false); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b Qkq - 0 1', + ); + + container + .read(controllerProvider.notifier) + .setCastling(Side.white, CastlingSide.queen, false); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b kq - 0 1', + ); + + container.read(controllerProvider.notifier).setCastling(Side.black, CastlingSide.king, false); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b q - 0 1', + ); + + container + .read(controllerProvider.notifier) + .setCastling(Side.black, CastlingSide.queen, false); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b - - 0 1', + ); + + container.read(controllerProvider.notifier).setCastling(Side.white, CastlingSide.king, true); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b K - 0 1', + ); + }); + + testWidgets('Castling rights ignored when rook is missing', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + // Starting position, but with all rooks removed + container + .read(controllerProvider.notifier) + .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); + + // By default, all castling rights are true, but since there are no rooks, the final FEN should have no castling rights + expect( + container.read(controllerProvider).fen, + '1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1 w - - 0 1', + ); + }); + + testWidgets('support chess960 castling rights', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/4RK1R'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/4RK1R w KQkq - 0 1', + ); + }); + + testWidgets('Castling rights ignored when king is not in backrank', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pppppppp/8/8/8/5K2/PPPPPPPP/4R2R'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/5K2/PPPPPPPP/4R2R w kq - 0 1', + ); + }); + + testWidgets('Possible en passant squares are calculated correctly', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + container + .read(controllerProvider.notifier) + .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); + + expect(container.read(controllerProvider).enPassantOptions, SquareSet.empty); + + container + .read(controllerProvider.notifier) + .loadFen('r1bqkbnr/4p1p1/3n4/pPppPppP/8/8/P1PP1P2/RNBQKBNR w KQkq - 0 1'); + expect( + container.read(controllerProvider).enPassantOptions, + SquareSet.fromSquares([Square.a6, Square.c6, Square.f6]), + ); + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pp1p1p1p/8/8/PpPpPQpP/8/NPRP1PP1/2B1KBNR b Kkq - 0 1'); + container.read(controllerProvider.notifier).setSideToPlay(Side.black); + expect( + container.read(controllerProvider).enPassantOptions, + SquareSet.fromSquares([Square.e3, Square.h3]), + ); + }); + + testWidgets('Can drag pieces to new squares', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + // Two legal moves by white + await dragFromTo(tester, 'e2', 'e4'); + await dragFromTo(tester, 'd2', 'd4'); + + // Illegal move by black + await dragFromTo(tester, 'a8', 'a6'); + + // White queen captures white bishop + await dragFromTo(tester, 'd1', 'c1'); + + expect( + container.read(controllerProvider).fen, + // Obtained by playing the moves above on lichess.org/editor + '1nbqkbnr/pppppppp/r7/8/3PP3/8/PPP2PPP/RNQ1KBNR w KQk - 0 1', + ); + }); + + testWidgets('illegal position cannot be analyzed', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + // White queen "captures" white king => illegal position + await dragFromTo(tester, 'd1', 'e1'); + + expect( + tester.widget(find.byKey(const Key('analysis-board-button'))).onTap, + isNull, + ); + }); + + testWidgets('Delete pieces via bin button', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + await tester.tap(find.byKey(const Key('delete-button-white'))); + await tester.pump(); + + await tapSquare(tester, 'e2'); + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + + // Change back to drag mode -> tapping has no effect anymore + await tester.tap(find.byKey(const Key('drag-button-white'))); + await tester.pump(); + await tapSquare(tester, 'e3'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + + // Now remove all of black's pawns + await tester.tap(find.byKey(const Key('delete-button-black'))); + await tester.pump(); + await panFromTo(tester, 'a7', 'h7'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/8/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + }); + + testWidgets('Add pieces via tap and pan', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.byKey(const Key('piece-button-white-queen'))); + await panFromTo(tester, 'a1', 'a8'); + await tester.tap(find.byKey(const Key('piece-button-black-rook'))); + await tapSquare(tester, 'h1'); + await tapSquare(tester, 'h3'); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + expect( + container.read(controllerProvider).fen, + 'Qnbqkbnr/Qppppppp/Q7/Q7/Q7/Q6r/QPPPPPPP/QNBQKBNr w k - 0 1', + ); + }); + + testWidgets('Drag pieces onto the board', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); + await tester.pumpWidget(app); + + // Start by pressing bin button, dragging a piece should override this + await tester.tap(find.byKey(const Key('delete-button-black'))); + await tester.pump(); + + final pieceButtonOffset = tester.getCenter(find.byKey(const Key('piece-button-white-pawn'))); + await tester.dragFrom( + pieceButtonOffset, + tester.getCenter(find.byKey(const Key('d3-empty'))) - pieceButtonOffset, + ); + await tester.dragFrom( + pieceButtonOffset, + tester.getCenter(find.byKey(const Key('d1-whitequeen'))) - pieceButtonOffset, + ); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = boardEditorControllerProvider(null); + + expect(container.read(controllerProvider).editorPointerMode, EditorPointerMode.drag); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/3P4/PPPPPPPP/RNBPKBNR w KQkq - 0 1', + ); + }); + }); +} + +Future dragFromTo(WidgetTester tester, String from, String to) async { + final fromOffset = squareOffset(tester, Square.fromName(from)); + + await tester.dragFrom(fromOffset, squareOffset(tester, Square.fromName(to)) - fromOffset); + await tester.pumpAndSettle(); +} + +Future panFromTo(WidgetTester tester, String from, String to) async { + final fromOffset = squareOffset(tester, Square.fromName(from)); + + await tester.timedDragFrom( + fromOffset, + squareOffset(tester, Square.fromName(to)) - fromOffset, + const Duration(seconds: 1), + ); + await tester.pumpAndSettle(); +} + +Future tapSquare(WidgetTester tester, String square) async { + await tester.tapAt(squareOffset(tester, Square.fromName(square))); + await tester.pumpAndSettle(); +} + +Offset squareOffset(WidgetTester tester, Square square) { + final editor = find.byType(ChessboardEditor); + final squareSize = tester.getSize(editor).width / 8; + + return tester.getTopLeft(editor) + + Offset( + square.file.value * squareSize + squareSize / 2, + (7 - square.rank.value) * squareSize + squareSize / 2, + ); +} diff --git a/test/view/broadcast/broadcast_list_screen_test.dart b/test/view/broadcast/broadcast_list_screen_test.dart new file mode 100644 index 0000000000..2a90722cb6 --- /dev/null +++ b/test/view/broadcast/broadcast_list_screen_test.dart @@ -0,0 +1,100 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +class FakeImageColorWorker implements ImageColorWorker { + @override + void close() {} + + @override + bool get closed => false; + + @override + Future getImageColors(Uint32List image) { + return Future.value(null); + } +} + +class FakeBroadcastImageWorkerFactory implements BroadcastImageWorkerFactory { + @override + Future spawn() { + return Future.value(FakeImageColorWorker()); + } +} + +final client = MockClient((request) { + if (request.url.path == '/api/broadcast/top') { + return mockResponse( + broadcastsResponse, + 200, + headers: {'content-type': 'application/json; charset=utf-8'}, + ); + } + return mockResponse('', 404); +}); + +void main() { + group('BroadcastListScreen', () { + testWidgets('Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(BroadcastCard), findsAtLeast(1)); + }); + }); + + testWidgets('Scroll broadcast tournament screen', variant: kPlatformVariant, (tester) async { + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); + + await tester.scrollUntilVisible(find.text('Completed'), 200.0); + }); + }); + }); +} + +const broadcastsResponse = r''' +{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

\n

Time control is 15 minutes for the entire game with a 10-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

\n

If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

\n

If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

\n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
The player must have reached or reach the required age during the year of competition.
There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

\n

The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

\n

Prize Fund: 10,000 EUR

\n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move one.

\n

GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

\n

Official Website | Standings

\n
\n

El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

\n

El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

\n

Web Oficial | Chess-Results

\n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 12 2024

\n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

Group 1: Boards 1-5
Group 2: Boards 6-10

\n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

\n

The winner will be crowned Belgian Chess Champion 2024.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

\n
    \n
  1. FM Manon, Reja Neer 2445
  2. \n
  3. IM Mohammad Fahad, Rahman 2437
  4. \n
  5. GM Rahman, Ziaur 2423
  6. \n
  7. GM Hossain, Enamul 2365
  8. \n
  9. GM Murshed, Niaz 2317
  10. \n
\n

The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

\n

Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

\n

The top 5 players qualify for the Olympiad team.

\n

Here are useful links:

\n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

\n

Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Photo by Nestoras Argiris on Unsplash

\n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n

Official Website

\n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

\n

Time control is 75 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

\n

Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

\n

Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

\n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

\n

Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

\n

Official Website | Results

\n
\n

Title image photo by Paul Frendach

\n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

\n

SE

\n

Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

\n

Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

\n

Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

\n

EN

\n

Official Website | Video commentary | Results and Pairings | Livechess PGN

\n

Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

\n

Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

\n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

Title image photo by Dario Daniel Silva on Unsplash

\n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Rapido Sub 10 2024

\n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Offiical Website | Standings

\n
\n

Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

\n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
Torredembarra, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Del 29 de juny al 7 de juliol de 2024
ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
(Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

\n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Standings

\n
\n

Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

\n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 10 2024

\n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Board 1-9 Elite
Board 10 - 28 Senior 65+

\n

Official Website | Results

\n
\n

Landsturneringen 2024

\n

Eliteklassen og Senior 65+

\n

Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

\n

Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

\n

Bord 1-9 Eliteklassen
Bord 10 - 28 Senior 65+

\n

Clono partier:
Mikroputt
\nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
Miniputt
https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
Lilleputt
https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
Junior B
https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
Klasse 1
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
Klasse 2
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
Klasse 3
https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
Klasse 4
https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
Klasse 5
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
Klasse Mester
https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

\n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

\n

Time control is 120 minutes for the entire game, plus a 30-second increment per move.

\n

Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

\n

Official Website | Results

\n
\n

In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

\n
\n

Grand Chess Tour | Tour Standings
2024 Superbet Poland Rapid & Blitz
2024 Superbet Romania Chess Classic
2024 Superunited Croatia Rapid & Blitz

\n
\n

Title image photo by Arvid Olson from Pixabay

\n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

\n

Chief Arbiter: IA Gregory Vaserstein

\n

Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

\n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} +'''; diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart deleted file mode 100644 index 1f501b11f0..0000000000 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; -import 'package:network_image_mock/network_image_mock.dart'; - -import '../../test_app.dart'; -import '../../test_utils.dart'; - -final client = MockClient((request) { - if (request.url.path == '/api/broadcast/top') { - return mockResponse( - broadcastsResponse, - 200, - headers: {'content-type': 'application/json; charset=utf-8'}, - ); - } - return mockResponse('', 404); -}); - -void main() { - group('BroadcastsListScreen', () { - testWidgets( - 'Displays broadcast tournament screen', - variant: kPlatformVariant, - (tester) async { - final app = await buildTestApp( - tester, - home: const BroadcastsListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // let time for request to complete - await mockNetworkImagesFor( - () => tester.pump(const Duration(milliseconds: 50)), - ); - - expect(find.byType(BroadcastGridItem), findsAtLeast(1)); - }, - ); - - testWidgets( - 'Scroll broadcast tournament screen', - variant: kPlatformVariant, - (tester) async { - final app = await buildTestApp( - tester, - home: const BroadcastsListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // let time for request to complete - await mockNetworkImagesFor( - () => tester.pump(const Duration(milliseconds: 50)), - ); - - tester.scrollUntilVisible(find.text('Past broadcasts'), 200.0); - }, - ); - }); -} - -const broadcastsResponse = r''' -{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

\n

Time control is 15 minutes for the entire game with a 10-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

\n

If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

\n

If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

\n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
The player must have reached or reach the required age during the year of competition.
There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

\n

The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

\n

Prize Fund: 10,000 EUR

\n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move one.

\n

GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

\n

Official Website | Standings

\n
\n

El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

\n

El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

\n

Web Oficial | Chess-Results

\n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 12 2024

\n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

Group 1: Boards 1-5
Group 2: Boards 6-10

\n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

\n

The winner will be crowned Belgian Chess Champion 2024.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

\n
    \n
  1. FM Manon, Reja Neer 2445
  2. \n
  3. IM Mohammad Fahad, Rahman 2437
  4. \n
  5. GM Rahman, Ziaur 2423
  6. \n
  7. GM Hossain, Enamul 2365
  8. \n
  9. GM Murshed, Niaz 2317
  10. \n
\n

The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

\n

Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

\n

The top 5 players qualify for the Olympiad team.

\n

Here are useful links:

\n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

\n

Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Photo by Nestoras Argiris on Unsplash

\n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n

Official Website

\n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

\n

Time control is 75 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

\n

Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

\n

Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

\n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"upcoming":[{"tour":{"id":"wXto4wTE","name":"Warsaw Chess Festival 2024 | Najdorf Memorial","slug":"warsaw-chess-festival-2024--najdorf-memorial","info":{"format":"9-round Swiss","tc":"Classical","players":"Kazakouski, Krasenkiw, Markowski, Rozentalis"},"createdAt":1720377119509,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/wXto4wTE","tier":3,"dates":[1720538100000,1721204100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=alefzero:relay:wXto4wTE:rdHCIp4Q.jpg&w=800&sig=8e8dad9d2c0ec3c494b4226173d1c14ca1fd0040","markup":"

The Najdorf Memorial 2024 is a 9-round Swiss, held from the 9th to the 17th of July in Warsaw, Poland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

23rd international open tournament to commemorate GM Miguel (Mieczysław) Najdorf

\n"},"round":{"id":"ENq91IQo","name":"Round 1","slug":"round-1","createdAt":1720377162688,"startsAt":1720538100000,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/round-1/ENq91IQo"},"group":"Warsaw Chess Festival 2024"},{"tour":{"id":"lb23JlWD","name":"2024 Australian University Chess League","slug":"2024-australian-university-chess-league","info":{"format":"6-team round-robin","tc":"Classical"},"createdAt":1718194400018,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/lb23JlWD","tier":3,"dates":[1720775700000,1720934100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:lb23JlWD:vBwmZg4h.jpg&w=800&sig=f6ed72005b1790b9bfc351908acad119b5c369ba","markup":"

The 2024 Australian University Chess League is a 6-team round-robin, held from the 12th to the 14th of July in Sydney, Australia. There are four players per team.

\n

Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

\n","teamTable":true},"round":{"id":"yAulT9F2","name":"Round 1","slug":"round-1","createdAt":1718194461669,"startsAt":1720775700000,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/round-1/yAulT9F2"}},{"tour":{"id":"GboZ8j0F","name":"IV Open Internacional Vila del Vendrell","slug":"iv-open-internacional-vila-del-vendrell","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719974387311,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/GboZ8j0F","tier":3,"dates":[1720855800000,1720889100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:GboZ8j0F:BSyQN0Ff.jpg&w=800&sig=a39542ad349425ce3be423a1b5a1d15ef326e1f9","markup":"

The IV Open Internacional Vila del Vendrell is a 9-round Swiss, held on 13th of July in El Vendrell, Tarragona, Catalonia, Spain.

\n

Time control is 15 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"LBvFee4s","name":"Round 1","slug":"round-1","createdAt":1719974485751,"startsAt":1720855800000,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/round-1/LBvFee4s"}},{"tour":{"id":"Es02AbFN","name":"Swiss Individual Championships 2024 | Open","slug":"swiss-individual-championships-2024--open","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1717543242414,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/Es02AbFN","tier":3,"dates":[1720871100000,1721546100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:Es02AbFN:Yz0Im12b.jpg&w=800&sig=8e0f71606055775d5ea2006c6f09f2851872823a","markup":"

The Swiss Individual Championships 2024 | Open is a 10-player round-robin, held from the 13th to the 21st of July in Flims, Switzerland.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"vhvpniLr","name":"Round 1","slug":"round-1","createdAt":1717543458521,"startsAt":1720871100000,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/round-1/vhvpniLr"},"group":"Swiss Individual Championships 2024"},{"tour":{"id":"op7Dy2aB","name":"Zadar Chess Festival 2024","slug":"zadar-chess-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720373669999,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/op7Dy2aB","tier":3,"dates":[1721142000000,1721721600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=coach_goran:relay:op7Dy2aB:SVKZikJd.jpg&w=800&sig=70198997a1e6ce8a8f9ecdce566d2c86cf1bcc2b","markup":"

The Zadar Chess Festival 2024 Tournament-A is a 9-round Swiss, held from the 16th to the 23rd of July in Zadar, Croatia.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Standings

\n
\n

The Zadar Chess Festival brings chess players from all continents to Zadar. The tournament is divided into two categories, Tournament A and Tournament B. Tournament A is a grandmaster-level event where players compete for master norms, while Tournament B is for amateurs. Attendance is expected to be on par with last year when around 300 players participated. As part of the festival, a blitz championship will also be held, which this year coincides with World Chess Day.

\n

Zadar is a beautiful and extremely popular seaside destination visited by numerous tourists from all over the world. Besides its unique beauty, Zadar is a place of culture and heritage, fantastic gastronomy, a well-known sports city, and a city where everyone can find everything for a perfect summer vacation.

\n

Zadar Tourist Board

\n"},"round":{"id":"Lx0mdzh1","name":"Round 1","slug":"round-1","createdAt":1720437555322,"startsAt":1721142000000,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/round-1/Lx0mdzh1"}},{"tour":{"id":"ilou0biG","name":"3rd Father Cup | Masters","slug":"3rd-father-cup--masters","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1717535598646,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/ilou0biG","tier":3,"dates":[1722169800000,1722666600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ali9fazeli:relay:ilou0biG:ni99wVnt.jpg&w=800&sig=795312938ba49ed18f08120aee2efa1eed924764","markup":"

The 3rd Father Cup | Masters is a 9-round Swiss, held from 28th of July to 3th of August in Rasht, Iran.

\n

Time control is 90 minutes for 40 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Results

\n"},"round":{"id":"jcUBMfjn","name":"Round 1","slug":"round-1","createdAt":1717535725866,"startsAt":1722169800000,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/round-1/jcUBMfjn"},"group":"3rd Father Cup"},{"tour":{"id":"nMDYY8rH","name":"28th Créon International 2024 | Main","slug":"28th-creon-international-2024--main","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720108038852,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/nMDYY8rH","tier":3,"dates":[1722254400000,1722754800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:nMDYY8rH:kljC7xWa.jpg&w=800&sig=b06f8d53b209c0e4eadd232c24700ced2535b02f","markup":"

The 28th Créon International 2024 | Main is a 9-round Swiss, held from the 29th of July to the 4th of August in Créon, France.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"4ONAFGnx","name":"Round 1","slug":"round-1","createdAt":1720108110532,"startsAt":1722254400000,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/round-1/4ONAFGnx"},"group":"28th Créon International 2024"}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

\n

Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

\n

Official Website | Results

\n
\n

Title image photo by Paul Frendach

\n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

\n

SE

\n

Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

\n

Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

\n

Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

\n

EN

\n

Official Website | Video commentary | Results and Pairings | Livechess PGN

\n

Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

\n

Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

\n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n
\n

Title image photo by Dario Daniel Silva on Unsplash

\n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Rapido Sub 10 2024

\n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Official Website

\n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Offiical Website | Standings

\n
\n

Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

\n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
Torredembarra, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Del 29 de juny al 7 de juliol de 2024
ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
(Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

\n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

\n

Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

Standings

\n
\n

Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

\n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

Campeonato de España Sub 10 2024

\n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Results

\n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

\n

Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

\n

Board 1-9 Elite
Board 10 - 28 Senior 65+

\n

Official Website | Results

\n
\n

Landsturneringen 2024

\n

Eliteklassen og Senior 65+

\n

Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

\n

Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

\n

Bord 1-9 Eliteklassen
Bord 10 - 28 Senior 65+

\n

Clono partier:
Mikroputt
\nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
Miniputt
https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
Lilleputt
https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
Junior B
https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
Klasse 1
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
Klasse 2
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
Klasse 3
https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
Klasse 4
https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
Klasse 5
https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
Klasse Mester
https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

\n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

\n

Time control is 120 minutes for the entire game, plus a 30-second increment per move.

\n

Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

\n

Official Website | Results

\n
\n

In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

\n

In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

\n
\n

Grand Chess Tour | Tour Standings
2024 Superbet Poland Rapid & Blitz
2024 Superbet Romania Chess Classic
2024 Superunited Croatia Rapid & Blitz

\n
\n

Title image photo by Arvid Olson from Pixabay

\n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website

\n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

\n

Time control is 90 minutes for the entire game with a 30-second increment from move 1.

\n

Official Website | Standings

\n
\n

The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

\n

Chief Arbiter: IA Gregory Vaserstein

\n

Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

\n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} -'''; diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart new file mode 100644 index 0000000000..687a2b9829 --- /dev/null +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -0,0 +1,110 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; + +import '../../test_provider_scope.dart'; + +void main() { + group('Coordinate Training', () { + testWidgets('Initial state when started in FindSquare mode', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + expect(container.read(controllerProvider).score, 0); + expect(container.read(controllerProvider).currentCoord, isNotNull); + expect(container.read(controllerProvider).nextCoord, isNotNull); + expect(container.read(controllerProvider).trainingActive, true); + + // Current and next coordinate prompt should be displayed + expect(find.text(container.read(controllerProvider).currentCoord!.name), findsOneWidget); + expect(find.text(container.read(controllerProvider).nextCoord!.name), findsOneWidget); + }); + + testWidgets('Tap wrong square', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + final currentCoord = container.read(controllerProvider).currentCoord; + final nextCoord = container.read(controllerProvider).nextCoord; + + final wrongCoord = Square.values[(currentCoord! + 1) % Square.values.length]; + + await tester.tap(find.byKey(ValueKey('${wrongCoord.name}-empty'))); + await tester.pump(); + + expect(container.read(controllerProvider).score, 0); + expect(container.read(controllerProvider).currentCoord, currentCoord); + expect(container.read(controllerProvider).nextCoord, nextCoord); + expect(container.read(controllerProvider).trainingActive, true); + + expect(find.byKey(ValueKey('${wrongCoord.name}-highlight')), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 300)); + expect(find.byKey(ValueKey('${wrongCoord.name}-highlight')), findsNothing); + }); + + testWidgets('Tap correct square', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + final currentCoord = container.read(controllerProvider).currentCoord; + final nextCoord = container.read(controllerProvider).nextCoord; + + await tester.tap(find.byKey(ValueKey('${currentCoord!.name}-empty'))); + await tester.pump(); + + expect(find.byKey(ValueKey('${currentCoord.name}-highlight')), findsOneWidget); + + expect(container.read(controllerProvider).score, 1); + expect(container.read(controllerProvider).currentCoord, nextCoord); + expect(container.read(controllerProvider).trainingActive, true); + + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + expect(find.byKey(ValueKey('${currentCoord.name}-highlight')), findsNothing); + + expect(find.text(container.read(controllerProvider).currentCoord!.name), findsOneWidget); + expect(find.text(container.read(controllerProvider).nextCoord!.name), findsOneWidget); + }); + }); +} diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index 4c699279b3..8350079209 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -1,10 +1,9 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -12,13 +11,14 @@ import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; -import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/move_list.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/game/export/qVChCOTc') { @@ -29,88 +29,78 @@ final client = MockClient((request) { void main() { group('ArchivedGameScreen', () { - testWidgets( - 'displays game data and last fen immediately, then moves', - (tester) async { - final app = await buildTestApp( - tester, - home: ArchivedGameScreen( - gameData: gameData, - orientation: Side.white, - ), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // data shown immediately - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect( - find.widgetWithText(GamePlayer, 'Stockfish level 1'), - findsOneWidget, - ); - - // cannot interact with board - expect( - tester.widget(find.byType(cg.Board)).data.interactableSide, - cg.InteractableSide.none, - ); - - // moves are not loaded - expect(find.byType(MoveList), findsNothing); - expect( - tester - .widget( - find.byKey(const ValueKey('cursor-back')), - ) - .onTap, - isNull, - ); - - // wait for game steps loading - await tester.pump(const Duration(milliseconds: 100)); - // wait for move list ensureVisible animation to finish - await tester.pumpAndSettle(); + testWidgets('loads game data if only game id is provided', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const ArchivedGameScreen(gameId: GameId('qVChCOTc'), orientation: Side.white), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PieceWidget), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for game data loading + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + }, variant: kPlatformVariant); + + testWidgets('displays game data and last fen immediately, then moves', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: ArchivedGameScreen(gameData: gameData, orientation: Side.white), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + // data shown immediately + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + + // cannot interact with board + expect(tester.widget(find.byType(Chessboard)).game, null); + + // moves are not loaded + expect(find.byType(MoveList), findsNothing); + expect( + tester.widget(find.byKey(const ValueKey('cursor-back'))).onTap, + isNull, + ); + + // wait for game steps loading + await tester.pump(const Duration(milliseconds: 100)); + // wait for move list ensureVisible animation to finish + await tester.pumpAndSettle(); + + // same info still displayed + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + + // now with the clocks + expect(find.text('1:46', findRichText: true), findsNWidgets(1)); + expect(find.text('0:46', findRichText: true), findsNWidgets(1)); - // same info still displayed - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect( - find.widgetWithText(GamePlayer, 'Stockfish level 1'), - findsOneWidget, - ); - - // now with the clocks - expect(find.text('1:46', findRichText: true), findsNWidgets(1)); - expect(find.text('0:46', findRichText: true), findsNWidgets(1)); - - // moves are loaded - expect(find.byType(MoveList), findsOneWidget); - expect( - tester - .widget( - find.byKey(const ValueKey('cursor-back')), - ) - .onTap, - isNotNull, - ); - }, - variant: kPlatformVariant, - ); + // moves are loaded + expect(find.byType(MoveList), findsOneWidget); + expect( + tester.widget(find.byKey(const ValueKey('cursor-back'))).onTap, + isNotNull, + ); + }, variant: kPlatformVariant); testWidgets('navigate game positions', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, - home: ArchivedGameScreen( - gameData: gameData, - orientation: Side.white, - ), + home: ArchivedGameScreen(gameData: gameData, orientation: Side.white), overrides: [ lichessClientProvider.overrideWith((ref) { return LichessClient(client, ref); @@ -135,23 +125,12 @@ void main() { .toList(); expect( - tester - .widget( - find.widgetWithText(InlineMoveItem, 'Qe1#'), - ) - .current, + tester.widget(find.widgetWithText(InlineMoveItem, 'Qe1#')).current, isTrue, ); // cannot go forward - expect( - tester - .widget( - find.byKey(const Key('cursor-forward')), - ) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('cursor-forward'))).onTap, isNull); for (var i = 0; i <= movesAfterE4.length; i++) { // go back in history @@ -162,25 +141,17 @@ void main() { // move list is updated final prevMoveIndex = i + 1; if (prevMoveIndex < movesAfterE4.length) { - final prevMove = - find.widgetWithText(InlineMoveItem, movesAfterE4[prevMoveIndex]); + final prevMove = find.widgetWithText(InlineMoveItem, movesAfterE4[prevMoveIndex]); expect(prevMove, findsAtLeastNWidgets(1)); expect( - tester - .widgetList(prevMove) - .any((e) => e.current ?? false), + tester.widgetList(prevMove).any((e) => e.current ?? false), isTrue, ); } } // cannot go backward anymore - expect( - tester - .widget(find.byKey(const Key('cursor-back'))) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('cursor-back'))).onTap, isNull); }); }); } @@ -188,7 +159,7 @@ void main() { // -- const gameResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} +{"id":"qVChCOTc","rated":false,"source":"lobby","variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1"} '''; final gameData = LightArchivedGame( @@ -201,11 +172,7 @@ final gameData = LightArchivedGame( status: GameStatus.mate, white: const Player(aiLevel: 1), black: const Player( - user: LightUser( - id: UserId('veloce'), - name: 'veloce', - isPatron: true, - ), + user: LightUser(id: UserId('veloce'), name: 'veloce', isPatron: true), rating: 1435, ), variant: Variant.standard, diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart new file mode 100644 index 0000000000..fb8ede009a --- /dev/null +++ b/test/view/game/game_screen_test.dart @@ -0,0 +1,595 @@ +import 'dart:convert'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../model/game/game_socket_example_data.dart'; +import '../../network/fake_websocket_channel.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + if (request.url.path == '/api/board/seek') { + return mockResponse('ok', 200); + } + return mockResponse('', 404); +}); + +class MockSoundService extends Mock implements SoundService {} + +void main() { + group('Loading', () { + testWidgets('a game directly with initialGameId', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen(initialGameId: GameFullId('qVChCOTcHSeW')), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => fakeSocket); + }), + ], + ); + await tester.pumpWidget(app); + + // while loading, displays an empty board + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + // now the game controller is loading and screen doesn't have changed yet + await tester.pump(const Duration(milliseconds: 10)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + await fakeSocket.connectionEstablished; + + fakeSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + }); + + testWidgets('a game from the pool with a seek', (WidgetTester tester) async { + final fakeLobbySocket = FakeWebSocketChannel(); + final fakeGameSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + seek: GameSeek(clock: (Duration(minutes: 3), Duration(seconds: 2)), rated: true), + ), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory( + (String url) => url.contains('lobby') ? fakeLobbySocket : fakeGameSocket, + ); + }), + ], + ); + await tester.pumpWidget(app); + + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsOneWidget); + expect(find.text('3+2'), findsOneWidget); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsOneWidget); + + // waiting for the game + await tester.pump(const Duration(seconds: 2)); + + // when a seek is accepted, server sends a 'redirect' message with game id + fakeLobbySocket.addIncomingMessages([ + '{"t": "redirect", "d": {"id": "qVChCOTcHSeW" }, "v": 1}', + ]); + await tester.pump(const Duration(milliseconds: 1)); + + // now the game controller is loading + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsNothing); + + await fakeGameSocket.connectionEstablished; + // now that game socket is open, lobby socket should be closed + expect(fakeLobbySocket.closeCode, isNotNull); + + fakeGameSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + }); + }); + + group('Clock', () { + testWidgets('loads on game start', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + }); + + testWidgets('ticks after the first full move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + await playMove(tester, 'e2', 'e4'); + // at that point clock is not yet started + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 1, "uci": "e2e4", "san": "e4", "clock": {"white": 180, "black": 180}}}', + '{"t": "move", "v": 2, "d": {"ply": 2, "uci": "e7e5", "san": "e5", "clock": {"white": 180, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + expect( + tester.widgetList(find.byType(Clock)).last.active, + true, + reason: 'my clock is now active', + ); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:58'), findsOneWidget); + }); + + testWidgets('ticks immediately when resuming game', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is already active', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:52'), findsOneWidget); + }); + + testWidgets('switch timer side after a move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + ); + expect(tester.widgetList(find.byType(Clock)).last.active, true); + // simulates think time of 3s + await tester.pump(const Duration(seconds: 3)); + await playMove(tester, 'g1', 'f3'); + expect(findClockWithTime('2:55'), findsOneWidget); + expect( + tester.widgetList(find.byType(Clock)).last.active, + false, + reason: 'white clock is stopped while waiting for server ack', + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is now active but not yet ticking', + ); + expect(findClockWithTime('3:00'), findsOneWidget); + // simulates a long lag just to show the clock is not running yet + await tester.pump(const Duration(milliseconds: 200)); + expect(findClockWithTime('3:00'), findsOneWidget); + // server ack having the white clock updated with the increment + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 3, "uci": "g1f3", "san": "Nf3", "clock": {"white": 177, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + // we see now the white clock has got its increment + expect(findClockWithTime('2:57'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + // black clock is ticking + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:58'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsNWidgets(2)); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:56'), findsOneWidget); + }); + + testWidgets('compensates opponent lag', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + int socketVersion = 0; + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3 Nc6', + clock: const ( + running: true, + initial: Duration(minutes: 1), + increment: Duration.zero, + white: Duration(seconds: 58), + black: Duration(seconds: 54), + emerg: Duration(seconds: 10), + ), + socketVersion: socketVersion, + ); + await tester.pump(const Duration(seconds: 3)); + await playMoveWithServerAck( + fakeSocket, + tester, + 'f1', + 'c4', + ply: 5, + san: 'Bc4', + clockAck: ( + white: const Duration(seconds: 55), + black: const Duration(seconds: 54), + lag: const Duration(milliseconds: 250), + ), + socketVersion: ++socketVersion, + ); + // black clock is active + expect(tester.widgetList(find.byType(Clock)).first.active, true); + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + // lag is 250ms, so clock will only start after that delay + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(findClockWithTime('0:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('0:52'), findsOneWidget); + }); + + testWidgets('onEmergency', (WidgetTester tester) async { + final mockSoundService = MockSoundService(); + when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {}); + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(seconds: 40), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + overrides: [soundServiceProvider.overrideWith((_) => mockSoundService)], + ); + expect( + tester.widget(findClockWithTime('0:40')).emergencyThreshold, + const Duration(seconds: 30), + ); + await tester.pump(const Duration(seconds: 10)); + expect(findClockWithTime('0:30'), findsOneWidget); + verify(() => mockSoundService.play(Sound.lowTime)).called(1); + }); + + testWidgets('flags', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is active', + ); + + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(minutes: 2, seconds: 53)); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.0'), findsOneWidget); + + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: + 'black clock is still active after flag (as long as we have not received server ack)', + ); + + // flag messages are throttled with 500ms delay + // we'll simulate an anormally long server response of 1s to check 2 + // flag messages are sent + expectLater( + fakeSocket.sentMessagesExceptPing, + emitsInOrder(['{"t":"flag","d":"black"}', '{"t":"flag","d":"black"}']), + ); + await tester.pump(const Duration(seconds: 1)); + fakeSocket.addIncomingMessages([ + '{"t":"endData","d":{"status":"outoftime","winner":"white","clock":{"wc":17800,"bc":0}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'both clocks are now inactive', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.00'), findsOneWidget); + + // wait for the dong + await tester.pump(const Duration(seconds: 500)); + }); + }); + + group('Opening analysis', () { + testWidgets('is not possible for an unfinished real time game', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + socketVersion: 0, + ); + expect(find.byType(Chessboard), findsOneWidget); + await tester.tap(find.byIcon(Icons.menu)); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.byType(Dialog), findsOneWidget); + expect(find.text('Analysis board'), findsNothing); + }); + + testWidgets('for an unfinished correspondence game', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + clock: null, + correspondenceClock: ( + daysPerTurn: 3, + white: const Duration(days: 3), + black: const Duration(days: 2, hours: 22, minutes: 49, seconds: 59), + ), + socketVersion: 0, + ); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byKey(const Key('d3-whitebishop')), findsOneWidget); + expect(find.byKey(const Key('b5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('d3-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(Icons.menu)); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('Analysis board')); + await tester.pumpAndSettle(); // wait for analysis screen to open + expect( + find.widgetWithText(PlatformAppBar, 'Analysis board'), + findsOneWidget, + ); // analysis screen is now open + expect(find.byKey(const Key('f3-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d3-whitebishop')), findsOneWidget); + expect(find.byKey(const Key('b5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('d3-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(LichessIcons.flow_cascade)); + await tester.pumpAndSettle(); // wait for the moves tab menu to open + expect(find.text('Moves played'), findsOneWidget); + // computer analysis is not available when game is not finished + expect(find.text('Computer analysis'), findsNothing); + }); + + testWidgets('for a finished game', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await loadFinishedTestGame(fakeSocket, tester); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byKey(const Key('e6-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e6-lastMove')), findsOneWidget); + await tester.pump(const Duration(milliseconds: 500)); // wait for popup + await tester.tap(find.text('Analysis board')); + await tester.pumpAndSettle(); // wait for analysis screen to open + expect( + find.widgetWithText(PlatformAppBar, 'Analysis board'), + findsOneWidget, + ); // analysis screen is now open + expect(find.byKey(const Key('e6-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e6-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(LichessIcons.flow_cascade)); + await tester.pumpAndSettle(); // wait for the moves tab menu to open + expect(find.text('Moves played'), findsOneWidget); + expect(find.text('Computer analysis'), findsOneWidget); // computer analysis is available + }); + }); +} + +Finder findClockWithTime(String text, {bool skipOffstage = true}) { + return find.ancestor( + of: find.text(text, findRichText: true, skipOffstage: skipOffstage), + matching: find.byType(Clock, skipOffstage: skipOffstage), + ); +} + +/// Simulates playing a move and getting the ack from the server after [elapsedTime]. +Future playMoveWithServerAck( + FakeWebSocketChannel socket, + WidgetTester tester, + String from, + String to, { + required String san, + required ({Duration white, Duration black, Duration? lag}) clockAck, + required int socketVersion, + required int ply, + Duration elapsedTime = const Duration(milliseconds: 10), + Side orientation = Side.white, +}) async { + await playMove(tester, from, to, orientation: orientation); + final uci = '$from$to'; + final lagStr = + clockAck.lag != null ? ', "lag": ${(clockAck.lag!.inMilliseconds / 10).round()}' : ''; + await tester.pump(elapsedTime - const Duration(milliseconds: 1)); + socket.addIncomingMessages([ + '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": ${(clockAck.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(clockAck.black.inMilliseconds / 1000).toStringAsFixed(2)}$lagStr}}}', + ]); + await tester.pump(const Duration(milliseconds: 1)); +} + +/// Convenient function to start a new test game +Future createTestGame( + FakeWebSocketChannel socket, + WidgetTester tester, { + Side? youAre = Side.white, + String? pgn, + int socketVersion = 0, + FullEventTestClock? clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + FullEventTestCorrespondenceClock? correspondenceClock, + List? overrides, +}) async { + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen(initialGameId: GameFullId('qVChCOTcHSeW')), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => socket); + }), + ...?overrides, + ], + ); + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 10)); + await socket.connectionEstablished; + + socket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + pgn ?? '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + youAre: youAre, + socketVersion: socketVersion, + clock: clock, + correspondenceClock: correspondenceClock, + ), + ]); + await tester.pump(const Duration(milliseconds: 10)); +} + +Future loadFinishedTestGame( + FakeWebSocketChannel socket, + WidgetTester tester, { + String serverFullEvent = _finishedGameFullEvent, + List? overrides, +}) async { + final json = jsonDecode(serverFullEvent) as Map; + final gameId = GameFullEvent.fromJson(json['d'] as Map).game.id; + final app = await makeTestProviderScopeApp( + tester, + home: GameScreen(initialGameId: GameFullId('${gameId.value}test')), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => socket); + }), + ...?overrides, + ], + ); + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 10)); + await socket.connectionEstablished; + + socket.addIncomingMessages([serverFullEvent]); + await tester.pump(const Duration(milliseconds: 10)); +} + +const _finishedGameFullEvent = ''' +{"t":"full","d":{"game":{"id":"CCW6EEru","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"6kr/p1p2rpp/4Q3/2b1p3/8/2P5/P2N1PPP/R3R1K1 b - - 0 22","turns":43,"source":"lobby","status":{"id":31,"name":"resign"},"createdAt":1706185945680,"winner":"white","pgn":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6"},"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9},"socket":0,"clock":{"running":false,"initial":120,"increment":1,"white":31.2,"black":27.42,"emerg":15,"moretime":15},"takebackable":true,"youAre":"white","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}},"v": 0} +'''; diff --git a/test/view/home/home_tab_screen_test.dart b/test/view/home/home_tab_screen_test.dart new file mode 100644 index 0000000000..0546253d87 --- /dev/null +++ b/test/view/home/home_tab_screen_test.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/app.dart'; +import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/view/play/quick_game_matrix.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +import '../../example_data.dart'; +import '../../mock_server_responses.dart'; +import '../../model/auth/fake_session_storage.dart'; +import '../../network/fake_http_client_factory.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + group('Home online', () { + testWidgets('shows Play button', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('shows players button', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(Icons.group_outlined), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNotNull, + ); + }); + + testWidgets('shows challenge button if has session', (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + ); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(LichessIcons.crossed_swords), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNotNull, + ); + }); + + testWidgets('shows quick pairing matrix', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(QuickGameMatrix), findsOneWidget); + }); + + testWidgets('no session, no stored game: shows welcome screen ', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect( + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), + findsOneWidget, + ); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets('session, no played game: shows welcome screen but no sign in button', ( + tester, + ) async { + int nbUserGamesRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/games/user/testuser') { + nbUserGamesRequests++; + return mockResponse('', 200); + } + return mockResponse('', 200); + }); + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect(nbUserGamesRequests, 1); + expect( + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), + findsOneWidget, + ); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets('no session, with stored games: shows list of recent games', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('Anonymous'), findsNWidgets(3)); + }); + + testWidgets('session, with played games: shows recent games', (tester) async { + int nbUserGamesRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/games/user/testuser') { + nbUserGamesRequests++; + return mockResponse(mockUserRecentGameResponse('testUser'), 200); + } + return mockResponse('', 200); + }); + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect(nbUserGamesRequests, 1); + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('MightyNanook'), findsOneWidget); + }); + }); + + group('Home offline', () { + testWidgets('shows offline banner', (tester) async { + final app = await makeOfflineTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(ConnectivityBanner), findsOneWidget); + }); + + testWidgets('shows Play button', (tester) async { + final app = await makeOfflineTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('shows disabled players button', (tester) async { + final app = await makeOfflineTestProviderScope(tester, child: const Application()); + + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(Icons.group_outlined), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNull, + ); + }); + + testWidgets('no session, no stored game: shows welcome screen ', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect( + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), + findsOneWidget, + ); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets('no session, with stored games: shows list of recent games', (tester) async { + final app = await makeOfflineTestProviderScope(tester, child: const Application()); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('Anonymous'), findsNWidgets(3)); + }); + + testWidgets('session, with stored games: shows list of recent games', (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3, username: 'testUser'); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + }); + }); +} diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart new file mode 100644 index 0000000000..bfc9ca9015 --- /dev/null +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -0,0 +1,318 @@ +import 'dart:convert'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + // final explorerViewFinder = find.descendant( + // of: find.byType(LayoutBuilder), + // matching: find.byType(Scrollable), + // ); + + final mockClient = MockClient((request) { + if (request.url.host == 'explorer.lichess.ovh') { + if (request.url.path == '/masters') { + return mockResponse(mastersOpeningExplorerResponse, 200); + } + if (request.url.path == '/lichess') { + return mockResponse(lichessOpeningExplorerResponse, 200); + } + if (request.url.path == '/player') { + return mockResponse(playerOpeningExplorerResponse, 200); + } + } + return mockResponse('', 404); + }); + + const options = AnalysisOptions( + orientation: Side.white, + standalone: (pgn: '', isComputerAnalysisAllowed: false, variant: Variant.standard), + ); + + const name = 'John'; + + final user = LightUser(id: UserId.fromUserName(name), name: name); + + final session = AuthSessionState(user: user, token: 'test-token'); + + group('OpeningExplorerScreen', () { + testWidgets('master opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); + + final moves = ['e4', 'd4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); + expect(find.widgetWithText(Container, 'Recent games'), findsNothing); + + // TODO: make a custom scrollUntilVisible that works with the non-scrollable + // board widget + + // await tester.scrollUntilVisible( + // find.text('Firouzja, A.'), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsNWidgets(2), + // ); + }, variant: kPlatformVariant); + + testWidgets('lichess opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + defaultPreferences: { + SessionPreferencesStorage.key(PrefCategory.openingExplorer.storageKey, null): jsonEncode( + OpeningExplorerPrefs.defaults().copyWith(db: OpeningDatabase.lichess).toJson(), + ), + }, + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); + + final moves = ['d4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); + }, variant: kPlatformVariant); + + testWidgets('player opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + userSession: session, + defaultPreferences: { + SessionPreferencesStorage.key( + PrefCategory.openingExplorer.storageKey, + session, + ): jsonEncode( + OpeningExplorerPrefs.defaults(user: user).copyWith(db: OpeningDatabase.player).toJson(), + ), + }, + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); + + final moves = ['c4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); + }, variant: kPlatformVariant); + }); +} + +const mastersOpeningExplorerResponse = ''' +{ + "white": 834333, + "draws": 1085272, + "black": 600303, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 2399, + "white": 372266, + "draws": 486092, + "black": 280238, + "game": null + }, + { + "uci": "d2d4", + "san": "d4", + "averageRating": 2414, + "white": 302160, + "draws": 397224, + "black": 209077, + "game": null + } + ], + "topGames": [ + { + "uci": "d2d4", + "id": "QR5UbqUY", + "winner": null, + "black": { + "name": "Caruana, F.", + "rating": 2818 + }, + "white": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "year": 2019, + "month": "2019-08" + }, + { + "uci": "e2e4", + "id": "Sxov6E94", + "winner": "white", + "black": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "white": { + "name": "Firouzja, A.", + "rating": 2808 + }, + "year": 2019, + "month": "2019-08" + } + ], + "opening": null +} +'''; + +const lichessOpeningExplorerResponse = ''' +{ + "white": 2848672002, + "draws": 225287646, + "black": 2649860106, + "moves": [ + { + "uci": "d2d4", + "san": "d4", + "averageRating": 1604, + "white": 1661457614, + "draws": 129433754, + "black": 1565161663, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "rapid", + "mode": "rated", + "black": { + "name": "Jcats1", + "rating": 1548 + }, + "white": { + "name": "carlosrivero32", + "rating": 1690 + }, + "year": 2024, + "month": "2024-06" + } + ], + "topGames": [], + "opening": null +} +'''; + +const playerOpeningExplorerResponse = ''' +{ + "white": 1713, + "draws": 119, + "black": 1459, + "moves": [ + { + "uci": "c2c4", + "san": "c4", + "averageOpponentRating": 1767, + "performance": 1796, + "white": 1691, + "draws": 116, + "black": 1432, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "bullet", + "mode": "rated", + "black": { + "name": "foo", + "rating": 1869 + }, + "white": { + "name": "baz", + "rating": 1912 + }, + "year": 2023, + "month": "2023-08" + } + ], + "opening": null, + "queuePosition": 0 +} +'''; diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart new file mode 100644 index 0000000000..206f017347 --- /dev/null +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -0,0 +1,221 @@ +import 'dart:io'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + group('Playing over the board (offline)', () { + testWidgets('Checkmate and Rematch', (tester) async { + final boardRect = await initOverTheBoardGame(tester, const TimeIncrement(60, 5)); + + // Default orientation is white at the bottom + expect( + tester.getBottomLeft(find.byKey(const ValueKey('a1-whiterook'))), + boardRect.bottomLeft, + ); + + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'f7', 'f6'); + await playMove(tester, 'd2', 'd4'); + await playMove(tester, 'g7', 'g5'); + await playMove(tester, 'd1', 'h5'); + + await tester.pumpAndSettle(const Duration(milliseconds: 600)); + expect(find.text('Checkmate • White is victorious'), findsOneWidget); + + await tester.tap(find.text('Rematch')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(Chessboard))); + final gameState = container.read(overTheBoardGameControllerProvider); + expect(gameState.game.steps.length, 1); + expect(gameState.game.steps.first.position, Chess.initial); + + // Rematch should flip orientation + expect(tester.getTopRight(find.byKey(const ValueKey('a1-whiterook'))), boardRect.topRight); + + expect(activeClock(tester), null); + }); + + testWidgets('Game ends when out of time', (tester) async { + const time = Duration(seconds: 1); + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); + + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); + + // The clock measures system time internally, so we need to actually sleep in order + // for the clock to reach 0, instead of using tester.pump() + sleep(time + const Duration(milliseconds: 100)); + + // Now for game result dialog to show up + await tester.pumpAndSettle(const Duration(milliseconds: 600)); + expect(find.text('White time out • Black is victorious'), findsOneWidget); + + await tester.tap(find.text('Rematch')); + expect(activeClock(tester), null); + }); + + testWidgets('Pausing the clock', (tester) async { + const time = Duration(seconds: 10); + + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); + + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); + + await tester.tap(find.byTooltip('Pause')); + await tester.pump(); + + expect(activeClock(tester), null); + + await tester.tap(find.byTooltip('Resume')); + await tester.pump(); + + expect(activeClock(tester), Side.white); + + // Going back a move should not unpause... + await tester.tap(find.byTooltip('Pause')); + await tester.pump(); + await tester.tap(find.byTooltip('Previous')); + await tester.pump(); + + expect(activeClock(tester), null); + + // ... but playing a move resumes the clock + await playMove(tester, 'd7', 'd5'); + + expect(activeClock(tester), Side.white); + }); + + testWidgets('Go back and Forward', (tester) async { + const time = Duration(seconds: 10); + + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); + + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); + + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e7-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.black); + + await tester.tap(find.byTooltip('Next')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e5-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.white); + + // Go back all the way to the initial position + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e2-whitepawn')), findsOneWidget); + expect(find.byKey(const ValueKey('e7-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.white); + + await playMove(tester, 'e2', 'e4'); + expect(find.byKey(const ValueKey('e4-whitepawn')), findsOneWidget); + + expect(activeClock(tester), Side.black); + }); + + testWidgets('No clock if time is infinite', (tester) async { + await initOverTheBoardGame(tester, const TimeIncrement(0, 0)); + + expect(find.byType(Clock), findsNothing); + }); + + testWidgets('Clock logic', (tester) async { + const time = Duration(minutes: 5); + + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 3)); + + expect(activeClock(tester), null); + + expect(findWhiteClock(tester).timeLeft, time); + expect(findBlackClock(tester).timeLeft, time); + + await playMove(tester, 'e2', 'e4'); + + const moveTime = Duration(milliseconds: 500); + await tester.pumpAndSettle(moveTime); + + expect(activeClock(tester), Side.black); + + expect(findWhiteClock(tester).timeLeft, time); + expect(findBlackClock(tester).timeLeft, lessThan(time)); + + await playMove(tester, 'e7', 'e5'); + await tester.pumpAndSettle(); + + expect(activeClock(tester), Side.white); + + // Expect increment to be added + expect(findBlackClock(tester).timeLeft, greaterThan(time)); + + expect(findWhiteClock(tester).timeLeft, lessThan(time)); + }); + }); +} + +Future initOverTheBoardGame(WidgetTester tester, TimeIncrement timeIncrement) async { + final app = await makeTestProviderScopeApp(tester, home: const OverTheBoardScreen()); + await tester.pumpWidget(app); + + await tester.pumpAndSettle(); + await tester.tap(find.text('Play')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(Chessboard))); + container.read(overTheBoardClockProvider.notifier).setupClock(timeIncrement); + await tester.pumpAndSettle(); + + return tester.getRect(find.byType(Chessboard)); +} + +Side? activeClock(WidgetTester tester, {Side orientation = Side.white}) { + final whiteClock = findWhiteClock(tester, orientation: orientation); + final blackClock = findBlackClock(tester, orientation: orientation); + + if (whiteClock.active) { + expect(blackClock.active, false); + return Side.white; + } + + if (blackClock.active) { + expect(whiteClock.active, false); + return Side.black; + } + + return null; +} + +Clock findWhiteClock(WidgetTester tester, {Side orientation = Side.white}) { + return tester.widget( + find.byKey(ValueKey(orientation == Side.white ? 'bottomClock' : 'topClock')), + ); +} + +Clock findBlackClock(WidgetTester tester, {Side orientation = Side.white}) { + return tester.widget( + find.byKey(ValueKey(orientation == Side.white ? 'topClock' : 'bottomClock')), + ); +} diff --git a/test/view/puzzle/example_data.dart b/test/view/puzzle/example_data.dart new file mode 100644 index 0000000000..9bf19cde6b --- /dev/null +++ b/test/view/puzzle/example_data.dart @@ -0,0 +1,48 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; + +final puzzle = Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('6Sz3s'), + initialPly: 40, + plays: 68176, + rating: 1984, + solution: IList(const ['h4h2', 'h1h2', 'e5f3', 'h2h3', 'b4h4']), + themes: ISet(const ['middlegame', 'attraction', 'long', 'mateIn3', 'sacrifice', 'doubleCheck']), + ), + game: const PuzzleGame( + rated: true, + id: GameId('zgBwsXLr'), + perf: Perf.blitz, + pgn: + 'e4 c5 Nf3 e6 c4 Nc6 d4 cxd4 Nxd4 Bc5 Nxc6 bxc6 Be2 Ne7 O-O Ng6 Nc3 Rb8 Kh1 Bb7 f4 d5 f5 Ne5 fxe6 fxe6 cxd5 cxd5 exd5 Bxd5 Qa4+ Bc6 Qf4 Bd6 Ne4 Bxe4 Qxe4 Rb4 Qe3 Qh4 Qxa7', + black: PuzzleGamePlayer(side: Side.black, name: 'CAMBIADOR'), + white: PuzzleGamePlayer(side: Side.white, name: 'arroyoM10'), + ), +); + +final batch = PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle])); + +final puzzle2 = Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('2nNdI'), + rating: 1090, + plays: 23890, + initialPly: 88, + solution: IList(const ['g4h4', 'h8h4', 'b4h4']), + themes: ISet(const {'endgame', 'short', 'crushing', 'fork', 'queenRookEndgame'}), + ), + game: const PuzzleGame( + id: GameId('w32JTzEf'), + perf: Perf.blitz, + rated: true, + white: PuzzleGamePlayer(side: Side.white, name: 'Li', title: null), + black: PuzzleGamePlayer(side: Side.black, name: 'Gabriela', title: null), + pgn: + 'e4 e5 Nf3 Nc6 Bb5 a6 Ba4 b5 Bb3 Nf6 c3 Nxe4 d4 exd4 cxd4 Qe7 O-O Qd8 Bd5 Nf6 Bb3 Bd6 Nc3 O-O Bg5 h6 Bh4 g5 Nxg5 hxg5 Bxg5 Kg7 Ne4 Be7 Bxf6+ Bxf6 Qg4+ Kh8 Qh5+ Kg8 Qg6+ Kh8 Qxf6+ Qxf6 Nxf6 Nxd4 Rfd1 Ne2+ Kh1 d6 Rd5 Kg7 Nh5+ Kh6 Rad1 Be6 R5d2 Bxb3 axb3 Kxh5 Rxe2 Rfe8 Red2 Re5 h3 Rae8 Kh2 Re2 Rd5+ Kg6 f4 Rxb2 R1d3 Ree2 Rg3+ Kf6 h4 Re4 Rg4 Rxb3 h5 Rbb4 h6 Rxf4 h7 Rxg4 h8=Q+ Ke7 Rd3', + ), +); diff --git a/test/view/puzzle/puzzle_history_screen_test.dart b/test/view/puzzle/puzzle_history_screen_test.dart new file mode 100644 index 0000000000..2f93dcaf4a --- /dev/null +++ b/test/view/puzzle/puzzle_history_screen_test.dart @@ -0,0 +1,177 @@ +import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_history_screen.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; + +import '../../model/auth/fake_session_storage.dart'; +import '../../model/puzzle/mock_server_responses.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + final Map mockActivityRequestsCount = {}; + + tearDown(() { + mockActivityRequestsCount.clear(); + }); + + MockClient makeClient(int totalNumberOfPuzzles) => MockClient((request) { + if (request.url.path == '/api/puzzle/activity') { + final query = request.url.queryParameters; + final max = int.parse(query['max']!); + final beforeDateParam = query['before']; + final beforeDate = + beforeDateParam != null + ? DateTime.fromMillisecondsSinceEpoch(int.parse(beforeDateParam)) + : null; + final totalAlreadyRequested = mockActivityRequestsCount.values.fold(0, (p, e) => p + e); + + if (totalAlreadyRequested >= totalNumberOfPuzzles) { + return mockResponse('', 200); + } + + final key = beforeDate != null ? DateFormat.yMd().format(beforeDate) : null; + + final nbPuzzles = math.min(max, totalNumberOfPuzzles); + mockActivityRequestsCount[key] = (mockActivityRequestsCount[key] ?? 0) + nbPuzzles; + return mockResponse(generateHistory(nbPuzzles, beforeDate), 200); + } else if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(mockMixBatchResponse, 200); + } else if (request.url.path.startsWith('/api/puzzle')) { + return mockResponse(''' +{"game":{"id":"MNMYnEjm","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"name":"Igor76","id":"igor76","color":"white","rating":2211},{"name":"dmitriy_duyun","id":"dmitriy_duyun","color":"black","rating":2180}],"pgn":"e4 c6 d4 d5 Nc3 g6 Nf3 Bg7 h3 dxe4 Nxe4 Nf6 Bd3 Nxe4 Bxe4 Nd7 O-O Nf6 Bd3 O-O Re1 Bf5 Bxf5 gxf5 c3 e6 Bg5 Qb6 Qc2 Rac8 Ne5 Qc7 Rad1 Nd7 Bf4 Nxe5 Bxe5 Bxe5 Rxe5 Rcd8 Qd2 Kh8 Rde1 Rg8 Qf4","clock":"20+15"},"puzzle":{"id":"0XqV2","rating":1929,"plays":93270,"solution":["f7f6","e5f5","c7g7","g2g3","e6f5"],"themes":["clearance","endgame","advantage","intermezzo","long"],"initialPly":44}} +''', 200); + } + return mockResponse('', 404); + }); + + testWidgets('Displays an initial list of puzzles', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleHistoryScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(makeClient(4), ref); + }), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + + // wait for puzzles to load + await tester.pump(const Duration(milliseconds: 20)); + + expect(mockActivityRequestsCount, equals({null: 4})); + + expect(find.byType(BoardThumbnail), findsNWidgets(4)); + + expect(find.text(DateFormat.yMMMd().format(firstPageDate)), findsOneWidget); + }); + + testWidgets('Scrolling down loads next page', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleHistoryScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(makeClient(80), ref); + }), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + + // wait for puzzles to load + await tester.pump(const Duration(milliseconds: 20)); + + // first page has 20 puzzles + expect(mockActivityRequestsCount, equals({null: 20})); + + // not everything will be displayed but we should see at least first 2 rows + expect(find.byType(BoardThumbnail), findsAtLeastNWidgets(4)); + + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'Bnull20', + description: 'last item of 1st page', + ), + 400, + ); + + // next pages have 50 puzzles + expect(mockActivityRequestsCount, equals({null: 20, '1/31/2024': 50})); + + // by the time we've scrolled to the end the next puzzles are already here + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3150', + description: 'last item of 2nd page', + ), + 1000, + ); + + // one more page + expect(mockActivityRequestsCount, equals({null: 20, '1/31/2024': 50, '1/30/2024': 50})); + + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + description: 'last item of 3rd page', + ), + 400, + ); + + // no more items + expect(mockActivityRequestsCount.length, 3); + + // wait for the scroll to finish + await tester.pumpAndSettle(); + + await tester.tap( + find.byWidgetPredicate( + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + ), + ); + + await tester.pumpAndSettle(); + expect(find.byType(PuzzleScreen), findsOneWidget); + + // go back, should be on the same page and history still loaded + await tester.pageBack(); + await tester.pumpAndSettle(); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + ), + findsOneWidget, + ); + }); +} + +// a date to use for the first page; all the puzzle with have the same date per +// page for simplification +final firstPageDate = DateTime.parse('2024-01-31 10:00:00'); + +String generateHistory(int count, DateTime? maybeDate) { + final buffer = StringBuffer(); + for (int i = 0; i < count; i++) { + final date = maybeDate?.subtract(const Duration(days: 1)) ?? firstPageDate; + final id = 'B${maybeDate?.day}${i + 1}'; + buffer.writeln(''' +{ "date": ${date.millisecondsSinceEpoch}, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "$id", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +'''); + } + return buffer.toString(); +} diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index f7e47eb86f..5789fcb318 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -6,21 +6,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:mocktail/mocktail.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; +import 'example_data.dart'; class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} @@ -28,12 +26,7 @@ class MockPuzzleStorage extends Mock implements PuzzleStorage {} void main() { setUpAll(() { - registerFallbackValue( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([puzzle]), - ), - ); + registerFallbackValue(PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle]))); registerFallbackValue(puzzle); }); @@ -41,74 +34,70 @@ void main() { final mockHistoryStorage = MockPuzzleStorage(); group('PuzzleScreen', () { - testWidgets( - 'meets accessibility guidelines', - variant: kPlatformVariant, - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', variant: kPlatformVariant, ( + WidgetTester tester, + ) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.puzzle.id, - ), - overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle.puzzle.id, + ), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id)) - .thenAnswer((_) async => puzzle); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id), + ).thenAnswer((_) async => puzzle); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - await meetsTapTargetGuideline(tester); + await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - handle.dispose(); - }, - ); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }); - testWidgets( - 'Loads puzzle directly by passing a puzzleId', - variant: kPlatformVariant, - (tester) async { - final app = await buildTestApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.puzzle.id, - ), - overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + testWidgets('Loads puzzle directly by passing a puzzleId', variant: kPlatformVariant, ( + tester, + ) async { + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle.puzzle.id, + ), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id)) - .thenAnswer((_) async => puzzle); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id), + ).thenAnswer((_) async => puzzle); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); - }, - ); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); + }); testWidgets('Loads next puzzle when no puzzleId is passed', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, - home: const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), + home: const PuzzleScreen(angle: PuzzleTheme(PuzzleThemeKey.mix)), overrides: [ puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), @@ -116,144 +105,134 @@ void main() { ); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); await tester.pumpWidget(app); - expect(find.byType(cg.Board), findsNothing); + expect(find.byType(Chessboard), findsNothing); expect(find.text('Your turn'), findsNothing); // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); }); - testWidgets( - 'solves a puzzle and loads the next one', - variant: kPlatformVariant, - (tester) async { - final mockClient = MockClient((request) { - if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse(batchOf1, 200); - } - return mockResponse('', 404); - }); + testWidgets('solves a puzzle and loads the next one', variant: kPlatformVariant, ( + tester, + ) async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(batchOf1, 200); + } + return mockResponse('', 404); + }); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); - final app = await buildTestApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle2.puzzle.id, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - puzzleBatchStorageProvider.overrideWith((ref) { - return mockBatchStorage; - }), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle2.puzzle.id, + ), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + puzzleBatchStorageProvider.overrideWith((ref) { + return mockBatchStorage; + }), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); - when(saveDBReq).thenAnswer((_) async {}); - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), - ).thenAnswer((_) async => batch); + Future saveDBReq() => mockBatchStorage.save( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); + when(saveDBReq).thenAnswer((_) async {}); + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); - await tester.pumpWidget(app); - - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpWidget(app); - expect(find.byType(cg.Board), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); - await tester.tap(find.byKey(const Key('g4-blackRook'))); - await tester.pump(); - expect(find.byKey(const Key('g4-selected')), findsNothing); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); - const orientation = cg.Side.black; + // before the first move is played, puzzle is not interactable + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + await tester.tap(find.byKey(const Key('g4-blackrook'))); + await tester.pump(); + expect(find.byKey(const Key('g4-selected')), findsNothing); - // await for first move to be played - await tester.pump(const Duration(milliseconds: 1500)); + const orientation = Side.black; - // in play mode we don't see the continue button - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsNothing); - // in play mode we see the solution button - expect(find.byIcon(Icons.help), findsOneWidget); + // await for first move to be played + await tester.pump(const Duration(milliseconds: 1500)); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); - expect(find.byKey(const Key('h8-whiteQueen')), findsOneWidget); + // in play mode we don't see the continue button + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsNothing); + // in play mode we see the solution button + expect(find.byIcon(Icons.help), findsOneWidget); - final boardRect = tester.getRect(find.byType(cg.Board)); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); - expect(find.text('Best move!'), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.text('Best move!'), findsOneWidget); - // wait for line reply and move animation - await tester.pump(const Duration(milliseconds: 500)); - await tester.pumpAndSettle(); + // wait for line reply and move animation + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-whiteQueen')), findsOneWidget); + expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); - expect(find.text('Success!'), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.text('Success!'), findsOneWidget); - // wait for move animation - await tester.pumpAndSettle(); + // wait for move animation + await tester.pumpAndSettle(); - // called once to save solution and once after fetching a new puzzle - verify(saveDBReq).called(2); + // called once to save solution and once after fetching a new puzzle + verify(saveDBReq).called(2); - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); - expect(find.byIcon(Icons.help), findsNothing); + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); + expect(find.byIcon(Icons.help), findsNothing); - await tester.tap(find.byIcon(CupertinoIcons.play_arrow_solid)); + await tester.tap(find.byIcon(CupertinoIcons.play_arrow_solid)); - // await for new puzzle load - await tester.pump(const Duration(milliseconds: 500)); + // await for new puzzle load + await tester.pump(const Duration(milliseconds: 500)); - expect(find.text('Success!'), findsNothing); - expect(find.text('Your turn'), findsOneWidget); + expect(find.text('Success!'), findsNothing); + expect(find.text('Your turn'), findsOneWidget); - // await for view solution timer - await tester.pump(const Duration(seconds: 4)); - }, - ); + // await for view solution timer + await tester.pump(const Duration(seconds: 4)); + }); for (final showRatings in [true, false]) { - testWidgets('fails a puzzle, (showRatings: $showRatings)', - variant: kPlatformVariant, (tester) async { + testWidgets('fails a puzzle, (showRatings: $showRatings)', variant: kPlatformVariant, ( + tester, + ) async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { return mockResponse(batchOf1, 200); @@ -261,10 +240,11 @@ void main() { return mockResponse('', 404); }); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -284,20 +264,16 @@ void main() { ], ); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); when(saveDBReq).thenAnswer((_) async {}); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); await tester.pumpWidget(app); @@ -305,48 +281,40 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); - const orientation = cg.Side.black; + const orientation = Side.black; // await for first move to be played await tester.pump(const Duration(milliseconds: 1500)); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); - - final boardRect = tester.getRect(find.byType(cg.Board)); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - await playMove(tester, boardRect, 'g4', 'f4', orientation: orientation); + await playMove(tester, 'g4', 'f4', orientation: orientation); - expect( - find.text("That's not the move!"), - findsOneWidget, - ); + expect(find.text("That's not the move!"), findsOneWidget); // wait for move cancel and animation await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); // can still play the puzzle - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); // wait for line reply and move animation await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); - expect( - find.text('Puzzle complete!'), - findsOneWidget, - ); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.text('Puzzle complete!'), findsOneWidget); final expectedPlayedXTimes = 'Played ${puzzle2.puzzle.plays.toString().localizeNumbers()} times.'; expect( @@ -366,177 +334,87 @@ void main() { }); } - testWidgets( - 'view solution', - variant: kPlatformVariant, - (tester) async { - final mockClient = MockClient((request) { - if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse(batchOf1, 200); - } - return mockResponse('', 404); - }); + testWidgets('view solution', variant: kPlatformVariant, (tester) async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(batchOf1, 200); + } + return mockResponse('', 404); + }); - final app = await buildTestApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle2.puzzle.id, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - puzzleBatchStorageProvider.overrideWith((ref) { - return mockBatchStorage; - }), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle2.puzzle.id, + ), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + puzzleBatchStorageProvider.overrideWith((ref) { + return mockBatchStorage; + }), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); - Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); - when(saveDBReq).thenAnswer((_) async {}); - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), - ).thenAnswer((_) async => batch); + Future saveDBReq() => mockBatchStorage.save( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); + when(saveDBReq).thenAnswer((_) async {}); + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); - // await for first move to be played and view solution button to appear - await tester.pump(const Duration(seconds: 5)); + // await for first move to be played and view solution button to appear + await tester.pump(const Duration(seconds: 5)); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - expect(find.byIcon(Icons.help), findsOneWidget); - await tester.tap(find.byIcon(Icons.help)); + expect(find.byIcon(Icons.help), findsOneWidget); + await tester.tap(find.byIcon(Icons.help)); - // wait for solution replay animation to finish - await tester.pump(const Duration(seconds: 1)); - await tester.pumpAndSettle(); + // wait for solution replay animation to finish + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); - expect(find.byKey(const Key('h8-whiteQueen')), findsOneWidget); - expect( - find.text('Puzzle complete!'), - findsOneWidget, - ); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); + expect(find.text('Puzzle complete!'), findsOneWidget); - final nextMoveBtnEnabled = find.byWidgetPredicate( - (widget) => - widget is BottomBarButton && - widget.icon == CupertinoIcons.chevron_forward && - widget.enabled, - ); - expect(nextMoveBtnEnabled, findsOneWidget); + final nextMoveBtnEnabled = find.byWidgetPredicate( + (widget) => + widget is BottomBarButton && + widget.icon == CupertinoIcons.chevron_forward && + widget.enabled, + ); + expect(nextMoveBtnEnabled, findsOneWidget); - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); - // called once to save solution and once after fetching a new puzzle - verify(saveDBReq).called(2); - }, - ); + // called once to save solution and once after fetching a new puzzle + verify(saveDBReq).called(2); + }); }); } -final puzzle = Puzzle( - puzzle: PuzzleData( - id: const PuzzleId('6Sz3s'), - initialPly: 40, - plays: 68176, - rating: 1984, - solution: IList(const [ - 'h4h2', - 'h1h2', - 'e5f3', - 'h2h3', - 'b4h4', - ]), - themes: ISet(const [ - 'middlegame', - 'attraction', - 'long', - 'mateIn3', - 'sacrifice', - 'doubleCheck', - ]), - ), - game: const PuzzleGame( - rated: true, - id: GameId('zgBwsXLr'), - perf: Perf.blitz, - pgn: - 'e4 c5 Nf3 e6 c4 Nc6 d4 cxd4 Nxd4 Bc5 Nxc6 bxc6 Be2 Ne7 O-O Ng6 Nc3 Rb8 Kh1 Bb7 f4 d5 f5 Ne5 fxe6 fxe6 cxd5 cxd5 exd5 Bxd5 Qa4+ Bc6 Qf4 Bd6 Ne4 Bxe4 Qxe4 Rb4 Qe3 Qh4 Qxa7', - black: PuzzleGamePlayer( - side: Side.black, - name: 'CAMBIADOR', - ), - white: PuzzleGamePlayer( - side: Side.white, - name: 'arroyoM10', - ), - ), -); - -final batch = PuzzleBatch( - solved: IList(const []), - unsolved: IList([ - puzzle, - ]), -); - -final puzzle2 = Puzzle( - puzzle: PuzzleData( - id: const PuzzleId('2nNdI'), - rating: 1090, - plays: 23890, - initialPly: 88, - solution: IList(const ['g4h4', 'h8h4', 'b4h4']), - themes: ISet(const { - 'endgame', - 'short', - 'crushing', - 'fork', - 'queenRookEndgame', - }), - ), - game: const PuzzleGame( - id: GameId('w32JTzEf'), - perf: Perf.blitz, - rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'Li', - title: null, - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'Gabriela', - title: null, - ), - pgn: - 'e4 e5 Nf3 Nc6 Bb5 a6 Ba4 b5 Bb3 Nf6 c3 Nxe4 d4 exd4 cxd4 Qe7 O-O Qd8 Bd5 Nf6 Bb3 Bd6 Nc3 O-O Bg5 h6 Bh4 g5 Nxg5 hxg5 Bxg5 Kg7 Ne4 Be7 Bxf6+ Bxf6 Qg4+ Kh8 Qh5+ Kg8 Qg6+ Kh8 Qxf6+ Qxf6 Nxf6 Nxd4 Rfd1 Ne2+ Kh1 d6 Rd5 Kg7 Nh5+ Kh6 Rad1 Be6 R5d2 Bxb3 axb3 Kxh5 Rxe2 Rfe8 Red2 Re5 h3 Rae8 Kh2 Re2 Rd5+ Kg6 f4 Rxb2 R1d3 Ree2 Rg3+ Kf6 h4 Re4 Rg4 Rxb3 h5 Rbb4 h6 Rxf4 h7 Rxg4 h8=Q+ Ke7 Rd3', - ), -); - const batchOf1 = ''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"name":"silverjo", "rating":1777,"color":"white"},{"name":"Robyarchitetto", "rating":1742,"color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}]} '''; diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart new file mode 100644 index 0000000000..188a18c0db --- /dev/null +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -0,0 +1,308 @@ +import 'dart:convert'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import '../../model/puzzle/mock_server_responses.dart'; +import '../../network/fake_http_client_factory.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; +import 'example_data.dart'; + +final mockClient = MockClient((request) async { + if (request.url.path == '/api/puzzle/daily') { + return mockResponse(mockDailyPuzzleResponse, 200); + } + return mockResponse('', 404); +}); + +class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} + +void main() { + setUpAll(() { + registerFallbackValue(PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle]))); + }); + + final mockBatchStorage = MockPuzzleBatchStorage(); + + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage)], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + await meetsTapTargetGuideline(tester); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + + testWidgets('shows puzzle menu', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.text('Puzzle Themes'), findsOneWidget); + expect(find.text('Puzzle Streak'), findsOneWidget); + expect(find.text('Puzzle Storm'), findsOneWidget); + }); + + testWidgets('shows daily puzzle', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(DailyPuzzle), findsOneWidget); + expect(find.widgetWithText(DailyPuzzle, 'Puzzle of the day'), findsOneWidget); + expect(find.widgetWithText(DailyPuzzle, 'Played 93,270 times'), findsOneWidget); + expect(find.widgetWithText(DailyPuzzle, 'Black to play'), findsOneWidget); + }); + + group('tactical training preview', () { + testWidgets('shows first puzzle from unsolved batch', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PuzzleAnglePreview), findsOneWidget); + expect(find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget); + final chessboard = + find + .descendant( + of: find.byType(PuzzleAnglePreview), + matching: find.byType(Chessboard), + ) + .evaluate() + .first + .widget + as Chessboard; + + expect(chessboard.fen, equals('4k2r/Q5pp/3bp3/4n3/1r5q/8/PP2B1PP/R1B2R1K b k - 0 21')); + }); + + testWidgets('shows saved puzzle batches', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.advancedPawn), + ), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleOpening('A00')), + ).thenAnswer((_) async => batch); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer( + (_) async => IList(const [ + (PuzzleTheme(PuzzleThemeKey.advancedPawn), 50), + (PuzzleOpening('A00'), 50), + ]), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + await tester.scrollUntilVisible(find.widgetWithText(PuzzleAnglePreview, 'A00'), 200); + expect(find.byType(PuzzleAnglePreview), findsNWidgets(3)); + expect(find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget); + expect(find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), findsOneWidget); + expect(find.widgetWithText(PuzzleAnglePreview, 'A00'), findsOneWidget); + }); + + testWidgets('delete a saved puzzle batch', (WidgetTester tester) async { + final testDb = await openAppDatabase(databaseFactoryFfiNoIsolate, inMemoryDatabasePath); + + for (final (angle, timestamp) in [ + (const PuzzleTheme(PuzzleThemeKey.mix), '2021-01-01T00:00:00Z'), + (const PuzzleTheme(PuzzleThemeKey.advancedPawn), '2021-01-01T00:00:00Z'), + (const PuzzleOpening('A00'), '2021-01-02T00:00:00Z'), + ]) { + await testDb.insert('puzzle_batchs', { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(onePuzzleBatch.toJson()), + 'lastModified': timestamp, + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + databaseProvider.overrideWith((ref) async { + ref.onDispose(testDb.close); + return testDb; + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + await tester.scrollUntilVisible( + find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), + 200, + ); + + expect(find.byType(PuzzleAnglePreview), findsNWidgets(3)); + + await tester.drag( + find.descendant( + of: find.widgetWithText(PuzzleAnglePreview, 'A00'), + matching: find.byType(Slidable), + ), + const Offset(-150, 0), + ); + await tester.pumpAndSettle(); + + expect(find.widgetWithText(SlidableAction, 'Delete'), findsOneWidget); + + await tester.tap(find.widgetWithText(SlidableAction, 'Delete')); + + await tester.pumpAndSettle(); + + expect(find.widgetWithText(PuzzleAnglePreview, 'A00'), findsNothing); + }); + }); +} + +final onePuzzleBatch = PuzzleBatch( + solved: IList(const [ + PuzzleSolution(id: PuzzleId('pId'), win: true, rated: true), + PuzzleSolution(id: PuzzleId('pId2'), win: false, rated: true), + ]), + unsolved: IList([ + Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('pId3'), + rating: 1988, + plays: 5, + initialPly: 23, + solution: IList(const ['a6a7', 'b2a2', 'c4c2']), + themes: ISet(const ['endgame', 'advantage']), + ), + game: const PuzzleGame( + id: GameId('PrlkCqOv'), + perf: Perf.blitz, + rated: true, + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), + pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + ), + ), + ]), +); diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 7e38602448..679eb5ddf1 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -1,17 +1,18 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/puzzle/storm_screen.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/storm') { @@ -22,148 +23,101 @@ final client = MockClient((request) { void main() { group('StormScreen', () { - testWidgets( - 'meets accessibility guidelines', - (tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - - final app = await buildTestApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - handle.dispose(); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'Load puzzle and play white pieces', - (tester) async { - final app = await buildTestApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - expect(find.byType(cg.Board), findsOneWidget); - expect( - find.text('You play the white pieces in all puzzles'), - findsWidgets, - ); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'Play one puzzle', - (tester) async { - final app = await buildTestApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('h5-whiteRook')), findsOneWidget); - await tester.tap(find.byKey(const Key('h5-whiteRook'))); - await tester.pump(); - expect(find.byKey(const Key('h5-selected')), findsNothing); - - // wait for first move to be played - await tester.pump(const Duration(seconds: 1)); - - expect(find.byKey(const Key('g8-blackKing')), findsOneWidget); - - final boardRect = tester.getRect(find.byType(cg.Board)); - - await playMove( - tester, - boardRect, - 'h5', - 'h7', - orientation: cg.Side.white, - ); - - await tester.pump(const Duration(milliseconds: 500)); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('h7-whiteRook')), findsOneWidget); - expect(find.byKey(const Key('d1-blackQueen')), findsOneWidget); - - await playMove( - tester, - boardRect, - 'e3', - 'g1', - orientation: cg.Side.white, - ); - - await tester.pump(const Duration(milliseconds: 500)); - - // should have loaded next puzzle - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); - }, - variant: kPlatformVariant, - ); + testWidgets('meets accessibility guidelines', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - testWidgets('shows end run result', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), ], ); await tester.pumpWidget(app); - // wait for first move to be played - await tester.pump(const Duration(seconds: 1)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); + + testWidgets('Load puzzle and play white pieces', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], + ); + + await tester.pumpWidget(app); - final boardRect = tester.getRect(find.byType(cg.Board)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('You play the white pieces in all puzzles'), findsWidgets); + }, variant: kPlatformVariant); - await playMove( + testWidgets('Play one puzzle', (tester) async { + final app = await makeTestProviderScopeApp( tester, - boardRect, - 'h5', - 'h7', - orientation: cg.Side.white, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], ); + await tester.pumpWidget(app); + + // before the first move is played, puzzle is not interactable + expect(find.byKey(const Key('h5-whiterook')), findsOneWidget); + await tester.tap(find.byKey(const Key('h5-whiterook'))); + await tester.pump(); + expect(find.byKey(const Key('h5-selected')), findsNothing); + + // wait for first move to be played + await tester.pump(const Duration(seconds: 1)); + + expect(find.byKey(const Key('g8-blackking')), findsOneWidget); + + await playMove(tester, 'h5', 'h7', orientation: Side.white); + + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('h7-whiterook')), findsOneWidget); + expect(find.byKey(const Key('d1-blackqueen')), findsOneWidget); + + await playMove(tester, 'e3', 'g1', orientation: Side.white); + await tester.pump(const Duration(milliseconds: 500)); - await playMove( + + // should have loaded next puzzle + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); + }, variant: kPlatformVariant); + + testWidgets('shows end run result', (tester) async { + final app = await makeTestProviderScopeApp( tester, - boardRect, - 'e3', - 'g1', - orientation: cg.Side.white, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], ); + await tester.pumpWidget(app); + + // wait for first move to be played + await tester.pump(const Duration(seconds: 1)); + + await playMove(tester, 'h5', 'h7', orientation: Side.white); + + await tester.pump(const Duration(milliseconds: 500)); + await playMove(tester, 'e3', 'g1', orientation: Side.white); + await tester.pump(const Duration(milliseconds: 500)); // should have loaded next puzzle - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); await tester.tap(find.text('End run')); await tester.pumpAndSettle(); @@ -172,25 +126,23 @@ void main() { }); testWidgets('play wrong move', (tester) async { - final app = await buildTestApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), ], ); await tester.pumpWidget(app); await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(cg.Board)); - await playMove(tester, boardRect, 'h5', 'h6'); + await playMove(tester, 'h5', 'h6'); await tester.pump(const Duration(milliseconds: 500)); - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); }); }); } diff --git a/test/view/puzzle/streak_screen_test.dart b/test/view/puzzle/streak_screen_test.dart new file mode 100644 index 0000000000..866b3659cb --- /dev/null +++ b/test/view/puzzle/streak_screen_test.dart @@ -0,0 +1,314 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/puzzle/streak_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + if (request.url.path == '/api/streak') { + return mockResponse(''' + { + "game": { + "id": "Xndtxsoa", + "perf": { + "key": "blitz", + "name": "Blitz" + }, + "rated": true, + "players": [ + { + "name": "VincV", + "id": "vincv", + "color": "white", + "rating": 1889 + }, + { + "name": "Rex9646", + "id": "rex9646", + "color": "black", + "rating": 1881 + } + ], + "pgn": "e4 c5 Nf3 e6 d4 cxd4 Nxd4 a6 Be2 Nf6 Nc3 Bb4 e5 Ne4 Bd2 Bxc3 Bxc3 Nxc3 bxc3 Qa5 O-O Qxe5 Re1 O-O Bxa6 Qc5 Bd3 Nc6 Re3 f5 Rh3 h6 Nxc6 bxc6 c4 Ra3 Qh5 Qd4 Rf1 Rxa2 Rg3 Kh8 Rg6 Rf6 Rxf6 Qxf6 Qe8+ Kh7 Qxc8 Qe7 h3 Qf7 Re1 Qe7 Bxf5+ g6 Bd3 Kg7 Qb7 Ra5 Rd1 Re5 Bxg6 Kxg6 Rxd7 Qf6 Qc8", + "clock": "3+2" + }, + "puzzle": { + "id": "MptxK", + "rating": 1012, + "plays": 5557, + "solution": [ + "e5e1", + "g1h2", + "f6f4", + "g2g3", + "f4f2" + ], + "themes": [ + "endgame", + "long", + "mateIn3" + ], + "initialPly": 66 + }, + "angle": { + "key": "mix", + "name": "Puzzle Themes", + "desc": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games." + }, + "streak": "MptxK 4CZxz kcN3a 1I9Ly kOx90 eTrkO G0tpf iwTxQ tg2IU TovLC 0miTI Jpmkf 8VqjS XftoM 70UGG lm8O8 R4Y49 76Llk XZOyq QUgzo dACnQ qFjLp ytKo4 6JIj1 SYz3x kEkib dkvMp Dk0Ln Ok3qk zbRCc fQSVb vmDLx VJw06 3up01 X9aHm EicvD 5lhwD fTJE0 08LZy XAsVO TVB8s VCLTk KH6zc CaByR E2dUi JOxJg Agtzu KwbY9 Rmcf7 k9jGo 0zTgd 5YCx8 BtqDp DQdRO ytwPd sHqWB 1WunB Fovke mmMDN UNcwu isI02 3sIJB mnuzi 4aaRt Jvkvj UsXO2 kLfmz gsC1H TADGH a0Jz6 oUPR2 1IOBO 9PUdj haSH3 wn5by 22fL0 CR3Wu FaBtd DorJu unTls qeu0r xo40H DssQ9 D6s6S hkWx4 GF7s5 rzREu vhsbo s1haw j9ckI ekJnL TvcVB a7T4o 1olwh pydoy rGs3G k5ljZ gowEl UNXOV XkaUw 10lYO 6Ufqg Q45go KxGe3 vgwIt lqoaX nBtOq uAo3e jsbpu JLtdz TGUcX PobG5 ScDAL YPEfv o52sU FV0lM evQzq qAny0 dkDJi 0AUNz uzI6q kh13r Rubxa ecY6Q T9EL2 TmBka DPT5t qmzEf dyo0g MsGbE hPkmk 3wZBI 7kpeT 6EKGn kozHL Vnaiz 6DzDP HQ5RQ 7Ilyn 9n7Pz PwtXo kgMG2 J7gat gXcxs 4YVfC e8jGb m71Kb 9OrKY z530i" + } + ''', 200); + } else if (request.url.path == '/api/puzzle/4CZxz') { + return mockResponse(''' + { + "game": { + "id": "MQOxq7Jl", + "perf": { + "key": "blitz", + "name": "Blitz" + }, + "rated": true, + "players": [ + { + "name": "mikeroh", + "flair": "nature.t-rex", + "id": "mikeroh", + "color": "white", + "rating": 1600 + }, + { + "name": "huthayfa78", + "id": "huthayfa78", + "color": "black", + "rating": 1577 + } + ], + "pgn": "d4 d5 Bf4 h6 e3 a6 Nf3 Nf6 c3 Nh5 Be5 f6 Bg3 Nxg3 hxg3 Qd6 Bd3 Nc6 Qc2 Be6 Nbd2 O-O-O e4 dxe4 Nxe4 Qd5 O-O-O Qxa2 Nc5 Bf7 Bf5+ e6 Be4 Bxc5 dxc5 Qa1+ Qb1 Rxd1+ Rxd1 Qa4 Nd4 Nxd4 cxd4 Re8 f4 Qc4+ Bc2 Rd8 b3 Qc3 Qb2 Qe3+ Rd2 Qxg3 f5 exf5 Bxf5+ Kb8 Qa3 Qe1+ Rd1 Qe3+ Kb1 Rxd4 Rxd4 Qxd4 c6 Qd1+ Kb2 Qe2+ Bc2 bxc6 Qf8+ Be8 Qb4+ Kc8 Kc3 Qxg2 Bf5+ Kd8 Qb8+ Ke7 Qxc7+ Kf8 Qd6+ Kg8 Qe6+ Bf7", + "clock": "3+2" + }, + "puzzle": { + "id": "4CZxz", + "rating": 1058, + "plays": 2060, + "solution": [ + "e6c8", + "f7e8", + "c8e8" + ], + "themes": [ + "short", + "endgame", + "mateIn2" + ], + "initialPly": 87 + } + } + ''', 200); + } else if (request.url.path == '/api/puzzle/kcN3a') { + return mockResponse(''' + { + "game": { + "id": "bEuHKQSa", + "perf": { + "key": "rapid", + "name": "Rapid" + }, + "rated": true, + "players": [ + { + "name": "franpite", + "id": "franpite", + "color": "white", + "rating": 1773 + }, + { + "name": "cleomarzin777", + "id": "cleomarzin777", + "color": "black", + "rating": 1752 + } + ], + "pgn": "e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 a6 Be3 e5 Nf3 b5 Be2 Bb7 O-O Nxe4 Nd5 Nf6 Nxf6+ Qxf6 Bg5 Qe6 Re1 Be7 Bxe7 Qxe7 Qd3 O-O Rad1 Rd8 Nd2 Qg5 Ne4 Qg6 Nxd6", + "clock": "10+0" + }, + "puzzle": { + "id": "kcN3a", + "rating": 1069, + "plays": 294, + "solution": [ + "g6g2" + ], + "themes": [ + "middlegame", + "oneMove", + "mateIn1", + "kingsideAttack" + ], + "initialPly": 36 + } + } + ''', 200); + } else if (request.url.path == '/api/puzzle/1I9Ly') { + return mockResponse(''' + { + "game": { + "id": "DTmg6BsX", + "perf": { + "key": "rapid", + "name": "Rapid" + }, + "rated": true, + "players": [ + { + "name": "Eskozhanov_1_Semey1", + "id": "eskozhanov_1_semey1", + "color": "white", + "rating": 1928 + }, + { + "name": "sirlancelots", + "id": "sirlancelots", + "color": "black", + "rating": 2124 + } + ], + "pgn": "e4 c5 Nc3 e6 Nf3 a6 d4 cxd4 Nxd4 Qc7 g3 Nf6 Bg2 Nc6 a3 d6 O-O Be7 f4 Nxd4 Qxd4 d5 exd5", + "clock": "10+0" + }, + "puzzle": { + "id": "1I9Ly", + "rating": 1087, + "plays": 3873, + "solution": [ + "e7c5", + "c1e3", + "c5d4" + ], + "themes": [ + "opening", + "crushing", + "short", + "pin" + ], + "initialPly": 22 + } + } + ''', 200); + } + return mockResponse('', 404); +}); + +void main() { + group('StreakScreen', () { + testWidgets('meets accessibility guidelines', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + final app = await makeTestProviderScopeApp( + tester, + home: const StreakScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); + + testWidgets('Score is saved when exiting screen', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: Builder( + builder: + (context) => PlatformScaffold( + appBar: const PlatformAppBar(title: Text('Test Streak Screen')), + body: FatButton( + semanticsLabel: 'Start Streak', + child: const Text('Start Streak'), + onPressed: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StreakScreen(), + ), + ), + ), + ), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Streak')); + + // wait for puzzle to load from api and opponent move to be played + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('0\$')), findsOneWidget); + + await playMove(tester, 'e5', 'e1', orientation: Side.black); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f6', 'f4', orientation: Side.black); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f4', 'f2', orientation: Side.black); + + // Wait for next puzzle to load and score to be updated + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.textContaining(RegExp('1\$')), findsOneWidget); + + // Exit screen -> score should be saved + await tester.pageBack(); + await tester.pump(); + await tester.tap(find.text('Yes')); + await tester.pumpAndSettle(); + + // Enter streak screen again -> previous score should be loaded + await tester.tap(find.text('Start Streak')); + // Wait for puzzle to load from api and opponent move to be played + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('1\$')), findsOneWidget); + + await playMove(tester, 'e6', 'c8', orientation: Side.white); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f7', 'e8', orientation: Side.white); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'c8', 'e8', orientation: Side.white); + + // Wait for next puzzle to load and score to be updated + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('2\$')), findsOneWidget); + + // Play a wrong move + await playMove(tester, 'd8', 'd7', orientation: Side.black); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(find.text('GAME OVER'), findsOneWidget); + + // Exit screen and enter screen again + // -> local score should be cleared, so score should be 0 again + await tester.pageBack(); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Start Streak')); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('0\$')), findsOneWidget); + }); + }); +} diff --git a/test/view/settings/settigs_tab_screen_test.dart b/test/view/settings/settigs_tab_screen_test.dart deleted file mode 100644 index 0d6fa2f69a..0000000000 --- a/test/view/settings/settigs_tab_screen_test.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/view/settings/settings_tab_screen.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; - -import '../../model/auth/fake_session_storage.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; - -final client = MockClient((request) { - if (request.method == 'DELETE' && request.url.path == '/api/token') { - return mockResponse('ok', 200); - } - return mockResponse('', 404); -}); - -void main() { - group('SettingsTabScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - - final app = await buildTestApp( - tester, - home: const SettingsTabScreen(), - ); - - await tester.pumpWidget(app); - - await meetsTapTargetGuideline(tester); - - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - - await expectLater(tester, meetsGuideline(textContrastGuideline)); - handle.dispose(); - }, - variant: kPlatformVariant, - ); - - testWidgets( - "don't show signOut if no session", - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: const SettingsTabScreen(), - ); - - await tester.pumpWidget(app); - - expect(find.text('Sign out'), findsNothing); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'signout', - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: const SettingsTabScreen(), - userSession: fakeSession, - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - expect(find.text('Sign out'), findsOneWidget); - - await tester.tap( - find.widgetWithText(PlatformListTile, 'Sign out'), - warnIfMissed: false, - ); - await tester.pumpAndSettle(); - - // confirm - if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { - await tester - .tap(find.widgetWithText(CupertinoActionSheetAction, 'Sign out')); - } else { - await tester.tap(find.text('OK')); - } - await tester.pump(); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - // wait for sign out future - await tester.pump(const Duration(seconds: 1)); - - expect(find.text('Sign out'), findsNothing); - }, - variant: kPlatformVariant, - ); - }); -} diff --git a/test/view/settings/settings_tab_screen_test.dart b/test/view/settings/settings_tab_screen_test.dart new file mode 100644 index 0000000000..2c4fdb627e --- /dev/null +++ b/test/view/settings/settings_tab_screen_test.dart @@ -0,0 +1,80 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/settings/settings_tab_screen.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +import '../../model/auth/fake_session_storage.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + if (request.method == 'DELETE' && request.url.path == '/api/token') { + return mockResponse('ok', 200); + } + return mockResponse('', 404); +}); + +void main() { + group('SettingsTabScreen', () { + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + final app = await makeTestProviderScopeApp(tester, home: const SettingsTabScreen()); + + await tester.pumpWidget(app); + + await meetsTapTargetGuideline(tester); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); + + testWidgets("don't show signOut if no session", (WidgetTester tester) async { + final app = await makeTestProviderScopeApp(tester, home: const SettingsTabScreen()); + + await tester.pumpWidget(app); + + expect(find.text('Sign out'), findsNothing); + }, variant: kPlatformVariant); + + testWidgets('signout', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SettingsTabScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + getDbSizeInBytesProvider.overrideWith((_) => 1000), + ], + ); + + await tester.pumpWidget(app); + + expect(find.text('Sign out'), findsOneWidget); + + await tester.tap(find.widgetWithText(PlatformListTile, 'Sign out'), warnIfMissed: false); + await tester.pumpAndSettle(); + + // confirm + if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { + await tester.tap(find.widgetWithText(CupertinoActionSheetAction, 'Sign out')); + } else { + await tester.tap(find.text('OK')); + } + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // wait for sign out future + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Sign out'), findsNothing); + }, variant: kPlatformVariant); + }); +} diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart new file mode 100644 index 0000000000..d0dbd37d22 --- /dev/null +++ b/test/view/study/study_list_screen_test.dart @@ -0,0 +1,693 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + group('Study list screen', () { + testWidgets('Scrolling down loads next page', (WidgetTester tester) async { + final requestedPages = []; + final mockClient = MockClient((request) { + if (request.url.path == '/study/all/hot') { + requestedPages.add(int.parse(request.url.queryParameters['page']!)); + + if (request.url.queryParameters['page'] == '1') { + return mockResponse(kStudyAllHotPage1Response, 200); + } + if (request.url.queryParameters['page'] == '2') { + return mockResponse(kStudyAllHotPage2Response, 200); + } + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + await tester.pumpWidget(app); + + // Wait for study list to load + await tester.pump(); + + expect(find.text('First Study Page 1'), findsOneWidget); + expect(find.text('First Study Page 2'), findsNothing); // On page 2 + + await tester.dragUntilVisible( + find.text('First Study Page 2'), + find.byType(ListView), + const Offset(0, -250), + ); + + expect(requestedPages, [1, 2]); + }); + + testWidgets('Searching', (WidgetTester tester) async { + final requestedUrls = []; + final mockClient = MockClient((request) { + requestedUrls.add(request.url.toString()); + if (request.url.path == '/study/all/hot' && request.url.queryParameters['page'] == '1') { + return mockResponse(kStudyAllHotPage1Response, 200); + } else if (request.url.path == '/study/search') { + if (request.url.queryParameters['q'] == 'Magnus') { + return mockResponse(''' +{ + "paginator": { + "currentPage": 1, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "Magnus Carlsen Games", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "tom-anders", + "id": "tom-anders" + }, + "chapters": [ + "Chapter 1", + "Chapter 2" + ], + "topics": [ ], + "members": [ ] + } + ], + "previousPage": null, + "nextPage": null, + "nbResults": 1, + "nbPages": 1 + } +} + ''', 200); + } + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + await tester.pumpWidget(app); + + // Wait for default study list (all/hot) to load + await tester.pump(); + + await tester.tap(find.byType(SearchBar)); + + await tester.enterText(find.byType(TextField), 'Magnus'); + // submit the search + await tester.testTextInput.receiveAction(TextInputAction.done); + + // Wait for search results to load + await tester.pumpAndSettle(); + + expect(find.text('Magnus Carlsen Games'), findsOneWidget); + + // loads context menu + await tester.longPress(find.text('Magnus Carlsen Games')); + await tester.pumpAndSettle(); + + expect(find.textContaining('Chapter 1'), findsOneWidget); + expect(find.textContaining('Chapter 2'), findsOneWidget); + expect(find.textContaining('tom-anders'), findsOneWidget); + + expect(requestedUrls, [ + 'https://lichess.dev/study/all/hot?page=1', + 'https://lichess.dev/study/search?page=1&q=Magnus', + ]); + }); + }); +} + +// Output based on the following command (with some modifications): +// curl -X GET 'https://lichess.dev/study/all/hot' -H "Accept: application/json" 2> /dev/null | jq +const kStudyAllHotPage1Response = ''' +{ + "paginator": { + "currentPage": 1, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "First Study Page 1", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Stevanic, David - Ilamparthi A R", + "Quizon, Daniel - Raahul V S" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "6QZbnn0u", + "name": "test", + "liked": false, + "likes": 1, + "updatedAt": 1722185354120, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Larkin, Vladyslav - Monteiro, Jose Macedo", + "Campora, Daniel H. - Pinto, Jose Joao Meireles Alves" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + } + ] + }, + { + "id": "Oc2oNWPH", + "name": "test", + "liked": false, + "likes": 1, + "updatedAt": 1722185242763, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Larkin, Vladyslav - Monteiro, Jose Flavio", + "Campora, Daniel H. - Pinto, Fernando Jose Seixas" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "thibault", + "flair": "smileys.disguised-face", + "patron": true, + "id": "thibault" + }, + "role": "w" + } + ] + }, + { + "id": "WLpIyPTB", + "name": "Round 9", + "liked": false, + "likes": 1, + "updatedAt": 1722016933485, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Macovei, Andrei", + "Gan-Erdene, Sugar - Assaubayeva, Bibisara", + "Chinguun, Sumiya - Materia, Marco", + "Sukandar, Irine Kharisma - Moiseenko, Alexander" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "7BaR9QW4", + "name": "Round 8", + "liked": false, + "likes": 1, + "updatedAt": 1722016933188, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Assaubayeva, Bibisara - Madaminov, Mukhiddin", + "Chinguun, Sumiya - Gan-Erdene, Sugar", + "Sukandar, Irine Kharisma - Macovei, Andrei", + "Materia, Marco - Sasikiran, Krishnan" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "oZdBmHQG", + "name": "Round 7", + "liked": false, + "likes": 1, + "updatedAt": 1722016933000, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Materia, Marco", + "Sasikiran, Krishnan - Sukandar, Irine Kharisma", + "Gan-Erdene, Sugar - Harsha Bharathakoti", + "Juksta, Karolis - Assaubayeva, Bibisara" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "wVTCciEa", + "name": "Round 6", + "liked": false, + "likes": 1, + "updatedAt": 1722016932803, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Jumabayev, Rinat - Madaminov, Mukhiddin", + "Materia, Marco - Ganguly, Surya Shekhar", + "Nesterov, Arseniy - Gan-Erdene, Sugar", + "Kersten, Uwe - Sasikiran, Krishnan" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9WB6pTtb", + "name": "Round 5", + "liked": false, + "likes": 1, + "updatedAt": 1722016932628, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Nesterov, Arseniy", + "Jumabayev, Rinat - Materia, Marco", + "Munguntuul, Batkhuyag - Ganguly, Surya Shekhar", + "Sasikiran, Krishnan - Harsha Bharathakoti" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "vWxkJ9Dp", + "name": "Round 4", + "liked": false, + "likes": 1, + "updatedAt": 1722016932385, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Gan-Erdene, Sugar - Sasikiran, Krishnan", + "Nesterov, Arseniy - Munkhzul, Turmunkh", + "Harsha Bharathakoti - Munguntuul, Batkhuyag", + "Tahay, Alexis - Jumabayev, Rinat" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "fJSDBhRQ", + "name": "Round 3", + "liked": false, + "likes": 1, + "updatedAt": 1722016932195, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Sasikiran, Krishnan - Haimovich, Tal", + "Arcuti, Davide - Nesterov, Arseniy", + "Peycheva, Gergana - Harsha Bharathakoti", + "Jumabayev, Rinat - Kersten, Uwe" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9xh4D5mM", + "name": "Round 2", + "liked": false, + "likes": 1, + "updatedAt": 1722016932070, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Toktomushev, Teimur - Ganguly, Surya Shekhar", + "Moiseenko, Alexander - Vemparala, Nikash", + "Papaux, Steve - Sasikiran, Krishnan", + "Nesterov, Arseniy - Bex, Pierre-Alain" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "D7TAt3rj", + "name": "Round 1", + "liked": false, + "likes": 1, + "updatedAt": 1722016931939, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Ganguly, Surya Shekhar - Lang, Fabian", + "Tcheau, Alain - Moiseenko, Alexander", + "Sasikiran, Krishnan - Arulanantham, Aneet", + "Ranieri, Pierpaolo - Nesterov, Arseniy" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9rZ88BkF", + "name": "Round 8", + "liked": false, + "likes": 1, + "updatedAt": 1718628262858, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "k79RXsyo", + "name": "Round 7", + "liked": false, + "likes": 1, + "updatedAt": 1718628262852, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "yqPw9epZ", + "name": "Round 6", + "liked": false, + "likes": 1, + "updatedAt": 1718628262846, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "wSLKGjre", + "name": "Last Study Page 1", + "liked": false, + "likes": 1, + "updatedAt": 1718628262840, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + } + ], + "previousPage": null, + "nextPage": 2, + "nbResults": 9999, + "nbPages": 2 + } +} +'''; + +const kStudyAllHotPage2Response = ''' +{ + "paginator": { + "currentPage": 2, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "First Study Page 2", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Stevanic, David - Ilamparthi A R", + "Quizon, Daniel - Raahul V S" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + } + ], + "previousPage": null, + "nextPage": null, + "nbResults": 9999, + "nbPages": 2 + } +} +'''; diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart new file mode 100644 index 0000000000..11b3e63f84 --- /dev/null +++ b/test/view/study/study_screen_test.dart @@ -0,0 +1,385 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/view/study/study_screen.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +class MockStudyRepository extends Mock implements StudyRepository {} + +const testId = StudyId('test-id'); + +StudyChapter makeChapter({ + required StudyChapterId id, + Side orientation = Side.white, + bool gamebook = false, +}) { + return StudyChapter( + id: id, + setup: StudyChapterSetup( + id: null, + orientation: orientation, + variant: Variant.standard, + fromFen: null, + ), + conceal: null, + features: (computer: false, explorer: false), + gamebook: gamebook, + practise: false, + ); +} + +Study makeStudy({ + StudyChapter? chapter, + IList? chapters, + IList hints = const IList.empty(), + IList deviationComments = const IList.empty(), +}) { + chapter = chapter ?? makeChapter(id: const StudyChapterId('1')); + return Study( + id: testId, + name: '', + liked: false, + likes: 0, + ownerId: null, + features: (cloneable: false, chat: false, sticky: false), + topics: const IList.empty(), + chapters: chapters ?? IList([StudyChapterMeta(id: chapter.id, name: '', fen: null)]), + chapter: chapter, + hints: hints, + deviationComments: deviationComments, + ); +} + +void main() { + group('Study screen', () { + testWidgets('Displays PGN moves and comments', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + + when( + () => mockRepository.getStudy(id: testId), + ).thenAnswer((_) async => (makeStudy(), '{root comment} 1. e4 {wow} e5 {such chess}')); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], + ); + await tester.pumpWidget(app); + + // Wait for study to load + await tester.pumpAndSettle(); + + expect(find.text('root comment'), findsOneWidget); + expect(find.text('1. e4'), findsOneWidget); + expect(find.textContaining('wow'), findsOneWidget); + expect(find.textContaining('e5'), findsOneWidget); + expect(find.textContaining('such chess'), findsOneWidget); + }); + + testWidgets('Switch between chapters', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + + final studyChapter1 = makeStudy( + chapter: makeChapter(id: const StudyChapterId('1')), + chapters: IList(const [ + StudyChapterMeta(id: StudyChapterId('1'), name: 'Chapter 1', fen: null), + StudyChapterMeta(id: StudyChapterId('2'), name: 'Chapter 2', fen: null), + ]), + ); + + final studyChapter2 = studyChapter1.copyWith( + chapter: makeChapter(id: const StudyChapterId('2')), + ); + + when( + () => mockRepository.getStudy(id: testId), + ).thenAnswer((_) async => (studyChapter1, '{pgn 1}')); + when( + () => mockRepository.getStudy(id: testId, chapterId: const StudyChapterId('1')), + ).thenAnswer((_) async => (studyChapter1, '{pgn 1}')); + when( + () => mockRepository.getStudy(id: testId, chapterId: const StudyChapterId('2')), + ).thenAnswer((_) async => (studyChapter2, '{pgn 2}')); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsOneWidget); + expect(find.text('Chapter 2'), findsNothing); + + expect(find.text('pgn 1'), findsOneWidget); + expect(find.text('pgn 2'), findsNothing); + + // 2nd press should not have any effect, we're already at the last chapter + await tester.tap(find.text('Next chapter')); + // Wait for next chapter to load + await tester.pumpAndSettle(); + await tester.tap(find.text('Next chapter')); + // Wait for next chapter to load (even though it shouldn't) + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsNothing); + expect(find.text('Chapter 2'), findsOneWidget); + + expect(find.text('pgn 1'), findsNothing); + expect(find.text('pgn 2'), findsOneWidget); + + // Open chapter selection dialog + await tester.tap(find.byTooltip('2 Chapters')); + // Wait for dialog to open + await tester.pumpAndSettle(); + + expect( + find.descendant(of: find.byType(Scrollable), matching: find.text('Chapter 1')), + findsOneWidget, + ); + expect( + find.descendant(of: find.byType(Scrollable), matching: find.text('Chapter 2')), + findsOneWidget, + ); + + await tester.tap(find.text('Chapter 1')); + // Wait for chapter to load + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsOneWidget); + expect(find.text('Chapter 2'), findsNothing); + + expect(find.text('pgn 1'), findsOneWidget); + expect(find.text('pgn 2'), findsNothing); + }); + + testWidgets('Can play moves for both sides', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy(chapter: makeChapter(id: const StudyChapterId('1'), orientation: Side.black)), + '', + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + await playMove(tester, 'e2', 'e4', orientation: Side.black); + + expect(find.byKey(const Key('e2-whitepawn')), findsNothing); + expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget); + + await playMove(tester, 'e7', 'e5', orientation: Side.black); + + expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget); + expect(find.byKey(const Key('e7-blackpawn')), findsNothing); + + expect(find.text('1. e4'), findsOneWidget); + expect(find.text('e5'), findsOneWidget); + }); + + testWidgets('Interactive study', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + ), + ''' +[Event "Improve Your Chess Calculation: Candidates| Ex 1: Hard"] +[Site "https://lichess.org/study/xgZOEizT/OfF4eLmN"] +[Result "*"] +[Variant "Standard"] +[ECO "?"] +[Opening "?"] +[Annotator "https://lichess.org/@/RushConnectedPawns"] +[FEN "r1b2rk1/3pbppp/p3p3/1p6/2qBPP2/P1N2R2/1PPQ2PP/R6K w - - 0 1"] +[SetUp "1"] +[UTCDate "2024.10.23"] +[UTCTime "02:04:11"] +[ChapterMode "gamebook"] + +{ We begin our lecture with an 'easy but not easy' example. White to play and win. } +1. Nd5!! { Brilliant! You noticed that the queen on c4 was kinda smothered. } (1. Ne2? { Not much to say after ...Qc7. }) 1... exd5 2. Rc3 Qa4 3. Rg3! { A fork, threatening Rg7 & b3. } { [%csl Gg7][%cal Gg3g7,Gd4g7,Gb2b3] } (3. Rxc8?? { Uh-oh! After Rc8, b3, there is the counter-sac Rxc2, which is winning for black!! } 3... Raxc8 4. b3 Rxc2!! 5. Qxc2 Qxd4 \$19) 3... g6 4. b3 \$18 { ...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples. } * + ''', + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + const introText = + "We begin our lecture with an 'easy but not easy' example. White to play and win."; + + expect(find.text(introText), findsOneWidget); + + expect( + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), + findsNothing, + ); + + // Play a wrong move + await playMove(tester, 'c3', 'a2'); + expect(find.text("That's not the move!"), findsOneWidget); + expect(find.text(introText), findsNothing); + + // Wrong move will be taken back automatically after a short delay + await tester.pump(const Duration(seconds: 1)); + expect(find.text("That's not move!"), findsNothing); + expect(find.text(introText), findsOneWidget); + + // Play another wrong move, but this one has an explicit comment + await playMove(tester, 'c3', 'e2'); + + // If there's an explicit comment, the move is not taken back automatically + // Verify this by waiting the same duration as above + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Not much to say after ...Qc7.'), findsOneWidget); + expect(find.text(introText), findsNothing); + + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text(introText), findsOneWidget); + + // Play the correct move + await playMove(tester, 'c3', 'd5'); + + expect( + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), + findsOneWidget, + ); + + // The move has an explicit feedback comment, so opponent move should not be played automatically + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), + findsOneWidget, + ); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect(find.text('What would you play in this position?'), findsOneWidget); + + await playMove(tester, 'f3', 'c3'); + expect(find.text('Good move'), findsOneWidget); + + // No explicit feedback, so opponent move should be played automatically after delay + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('What would you play in this position?'), findsOneWidget); + + await playMove(tester, 'c3', 'g3'); + expect(find.text('A fork, threatening Rg7 & b3.'), findsOneWidget); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect(find.text('What would you play in this position?'), findsOneWidget); + + await playMove(tester, 'b2', 'b3'); + + expect( + find.text( + "...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples.", + ), + findsOneWidget, + ); + + expect(find.byTooltip('Play again'), findsOneWidget); + expect(find.byTooltip('Next chapter'), findsOneWidget); + expect(find.byTooltip('Analysis board'), findsOneWidget); + }); + + testWidgets('Interactive study hints and deviation comments', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + hints: ['Hint 1', null, null, null].lock, + deviationComments: [null, 'Shown if any move other than d4 is played', null, null].lock, + ), + '1. e4 (1. d4 {Shown if d4 is played}) e5 2. Nf3', + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + expect(find.text('Get a hint'), findsOneWidget); + expect(find.text('Hint 1'), findsNothing); + + await tester.tap(find.text('Get a hint')); + await tester.pump(); // Wait for hint to be shown + expect(find.text('Hint 1'), findsOneWidget); + expect(find.text('Get a hint'), findsNothing); + + await playMove(tester, 'e2', 'e3'); + expect(find.text('Shown if any move other than d4 is played'), findsOneWidget); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + await playMove(tester, 'd2', 'd4'); + expect(find.text('Shown if d4 is played'), findsOneWidget); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text('View the solution'), findsOneWidget); + await tester.tap(find.byTooltip('View the solution')); + // Wait for correct move and opponent's response to be played + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Get a hint'), findsNothing); + + // Play a wrong move again - generic feedback should be shown + await playMove(tester, 'a2', 'a3'); + expect(find.text("That's not the move!"), findsOneWidget); + // Wait for wrong move to be taken back + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('What would you play in this position?'), findsOneWidget); + expect(find.text("That's not the move!"), findsNothing); + }); + }); +} diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index f060f3cf28..70e18efd3a 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -1,11 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player') { @@ -16,36 +16,29 @@ final client = MockClient((request) { void main() { group('LeaderboardScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( - tester, - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - home: const LeaderboardScreen(), - ); + final app = await makeTestProviderScopeApp( + tester, + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + home: const LeaderboardScreen(), + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); - // TODO find why it fails on android - // await meetsTapTargetGuideline(tester); + // TODO find why it fails on android + // await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - await expectLater(tester, meetsGuideline(textContrastGuideline)); - } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + await expectLater(tester, meetsGuideline(textContrastGuideline)); + } + handle.dispose(); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index b51d501dda..17611b545a 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player/top/1/standard') { @@ -17,47 +17,31 @@ final client = MockClient((request) { void main() { group('LeaderboardWidget', () { - testWidgets( - 'accessibility and basic info showing test', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( - tester, - home: Column(children: [LeaderboardWidget()]), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + testWidgets('accessibility and basic info showing test', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final app = await makeTestProviderScopeApp( + tester, + home: Column(children: [LeaderboardWidget()]), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(const Duration(milliseconds: 50)); - for (final name in [ - 'Svetlana', - 'Marcel', - 'Anthony', - 'Patoulatchi', - 'Cerdan', - ]) { - expect( - find.widgetWithText(LeaderboardListTile, name), - findsOneWidget, - ); - } + for (final name in ['Svetlana', 'Marcel', 'Anthony', 'Patoulatchi', 'Cerdan']) { + expect(find.widgetWithText(LeaderboardListTile, name), findsOneWidget); + } - // await meetsTapTargetGuideline(tester); + // await meetsTapTargetGuideline(tester); - // await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + // await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - // if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - // await expectLater(tester, meetsGuideline(textContrastGuideline)); - // } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + // if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + // await expectLater(tester, meetsGuideline(textContrastGuideline)); + // } + handle.dispose(); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index a5791e4ca8..95a8a02562 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -1,14 +1,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/perf_stats_screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import '../../model/auth/fake_auth_repository.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/user/${fakeUser.id}/perf/${testPerf.name}') { @@ -22,85 +22,68 @@ final client = MockClient((request) { void main() { group('PerfStatsScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( - tester, - home: PerfStatsScreen( - user: fakeUser, - perf: testPerf, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(client, ref); - }), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PerfStatsScreen(user: fakeUser, perf: testPerf), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), + ], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for auth state and perf stats - await tester.pump(const Duration(milliseconds: 50)); + // wait for auth state and perf stats + await tester.pump(const Duration(milliseconds: 50)); - await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await meetsTapTargetGuideline(tester); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - await expectLater(tester, meetsGuideline(textContrastGuideline)); - } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + await expectLater(tester, meetsGuideline(textContrastGuideline)); + } + handle.dispose(); + }, variant: kPlatformVariant); - testWidgets( - 'screen loads, required stats are shown', - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: PerfStatsScreen( - user: fakeUser, - perf: testPerf, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(client, ref); - }), - ], - ); + testWidgets('screen loads, required stats are shown', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: PerfStatsScreen(user: fakeUser, perf: testPerf), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), + ], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for auth state and perf stats - await tester.pump(const Duration(milliseconds: 50)); + // wait for auth state and perf stats + await tester.pump(const Duration(milliseconds: 50)); - final requiredStatsValues = [ - '50.24', // Deviation - '20', // Progression in last 12 games - '0', // Berserked games - '0', // Tournament games - '3', // Rated games - '2', // Won games - '2', // Lost games - '1', // Drawn games - '1', // Disconnections - ]; + final requiredStatsValues = [ + '50.24', // Deviation + '20', // Progression in last 12 games + '0', // Berserked games + '0', // Tournament games + '3', // Rated games + '2', // Won games + '2', // Lost games + '1', // Drawn games + '1', // Disconnections + ]; - // rating - expect(find.text('1500.42'), findsOneWidget); + // rating + expect(find.text('1500'), findsOneWidget); - for (final val in requiredStatsValues) { - expect( - find.widgetWithText(PlatformCard, val), - findsAtLeastNWidgets(1), - ); - } - }, - variant: kPlatformVariant, - ); + for (final val in requiredStatsValues) { + expect(find.widgetWithText(PlatformCard, val), findsAtLeastNWidgets(1)); + } + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index deebdfa861..eefe7e3c89 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -3,12 +3,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player/autocomplete') { @@ -22,83 +22,69 @@ final client = MockClient((request) { void main() { group('SearchScreen', () { - testWidgets( - 'should see search results', - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: const SearchScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - final textFieldFinder = - debugDefaultTargetPlatformOverride == TargetPlatform.iOS - ? find.byType(CupertinoSearchTextField) - : find.byType(SearchBar); - - await tester.enterText(textFieldFinder, 'joh'); - - // await debouce call - await tester.pump(const Duration(milliseconds: 300)); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // await response - await tester.pumpAndSettle(const Duration(milliseconds: 100)); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('Players with "joh"'), findsOneWidget); - expect(find.byType(UserListTile), findsNWidgets(2)); - expect(find.text('John Doe'), findsOneWidget); - expect(find.text('John Doe 2'), findsOneWidget); - - // await debouce call for saving search history - await tester.pump(const Duration(seconds: 2)); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'should see "no result" when search finds nothing', - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: const SearchScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - final textFieldFinder = - debugDefaultTargetPlatformOverride == TargetPlatform.iOS - ? find.byType(CupertinoSearchTextField) - : find.byType(SearchBar); - - await tester.enterText(textFieldFinder, 'johnny'); - // await debouce call - await tester.pump(const Duration(milliseconds: 300)); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // await response - await tester.pumpAndSettle(const Duration(milliseconds: 100)); - - expect(find.text('Players with "johnny"'), findsNothing); - expect(find.text('No results'), findsOneWidget); - - // await debouce call for saving search history - await tester.pump(const Duration(seconds: 2)); - }, - variant: kPlatformVariant, - ); + testWidgets('should see search results', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SearchScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + final textFieldFinder = + debugDefaultTargetPlatformOverride == TargetPlatform.iOS + ? find.byType(CupertinoSearchTextField) + : find.byType(SearchBar); + + await tester.enterText(textFieldFinder, 'joh'); + + // await debouce call + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // await response + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('Players with "joh"'), findsOneWidget); + expect(find.byType(UserListTile), findsNWidgets(2)); + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('John Doe 2'), findsOneWidget); + + // await debouce call for saving search history + await tester.pump(const Duration(seconds: 2)); + }, variant: kPlatformVariant); + + testWidgets('should see "no result" when search finds nothing', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SearchScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + final textFieldFinder = + debugDefaultTargetPlatformOverride == TargetPlatform.iOS + ? find.byType(CupertinoSearchTextField) + : find.byType(SearchBar); + + await tester.enterText(textFieldFinder, 'johnny'); + // await debouce call + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // await response + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.text('Players with "johnny"'), findsNothing); + expect(find.text('No results'), findsOneWidget); + + // await debouce call for saving search history + await tester.pump(const Duration(seconds: 2)); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index 5f3999cc92..fdc11214e4 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -1,13 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import '../../model/user/user_repository_test.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/games/user/$testUserId') { @@ -15,8 +15,7 @@ final client = MockClient((request) { } else if (request.url.path == '/api/user/$testUserId') { return mockResponse(testUserResponse, 200); } else if (request.url.path == '/api/users/status') { - return mockResponse( - ''' + return mockResponse(''' [ { "id": "$testUserId", @@ -24,9 +23,7 @@ final client = MockClient((request) { "online": true } ] -''', - 200, - ); +''', 200); } else if (request.url.path == '/api/user/$testUserId/activity') { return mockResponse(userActivityResponse, 200); } @@ -35,36 +32,26 @@ final client = MockClient((request) { void main() { group('UserScreen', () { - testWidgets( - 'should see activity and recent games', - (WidgetTester tester) async { - final app = await buildTestApp( - tester, - home: const UserScreen(user: testUser), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + testWidgets('should see activity and recent games', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const UserScreen(user: testUser), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for user request - await tester.pump(const Duration(milliseconds: 50)); + // wait for user request + await tester.pump(const Duration(milliseconds: 50)); - // full name at the top - expect( - find.text('John Doe'), - findsOneWidget, - ); + // full name at the top + expect(find.text('John Doe'), findsOneWidget); - // wait for recent games and activity - await tester.pump(const Duration(milliseconds: 50)); + // wait for recent games and activity + await tester.pump(const Duration(milliseconds: 50)); - expect(find.text('Activity'), findsOneWidget); - }, - variant: kPlatformVariant, - ); + expect(find.text('Activity'), findsOneWidget); + }, variant: kPlatformVariant); }); } diff --git a/test/widgets/adaptive_choice_picker_test.dart b/test/widgets/adaptive_choice_picker_test.dart index cc76f60067..5cbaed910d 100644 --- a/test/widgets/adaptive_choice_picker_test.dart +++ b/test/widgets/adaptive_choice_picker_test.dart @@ -4,121 +4,105 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import '../test_utils.dart'; +import '../test_helpers.dart'; -enum TestEnumLarge { - one, - two, - three, - four, - five, - six, - seven, - eight, - nine, - ten, - eleven -} +enum TestEnumLarge { one, two, three, four, five, six, seven, eight, nine, ten, eleven } enum TestEnumSmall { one, two, three } void main() { - testWidgets( - 'showChoicePicker call onSelectedItemChanged (large choices)', - (WidgetTester tester) async { - final List selectedItems = []; + testWidgets('showChoicePicker call onSelectedItemChanged (large choices)', ( + WidgetTester tester, + ) async { + final List selectedItems = []; - await tester.pumpWidget( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: Scaffold( - body: Builder( - builder: (context) { - return Center( - child: ElevatedButton( - child: const Text('Show picker'), - onPressed: () { - showChoicePicker( - context, - choices: TestEnumLarge.values, - selectedItem: TestEnumLarge.one, - labelBuilder: (choice) => Text(choice.name), - onSelectedItemChanged: (choice) { - selectedItems.add(choice); - }, - ); - }, - ), - ); - }, - ), + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show picker'), + onPressed: () { + showChoicePicker( + context, + choices: TestEnumLarge.values, + selectedItem: TestEnumLarge.one, + labelBuilder: (choice) => Text(choice.name), + onSelectedItemChanged: (choice) { + selectedItems.add(choice); + }, + ); + }, + ), + ); + }, ), ), - ); + ), + ); - await tester.tap(find.text('Show picker')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); - // with large choices (>= 6), on iOS the picker scrolls - if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { - // scroll 2 items (2 * 40 height) - await tester.drag( - find.text('one'), - const Offset(0.0, -80.0), - warnIfMissed: false, - ); // has an IgnorePointer - expect(selectedItems, []); - await tester.pumpAndSettle(); // await for scroll ends - // only third item is selected as the scroll ends - expect(selectedItems, [TestEnumLarge.three]); - } else { - await tester.tap(find.text('three')); - expect(selectedItems, [TestEnumLarge.three]); - } - }, - variant: kPlatformVariant, - ); + // with large choices (>= 6), on iOS the picker scrolls + if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { + // scroll 2 items (2 * 40 height) + await tester.drag( + find.text('one'), + const Offset(0.0, -80.0), + warnIfMissed: false, + ); // has an IgnorePointer + expect(selectedItems, []); + await tester.pumpAndSettle(); // await for scroll ends + // only third item is selected as the scroll ends + expect(selectedItems, [TestEnumLarge.three]); + } else { + await tester.tap(find.text('three')); + expect(selectedItems, [TestEnumLarge.three]); + } + }, variant: kPlatformVariant); - testWidgets( - 'showChoicePicker call onSelectedItemChanged (small choices)', - (WidgetTester tester) async { - final List selectedItems = []; + testWidgets('showChoicePicker call onSelectedItemChanged (small choices)', ( + WidgetTester tester, + ) async { + final List selectedItems = []; - await tester.pumpWidget( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: Scaffold( - body: Builder( - builder: (context) { - return Center( - child: ElevatedButton( - child: const Text('Show picker'), - onPressed: () { - showChoicePicker( - context, - choices: TestEnumSmall.values, - selectedItem: TestEnumSmall.one, - labelBuilder: (choice) => Text(choice.name), - onSelectedItemChanged: (choice) { - selectedItems.add(choice); - }, - ); - }, - ), - ); - }, - ), + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show picker'), + onPressed: () { + showChoicePicker( + context, + choices: TestEnumSmall.values, + selectedItem: TestEnumSmall.one, + labelBuilder: (choice) => Text(choice.name), + onSelectedItemChanged: (choice) { + selectedItems.add(choice); + }, + ); + }, + ), + ); + }, ), ), - ); + ), + ); - await tester.tap(find.text('Show picker')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); - // With small choices, on iOS the picker is an action sheet - await tester.tap(find.text('three')); - expect(selectedItems, [TestEnumSmall.three]); - }, - variant: kPlatformVariant, - ); + // With small choices, on iOS the picker is an action sheet + await tester.tap(find.text('three')); + expect(selectedItems, [TestEnumSmall.three]); + }, variant: kPlatformVariant); } diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart new file mode 100644 index 0000000000..3d260ede55 --- /dev/null +++ b/test/widgets/board_table_test.dart @@ -0,0 +1,130 @@ +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/widgets/board_table.dart'; + +import '../test_helpers.dart'; +import '../test_provider_scope.dart'; + +void main() { + testWidgets('board background size should match board size on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [Text('Top table')], + ), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [Text('Bottom table')], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final backgroundSize = tester.getSize(find.byType(SolidColorChessboardBackground)); + + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square on $surface', + ); + + final boardSize = tester.getSize(find.byType(Chessboard)); + + expect(boardSize.width, boardSize.height, reason: 'Board size is square on $surface'); + + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size on $surface', + ); + } + }, variant: kPlatformVariant); + + testWidgets('board size and table side size should be harmonious on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [Text('Top table')], + ), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [Text('Bottom table')], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final isPortrait = surface.aspectRatio < 1.0; + final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); + + if (isPortrait) { + final expectedBoardSize = isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final topTableSize = tester.getSize(find.byKey(const ValueKey('top_table'))); + final bottomTableSize = tester.getSize(find.byKey(const ValueKey('bottom_table'))); + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = surface.shortestSide - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); + final minSideWidth = min(surface.longestSide - goldenBoardSize - 16.0 * 3, 250.0); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + bottomTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Bottom table width should be at least $minSideWidth on $surface', + ); + expect( + topTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Top table width should be at least $minSideWidth on $surface', + ); + } + } + }, variant: kPlatformVariant); +} diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart new file mode 100644 index 0000000000..555ca56d0d --- /dev/null +++ b/test/widgets/clock_test.dart @@ -0,0 +1,238 @@ +import 'package:clock/clock.dart' as clock; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; + +void main() { + group('Clock', () { + testWidgets('shows milliseconds when time < 1s and active is false', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp(home: Clock(timeLeft: Duration(seconds: 1), active: true)), + ); + + expect(find.text('0:01.0', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp(home: Clock(timeLeft: Duration(milliseconds: 988), active: false)), + duration: const Duration(milliseconds: 1000), + ); + + expect(find.text('0:00.98', findRichText: true), findsOneWidget); + }); + }); + + group('CountdownClockBuilder', () { + Widget clockBuilder(BuildContext context, Duration timeLeft) { + final mins = timeLeft.inMinutes.remainder(60); + final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); + final tenths = timeLeft.inMilliseconds.remainder(1000) ~/ 100; + return Text('$mins:$secs.$tenths'); + } + + testWidgets('does not tick when not active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + + await tester.pump(const Duration(seconds: 2)); + expect(find.text('0:10.0'), findsOneWidget); + }); + + testWidgets('ticks when active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('update time by changing widget configuration', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:11.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.9'), findsOneWidget); + await tester.pump(const Duration(seconds: 11)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('do not update if clockUpdatedAt is same', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:09.7'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('stops when active become false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + + // clock is rebuilt with same time but inactive: + // the time is kept and the clock stops counting the elapsed time + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + duration: const Duration(milliseconds: 100), + ); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + }); + + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 250), + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 200), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag negative start delay', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 200)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 100), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + }); +} diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index 8ea73baed3..3d336b4a68 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -1,47 +1,46 @@ + All games + Are you sure? + Cancel takeback offer + Clear + Clear saved move + Join a game + Feedback + Hello, %s + Hello + Hide variation Home - Puzzles - Tools - Watch - Settings + Live streamers You must be logged in to view this page. - System colors - Feedback + No results + You are not following any user. OK + Players with "%s" + Magnify dragged piece + Do you want to end this run? + Nothing to show, please change the filters + Nothing to show. Play some runs of Puzzle Storm. + Solve as many puzzles as possible in 3 minutes. + You will lose your current streak and your score will be saved. + Play puzzles from your favorite openings, or choose a theme. + Puzzles + Recent searches Haptic feedback Immersive mode Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens. - You are not following any user. - All games - Recent searches - Clear - Players with "%s" - No results - Are you sure? - You will lose your current streak and your score will be saved. - Nothing to show. Play some runs of Puzzle Storm. - Share this puzzle - Share game URL + Settings Share PGN + Share game URL Share position as FEN - Show variations - Hide variation + Share this puzzle Show comments - Do you want to end this run? - Nothing to show, please change the filters - Cancel takeback offer - Cancel draw offer - Waiting for opponent to join... - Blindfold - Live streamers - Join a game - Clear saved move - Something went wrong. Show result - Play puzzles from your favorite openings, or choose a theme. - Solve as many puzzles as possible in 3 minutes. - Hello, %s - Hello - Magnify dragged piece + Show variations + Something went wrong. + System colors + Theme + Tools + Waiting for opponent to join... + Watch